Files
2026-08-15 17:04:47 +08:00

143 lines
4.2 KiB
Go

package service
import (
"strings"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"nl-pms-api/internal/commonservice"
"nl-pms-api/internal/config"
"nl-pms-api/internal/model"
)
// AuthService 注册 / 登录 / 刷新 / 改密。
type AuthService struct {
DB *gorm.DB
Cfg *config.Config
}
// AuthTokens 登录/刷新返回的令牌与用户信息。
type AuthTokens struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
UserID int64 `json:"userId"`
Username string `json:"username"`
}
func validAccount(username, password string) error {
username = strings.TrimSpace(username)
if len(username) < 3 || len(username) > 64 {
return commonservice.BadRequest("SYNC_USERNAME_INVALID")
}
if len(password) < 6 {
return commonservice.BadRequest("SYNC_PASSWORD_TOO_SHORT")
}
return nil
}
// Register 注册新账号。
func (s *AuthService) Register(username, password string) error {
if err := validAccount(username, password); err != nil {
return err
}
username = strings.TrimSpace(username)
var n int64
s.DB.Model(&model.User{}).Where("username = ?", username).Count(&n)
if n > 0 {
return commonservice.Conflict("SYNC_USER_EXISTS")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return commonservice.Internal("INTERNAL")
}
u := model.User{
Username: username,
PasswordHash: string(hash),
CreatedAt: commonservice.NowRFC(),
}
if err := s.DB.Create(&u).Error; err != nil {
// 并发注册由唯一索引兜底。
return commonservice.Conflict("SYNC_USER_EXISTS")
}
return nil
}
// Login 校验账号并签发双令牌。
func (s *AuthService) Login(username, password, clientIP string) (*AuthTokens, error) {
username = strings.TrimSpace(username)
var u model.User
if err := s.DB.Where("username = ?", username).First(&u).Error; err != nil {
return nil, commonservice.Unauthorized("SYNC_BAD_CREDENTIALS")
}
if u.Disabled != 0 {
return nil, commonservice.Forbidden("ACCOUNT_DISABLED")
}
if bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) != nil {
return nil, commonservice.Unauthorized("SYNC_BAD_CREDENTIALS")
}
tok, err := s.issueTokens(u.ID, u.Username)
if err != nil {
return nil, err
}
_ = (&ActivityService{DB: s.DB}).Ping(u.ID, clientIP)
return tok, nil
}
// Refresh 用 refresh token 换新的双令牌。
func (s *AuthService) Refresh(refreshToken, clientIP string) (*AuthTokens, error) {
claims, err := commonservice.Parse(s.Cfg.JWTSecret, strings.TrimSpace(refreshToken), "refresh")
if err != nil {
return nil, err
}
var u model.User
if err := s.DB.First(&u, claims.UserID).Error; err != nil {
return nil, commonservice.Unauthorized("UNAUTHORIZED")
}
if u.Disabled != 0 {
return nil, commonservice.Forbidden("ACCOUNT_DISABLED")
}
tok, err := s.issueTokens(u.ID, u.Username)
if err != nil {
return nil, err
}
_ = (&ActivityService{DB: s.DB}).Ping(u.ID, clientIP)
return tok, nil
}
// ChangePassword 在线修改密码。
func (s *AuthService) ChangePassword(userID int64, oldPassword, newPassword string) error {
if len(newPassword) < 6 {
return commonservice.BadRequest("SYNC_PASSWORD_TOO_SHORT")
}
var u model.User
if err := s.DB.First(&u, userID).Error; err != nil {
return commonservice.Unauthorized("UNAUTHORIZED")
}
if bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(oldPassword)) != nil {
return commonservice.BadRequest("SYNC_OLD_PASSWORD_WRONG")
}
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return commonservice.Internal("INTERNAL")
}
return s.DB.Model(&u).Update("password_hash", string(hash)).Error
}
func (s *AuthService) issueTokens(userID int64, username string) (*AuthTokens, error) {
access, err := commonservice.IssueAccess(s.Cfg.JWTSecret, userID, username, s.Cfg.AccessTTLHours)
if err != nil {
return nil, commonservice.Internal("INTERNAL")
}
refresh, err := commonservice.IssueRefresh(s.Cfg.JWTSecret, userID, username, s.Cfg.RefreshTTLDays)
if err != nil {
return nil, commonservice.Internal("INTERNAL")
}
return &AuthTokens{
AccessToken: access,
RefreshToken: refresh,
UserID: userID,
Username: username,
}, nil
}