package identity import ( "context" "crypto/subtle" "errors" "fmt" "strings" "time" ) type Config struct { SigningKey []byte BootstrapToken string SecureCookie bool SessionTTL time.Duration } type Service struct { store Store cfg Config now func() time.Time } func NewService(store Store, cfg Config) (*Service, error) { if len(cfg.SigningKey) < 32 { return nil, fmt.Errorf("Sense identity signing key must contain at least 32 bytes") } if cfg.SessionTTL <= 0 { cfg.SessionTTL = 8 * time.Hour } return &Service{store: store, cfg: cfg, now: time.Now}, nil } func (s *Service) Bootstrap(ctx context.Context, token, username, displayName, password string) (User, error) { if s.cfg.BootstrapToken == "" || subtle.ConstantTimeCompare([]byte(token), []byte(s.cfg.BootstrapToken)) != 1 { return User{}, errors.New("bootstrap denied") } username = strings.TrimSpace(username) if username == "" || len(username) > 64 { return User{}, fmt.Errorf("用户名格式不正确") } if err := ValidatePassword(username, password); err != nil { return User{}, err } hash, err := HashPassword(password) if err != nil { return User{}, err } now := s.now().UTC() user := User{ID: newID("usr"), Username: username, DisplayName: strings.TrimSpace(displayName), Role: RoleAdministrator, Enabled: true, PasswordHash: hash, CreatedAt: now, UpdatedAt: now} if err := s.store.BootstrapUser(ctx, user); err != nil { return User{}, err } s.audit(ctx, user.ID, "identity.bootstrap", user.ID, "success", nil) return user, nil } func (s *Service) Login(ctx context.Context, username, password string) (Principal, string, error) { user, err := s.store.FindUserByUsername(ctx, strings.TrimSpace(username)) if err != nil || !user.Enabled || !VerifyPassword(user.PasswordHash, password) { s.audit(ctx, "", "identity.login", "", "failure", map[string]any{"reason": "invalid_credentials"}) return Principal{}, "", errors.New("invalid credentials") } now := s.now().UTC() session := Session{ID: newID("ses"), UserID: user.ID, ExpiresAt: now.Add(s.cfg.SessionTTL)} if err := s.store.CreateSession(ctx, session); err != nil { return Principal{}, "", err } raw, err := issueToken(s.cfg.SigningKey, user, session, now) if err != nil { return Principal{}, "", err } s.audit(ctx, user.ID, "identity.login", user.ID, "success", nil) return principalFor(user, session.ID), raw, nil } func (s *Service) Authenticate(ctx context.Context, raw string) (Principal, error) { claims, err := parseToken(s.cfg.SigningKey, raw, s.now()) if err != nil { return Principal{}, err } active, err := s.store.SessionActive(ctx, claims.ID, s.now()) if err != nil || !active { return Principal{}, errors.New("inactive Sense session") } user, err := s.store.FindUserByID(ctx, claims.Subject) if err != nil || !user.Enabled || user.Role != claims.Role { return Principal{}, errors.New("invalid Sense principal") } return principalFor(user, claims.ID), nil } func (s *Service) Logout(ctx context.Context, principal Principal) error { if err := s.store.RevokeSession(ctx, principal.SessionID, s.now().UTC()); err != nil && !errors.Is(err, ErrNotFound) { return err } s.audit(ctx, principal.UserID, "identity.logout", principal.UserID, "success", nil) return nil } func (s *Service) ListUsers(ctx context.Context) ([]User, error) { return s.store.ListUsers(ctx) } func (s *Service) ListAudit(ctx context.Context) ([]AuditEntry, error) { return s.store.ListAudit(ctx, 100) } func (s *Service) CreateUser(ctx context.Context, actor Principal, username, displayName, role, password string) (User, error) { if !ValidRole(role) { return User{}, fmt.Errorf("角色不正确") } username = strings.TrimSpace(username) if username == "" || len(username) > 64 { return User{}, fmt.Errorf("用户名格式不正确") } if err := ValidatePassword(username, password); err != nil { return User{}, err } hash, err := HashPassword(password) if err != nil { return User{}, err } now := s.now().UTC() user := User{ID: newID("usr"), Username: username, DisplayName: strings.TrimSpace(displayName), Role: role, Enabled: true, PasswordHash: hash, CreatedAt: now, UpdatedAt: now} if err := s.store.CreateUser(ctx, user); err != nil { return User{}, err } s.audit(ctx, actor.UserID, "identity.user.create", user.ID, "success", map[string]any{"role": role}) return user, nil } func (s *Service) UpdateUser(ctx context.Context, actor Principal, id, displayName, role string, enabled bool) (User, error) { if !ValidRole(role) { return User{}, fmt.Errorf("角色不正确") } user, err := s.store.FindUserByID(ctx, id) if err != nil { return User{}, err } if actor.UserID == id && !enabled { return User{}, fmt.Errorf("不能停用当前登录账户") } user.DisplayName = strings.TrimSpace(displayName) user.Role = role user.Enabled = enabled user.UpdatedAt = s.now().UTC() if err := s.store.UpdateUser(ctx, user); err != nil { return User{}, err } s.audit(ctx, actor.UserID, "identity.user.update", user.ID, "success", map[string]any{"role": role, "enabled": enabled}) return user, nil } func (s *Service) audit(ctx context.Context, actor, action, target, outcome string, metadata map[string]any) { _ = s.store.AppendAudit(ctx, AuditEntry{ID: newID("aud"), ActorID: actor, Action: action, Target: target, Outcome: outcome, Metadata: metadata, CreatedAt: s.now().UTC()}) } func principalFor(user User, sessionID string) Principal { return Principal{UserID: user.ID, Username: user.Username, DisplayName: user.DisplayName, Role: user.Role, Permissions: PermissionsForRole(user.Role), SessionID: sessionID} }