* 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>
57 lines
1.6 KiB
Go
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
|
|
}
|