Files

95 lines
2.8 KiB
Go

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{})
}