package auth import ( "context" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "strings" "time" "git.ilapage.cn/ila/yovision/Bell/server/app/audit" "github.com/jackc/pgx/v5" ) var ErrInvalidCredentials = errors.New("用户名或密码错误") var ErrUnauthorized = errors.New("会话无效或已过期") type Service struct { Store Store Audit audit.Store Secret []byte SessionTTL time.Duration } func (s Service) digest(token string) []byte { mac := hmac.New(sha256.New, s.Secret) _, _ = mac.Write([]byte(token)) return mac.Sum(nil) } func (s Service) Login(ctx context.Context, input LoginInput) (LoginResult, error) { username := strings.ToLower(strings.TrimSpace(input.Username)) user, err := s.Store.UserByUsername(ctx, username) if err != nil || !user.Enabled || !passwordMatches(user.PasswordHash, input.Password) { _ = s.Audit.Record(ctx, nil, "auth.login", "user", nil, "failure", map[string]any{"reason": "invalid_credentials"}) return LoginResult{}, ErrInvalidCredentials } bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return LoginResult{}, err } token := base64.RawURLEncoding.EncodeToString(bytes) expires := time.Now().UTC().Add(s.SessionTTL) if s.SessionTTL == 0 { expires = time.Now().UTC().Add(8 * time.Hour) } if err := s.Store.CreateSession(ctx, user.ID, s.digest(token), expires); err != nil { return LoginResult{}, err } _ = s.Audit.Record(ctx, &user.ID, "auth.login", "user", &user.ID, "success", map[string]any{}) return LoginResult{Token: token, User: user, ExpiresAt: expires.Format(time.RFC3339)}, nil } func (s Service) Authenticate(ctx context.Context, token string) (User, error) { if token == "" { return User{}, ErrUnauthorized } user, err := s.Store.UserBySessionDigest(ctx, s.digest(token)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return User{}, ErrUnauthorized } return User{}, err } if !user.Enabled { return User{}, ErrUnauthorized } return user, nil } func (s Service) Logout(ctx context.Context, token string, user User) error { if token != "" { if err := s.Store.RevokeSession(ctx, s.digest(token)); err != nil { return err } } return s.Audit.Record(ctx, &user.ID, "auth.logout", "user", &user.ID, "success", map[string]any{}) } func (s Service) BootstrapAdministrator(ctx context.Context, username, displayName, password string) error { username = strings.ToLower(strings.TrimSpace(username)) if username == "" { return errors.New("username is required") } hash, err := hashPassword(password) if err != nil { return err } id, err := s.Store.BootstrapAdministrator(ctx, username, displayName, hash) if err != nil { return err } return s.Audit.Record(ctx, &id, "rbac.bootstrap_administrator", "user", &id, "success", map[string]any{}) }