180 lines
5.8 KiB
Go
180 lines
5.8 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"strings"
|
||
"time"
|
||
|
||
"juhe-factory/api/internal/model"
|
||
"juhe-factory/api/internal/security"
|
||
|
||
"github.com/google/uuid"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
var ErrUnauthorized = errors.New("账号或密码错误")
|
||
var ErrReauthFailed = errors.New("当前管理员密码错误")
|
||
var ErrAdminPasswordConfirmation = errors.New("两次输入的新密码不一致")
|
||
var ErrAdminPasswordLength = errors.New("新密码长度必须为 8~20 位")
|
||
|
||
type Auth struct {
|
||
db *gorm.DB
|
||
passwords security.PasswordHasher
|
||
tokens security.TokenService
|
||
}
|
||
|
||
type TokenPair struct {
|
||
AccessToken string `json:"access_token"`
|
||
RefreshToken string `json:"refresh_token"`
|
||
ExpiresAt time.Time `json:"expires_at"`
|
||
Admin map[string]any `json:"admin"`
|
||
}
|
||
|
||
func NewAuth(db *gorm.DB, passwords security.PasswordHasher, tokens security.TokenService) *Auth {
|
||
return &Auth{db: db, passwords: passwords, tokens: tokens}
|
||
}
|
||
|
||
func (s *Auth) Bootstrap(username, password string) error {
|
||
if strings.TrimSpace(password) == "" {
|
||
return nil
|
||
}
|
||
username = strings.TrimSpace(username)
|
||
var admin model.AdminUser
|
||
err := s.db.Where("username = ? AND deleted_at IS NULL", username).First(&admin).Error
|
||
if err == nil {
|
||
if s.passwords.Verify(admin.PasswordHash, password) {
|
||
return nil
|
||
}
|
||
hash, hashErr := s.passwords.Hash(password)
|
||
if hashErr != nil {
|
||
return hashErr
|
||
}
|
||
return s.db.Model(&admin).Update("password_hash", hash).Error
|
||
}
|
||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return err
|
||
}
|
||
var count int64
|
||
if err := s.db.Model(&model.AdminUser{}).Count(&count).Error; err != nil || count > 0 {
|
||
return err
|
||
}
|
||
hash, err := s.passwords.Hash(password)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return s.db.Create(&model.AdminUser{Username: username, PasswordHash: hash, Enabled: true}).Error
|
||
}
|
||
|
||
func (s *Auth) Login(username, password string) (*TokenPair, error) {
|
||
var admin model.AdminUser
|
||
if err := s.db.Where("username = ? AND deleted_at IS NULL", strings.TrimSpace(username)).First(&admin).Error; err != nil || !admin.Enabled || !s.passwords.Verify(admin.PasswordHash, password) {
|
||
return nil, ErrUnauthorized
|
||
}
|
||
return s.issue(&admin, true)
|
||
}
|
||
|
||
func (s *Auth) Refresh(refreshToken string) (*TokenPair, error) {
|
||
hash := security.HashToken(refreshToken)
|
||
var record model.AdminRefreshToken
|
||
if err := s.db.Where("token_hash = ? AND revoked_at IS NULL AND expires_at > ?", hash, time.Now()).First(&record).Error; err != nil {
|
||
return nil, ErrUnauthorized
|
||
}
|
||
var admin model.AdminUser
|
||
if err := s.db.First(&admin, "id = ? AND enabled = true", record.AdminID).Error; err != nil {
|
||
return nil, ErrUnauthorized
|
||
}
|
||
now := time.Now()
|
||
if err := s.db.Model(&record).Update("revoked_at", now).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
return s.issue(&admin, false)
|
||
}
|
||
|
||
func (s *Auth) Logout(refreshToken string) error {
|
||
if refreshToken == "" {
|
||
return nil
|
||
}
|
||
now := time.Now()
|
||
return s.db.Model(&model.AdminRefreshToken{}).Where("token_hash = ? AND revoked_at IS NULL", security.HashToken(refreshToken)).Update("revoked_at", now).Error
|
||
}
|
||
|
||
func (s *Auth) Authenticate(raw string) (*model.AdminUser, error) {
|
||
claims, err := s.tokens.ParseAccessToken(raw)
|
||
if err != nil {
|
||
return nil, ErrUnauthorized
|
||
}
|
||
id, err := uuid.Parse(claims.AdminID)
|
||
if err != nil {
|
||
return nil, ErrUnauthorized
|
||
}
|
||
var admin model.AdminUser
|
||
if err := s.db.First(&admin, "id = ? AND enabled = true", id).Error; err != nil {
|
||
return nil, ErrUnauthorized
|
||
}
|
||
return &admin, nil
|
||
}
|
||
|
||
func (s *Auth) ChangePassword(adminID uuid.UUID, currentPassword, newPassword, confirmPassword string) error {
|
||
if newPassword != confirmPassword {
|
||
return ErrAdminPasswordConfirmation
|
||
}
|
||
if len(newPassword) < 8 || len(newPassword) > 20 {
|
||
return ErrAdminPasswordLength
|
||
}
|
||
var admin model.AdminUser
|
||
if err := s.db.Where("id = ? AND enabled = true AND deleted_at IS NULL", adminID).First(&admin).Error; err != nil {
|
||
return ErrUnauthorized
|
||
}
|
||
if !s.passwords.Verify(admin.PasswordHash, currentPassword) {
|
||
return ErrReauthFailed
|
||
}
|
||
hash, err := s.passwords.Hash(newPassword)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
now := time.Now()
|
||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||
if err := tx.Model(&admin).Update("password_hash", hash).Error; err != nil {
|
||
return err
|
||
}
|
||
return tx.Model(&model.AdminRefreshToken{}).
|
||
Where("admin_id = ? AND revoked_at IS NULL", adminID).
|
||
Update("revoked_at", now).Error
|
||
})
|
||
}
|
||
|
||
func (s *Auth) Audit(admin *model.AdminUser, action, resource, resourceID, reason, ip, traceID string, detail any) error {
|
||
return WriteAudit(s.db, admin, action, resource, resourceID, reason, ip, traceID, detail)
|
||
}
|
||
|
||
func (s *Auth) issue(admin *model.AdminUser, updateLogin bool) (*TokenPair, error) {
|
||
access, expiresAt, err := s.tokens.NewAccessToken(admin.ID.String(), admin.Username)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
refresh, hash, refreshExpires, err := s.tokens.NewRefreshToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
record := model.AdminRefreshToken{AdminID: admin.ID, TokenHash: hash, ExpiresAt: refreshExpires}
|
||
if err := s.db.Transaction(func(tx *gorm.DB) error {
|
||
if err := tx.Create(&record).Error; err != nil {
|
||
return err
|
||
}
|
||
if updateLogin {
|
||
return tx.Model(admin).Update("last_login_at", time.Now()).Error
|
||
}
|
||
return nil
|
||
}); err != nil {
|
||
return nil, err
|
||
}
|
||
return &TokenPair{AccessToken: access, RefreshToken: refresh, ExpiresAt: expiresAt, Admin: map[string]any{"id": admin.ID, "username": admin.Username}}, nil
|
||
}
|
||
|
||
func WriteAudit(db *gorm.DB, admin *model.AdminUser, action, resource, resourceID, reason, ip, traceID string, detail any) error {
|
||
data, _ := json.Marshal(detail)
|
||
log := model.AdminAuditLog{AdminID: &admin.ID, AdminUsername: admin.Username, Action: action, ResourceType: resource, ResourceID: resourceID, Reason: reason, Detail: string(data), IPAddress: ip, TraceID: traceID}
|
||
return db.Create(&log).Error
|
||
}
|