143 lines
4.2 KiB
Go
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
|
|
}
|