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 }