初始化
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user