package services import ( "cmbone/internal/models" "crypto/rand" "crypto/sha256" "database/sql" "encoding/hex" "errors" "fmt" "strings" "time" ) const ( authProviderLocal = "local" authProviderRemote = "remote" auditActionLoginSuccess = "auth.login.success" auditActionLoginFailure = "auth.login.failure" auditActionLogout = "auth.logout" ) var ( errInvalidCredentials = errors.New("用户名或密码错误") errRemoteProviderNotReady = errors.New("remote auth provider is not configured") errUnsupportedAuthProvider = errors.New("unsupported auth provider") errMissingUsernameOrPassword = errors.New("用户名和密码不能为空") ) type AuthService struct { store *SuiStore providers map[string]AuthProvider } type AuthProvider interface { Mode() string Authenticate(username string, password string) (authPrincipal, error) } type authPrincipal struct { UserID string Username string Provider string } func NewAuthService(store *SuiStore) *AuthService { service := &AuthService{store: store} service.providers = map[string]AuthProvider{ authProviderLocal: &LocalAuthProvider{store: store}, authProviderRemote: &RemoteAuthProvider{}, } return service } func (s *AuthService) GetAuthProviders() []models.AuthProviderOption { return []models.AuthProviderOption{ { Value: authProviderLocal, Label: "Local", Description: "本地固定账号 provider,用于模板演示和离线模式", Enabled: true, }, { Value: authProviderRemote, Label: "Remote", Description: "真实后端 provider 预留入口", Enabled: false, }, } } func (s *AuthService) GetAuthMode() string { mode, err := NewAppConfigService(s.store).GetAppConfig("auth.provider") if err != nil || mode == "" { return authProviderLocal } return mode } func (s *AuthService) SetAuthMode(mode string) error { if _, ok := s.providers[mode]; !ok { return fmt.Errorf("%w: %s", errUnsupportedAuthProvider, mode) } return NewAppConfigService(s.store).SetAppConfig("auth.provider", mode) } func (s *AuthService) Login(provider string, username string, password string) (models.AuthSession, error) { provider = strings.TrimSpace(provider) username = strings.TrimSpace(username) if provider == "" { provider = s.GetAuthMode() } if username == "" || password == "" { _ = s.recordAudit(username, auditActionLoginFailure, "auth_session", "", "missing username or password") return models.AuthSession{}, errMissingUsernameOrPassword } authProvider, ok := s.providers[provider] if !ok { _ = s.recordAudit(username, auditActionLoginFailure, "auth_session", provider, "unsupported provider") return models.AuthSession{}, fmt.Errorf("%w: %s", errUnsupportedAuthProvider, provider) } principal, err := authProvider.Authenticate(username, password) if err != nil { _ = s.recordAudit(username, auditActionLoginFailure, "auth_session", provider, err.Error()) return models.AuthSession{}, err } session, err := s.createSession(principal) if err != nil { return models.AuthSession{}, err } _ = s.SetAuthMode(provider) _ = s.recordAudit(principal.Username, auditActionLoginSuccess, "auth_session", fmt.Sprint(session.ID), "login succeeded") return session, nil } func (s *AuthService) Logout() error { session, err := s.GetCurrentSession() if err != nil { return err } if !session.Active { return nil } _, err = s.store.DB.Exec("UPDATE auth_sessions SET active = 0 WHERE id = ?", session.ID) if err != nil { return err } return s.recordAudit(session.Username, auditActionLogout, "auth_session", fmt.Sprint(session.ID), "logout succeeded") } func (s *AuthService) GetCurrentSession() (models.AuthSession, error) { now := time.Now().UTC().Format(time.RFC3339) row := s.store.DB.QueryRow(` SELECT id, user_id, username, provider, token, created_at, expires_at, active FROM auth_sessions WHERE active = 1 AND expires_at > ? ORDER BY id DESC LIMIT 1 `, now) return scanAuthSession(row) } func (s *AuthService) createSession(principal authPrincipal) (models.AuthSession, error) { token, err := newSessionToken() if err != nil { return models.AuthSession{}, err } now := time.Now().UTC() expiresAt := now.Add(24 * time.Hour) tx, err := s.store.DB.Begin() if err != nil { return models.AuthSession{}, err } defer func() { if err != nil { _ = tx.Rollback() } }() if _, err = tx.Exec("UPDATE auth_sessions SET active = 0 WHERE active = 1"); err != nil { return models.AuthSession{}, err } result, err := tx.Exec(` INSERT INTO auth_sessions (user_id, username, provider, token, created_at, expires_at, active) VALUES (?, ?, ?, ?, ?, ?, 1) `, principal.UserID, principal.Username, principal.Provider, token, now.Format(time.RFC3339), expiresAt.Format(time.RFC3339)) if err != nil { return models.AuthSession{}, err } id, err := result.LastInsertId() if err != nil { return models.AuthSession{}, err } if err = tx.Commit(); err != nil { return models.AuthSession{}, err } return models.AuthSession{ ID: id, UserID: principal.UserID, Username: principal.Username, Provider: principal.Provider, Token: token, CreatedAt: now.Format(time.RFC3339), ExpiresAt: expiresAt.Format(time.RFC3339), Active: true, }, nil } func (s *AuthService) recordAudit(actor string, action string, targetType string, targetID string, detail string) error { if strings.TrimSpace(actor) == "" { actor = "anonymous" } return recordAuditLog(s.store, actor, action, targetType, targetID, detail) } func scanAuthSession(row *sql.Row) (models.AuthSession, error) { var session models.AuthSession var active int err := row.Scan( &session.ID, &session.UserID, &session.Username, &session.Provider, &session.Token, &session.CreatedAt, &session.ExpiresAt, &active, ) if errors.Is(err, sql.ErrNoRows) { return models.AuthSession{}, nil } if err != nil { return models.AuthSession{}, err } session.Active = active == 1 return session, nil } func newSessionToken() (string, error) { token := make([]byte, 32) if _, err := rand.Read(token); err != nil { return "", err } return hex.EncodeToString(token), nil } type LocalAuthProvider struct { store *SuiStore } func (p *LocalAuthProvider) Mode() string { return authProviderLocal } func (p *LocalAuthProvider) Authenticate(username string, password string) (authPrincipal, error) { config := NewAppConfigService(p.store) expectedUsername, err := config.GetAppConfig("auth.local.username") if err != nil { return authPrincipal{}, err } salt, err := config.GetAppConfig("auth.local.password_salt") if err != nil { return authPrincipal{}, err } expectedHash, err := config.GetAppConfig("auth.local.password_hash") if err != nil { return authPrincipal{}, err } if username != expectedUsername || passwordHash(salt, password) != expectedHash { return authPrincipal{}, errInvalidCredentials } return authPrincipal{ UserID: "local:" + username, Username: username, Provider: p.Mode(), }, nil } type RemoteAuthProvider struct{} func (p *RemoteAuthProvider) Mode() string { return authProviderRemote } func (p *RemoteAuthProvider) Authenticate(username string, password string) (authPrincipal, error) { return authPrincipal{}, errRemoteProviderNotReady } func passwordHash(salt string, password string) string { sum := sha256.Sum256([]byte(salt + ":" + password)) return hex.EncodeToString(sum[:]) }