95 lines
2.8 KiB
Go
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{})
|
|
}
|