Files
gochat/backend/internal/auth/access_validator.go
T
Rogeeandrogee 6c78820a1f H-338: close H-335 release blockers (#59)
* H-16: align takeover with channel AI workflow (#2)

* feat(conversations): complete manual AI takeover

* fix(conversations): align AI takeover flow with channel AI

* fix(conversations): close takeover review gaps

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* feat(shangwutong): sync customer names back to channel (#3)

Co-authored-by: Rogee <rogee@ipao.vip>

* fix(shangwutong): close contact sync review gaps (#4)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-28: harden Shangwutong CID sync (#5)

* fix(shangwutong): close contact sync review gaps

* fix(shangwutong): harden CID sync boundaries

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* fix(conversations): sync AI takeover exit in realtime (#6)

Co-authored-by: Rogee <rogee@ipao.vip>

* test(shangwutong): cover CID rename reliability (#7)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-43: fix WEB Captain takeover E2E flow (#8)

* test(shangwutong): cover CID rename reliability

* H-43: fix WEB Captain takeover flow

* H-48: preserve compatible provider model

* H-49: make Captain takeover atomic

* H-50: prevent duplicate widget initialization

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* H-55: make Captain bindings atomic (#9)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-60: harden Captain migration rollback and concurrency

* chore(agent): baseline — uncommitted work from the local directory

* H-335: add safe Captain skills and user deactivation

* H-338: close auth and Captain review blockers

* H-338: close assignment and session races

* H-338: close assignment and websocket invalidation gaps

* H-338: enforce assignment write invariants

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-20 10:21:19 +08:00

57 lines
1.6 KiB
Go

package auth
import (
"context"
"errors"
"fmt"
"time"
"github.com/gochat/gochat/internal/model"
"gorm.io/gorm"
)
var (
ErrUserInactive = errors.New("user account is inactive")
ErrSessionRevoked = errors.New("session revoked")
)
// ValidateUserAccessToken is the shared HTTP/WebSocket access-token gate.
func ValidateUserAccessToken(ctx context.Context, jwtService *JWTService, db *gorm.DB, token string) (*Claims, *model.User, error) {
claims, err := jwtService.ValidateAccessToken(token)
if err != nil || db == nil {
return claims, nil, err
}
user, err := ValidateUserAccess(ctx, db, claims.UserID, claims.ClientID)
if err != nil {
return nil, nil, err
}
return claims, user, nil
}
// ValidateUserAccess rechecks the mutable user and session state behind an
// already-validated access token.
func ValidateUserAccess(ctx context.Context, db *gorm.DB, userID uint, clientID string) (*model.User, error) {
if db == nil {
return nil, nil
}
if clientID != "" {
var session model.UserSession
if err := db.WithContext(ctx).Where("user_id = ? AND client_id = ?", userID, clientID).First(&session).Error; err != nil {
return nil, ErrSessionRevoked
}
if session.LastActivityAt == nil || session.LastActivityAt.Before(time.Now().Add(-5*time.Minute)) {
now := time.Now().UTC()
_ = db.WithContext(ctx).Model(&session).Updates(map[string]any{"last_activity_at": now, "updated_at": now}).Error
}
}
var user model.User
if err := db.WithContext(ctx).First(&user, userID).Error; err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
if !user.Active {
return nil, ErrUserInactive
}
return &user, nil
}