mengstack-api/internal/modules/auth/application/service.go
MengStack Dev df809f1045
Some checks failed
CI / Build & Test (push) Failing after 1m31s
feat: user CRUD API + dashboard stats + kernel infrastructure
- Add user management endpoints (list/create/get/update/delete) with pagination and search
- Add dashboard stats endpoint with tenant/user/online counts and growth metrics
- Add tenant resolver middleware for multi-tenant request scoping
- Add i18n kernel with zh/en message files and AcceptLanguage middleware
- Add WebSocket hub/handler for real-time communication
- Add job scheduler kernel with cron support
- Add plugin sandbox for isolated execution
- Add storage kernel (local filesystem)
- Add event bus kernel for pub/sub
- Add cache kernel abstraction
- Add database migration runner and version upgrade checker
- Add rate limiting middleware with Redis backend
- Add SQL migrations for rbac, audit_logs, settings, notifications, examples
- Extend user repository with list/delete/count operations
- Register all module routes with tenant resolver
2026-10-03 03:42:58 +08:00

317 lines
8.3 KiB
Go

package application
import (
"context"
"time"
"mengstack/internal/kernel/errors"
"mengstack/internal/kernel/tenant"
"mengstack/internal/modules/auth/domain"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type Service struct {
repo domain.UserRepository
jwtCfg JWTConfig
}
type JWTConfig struct {
Secret string
AccessExpiryMinutes int
RefreshExpiryDays int
Issuer string
}
func NewService(repo domain.UserRepository, jwtCfg JWTConfig) *Service {
return &Service{repo: repo, jwtCfg: jwtCfg}
}
func (s *Service) Register(ctx context.Context, req domain.RegisterRequest) (domain.TokenPair, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.TokenPair{}, errors.ErrTenantRequired
}
if _, err := s.repo.FindByEmail(ctx, tenantID, req.Email); err == nil {
return domain.TokenPair{}, errors.ErrUserExists
} else if err != gorm.ErrRecordNotFound {
return domain.TokenPair{}, errors.Wrap(err, "QUERY_USER_FAILED", "failed to check existing user", 500)
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
if err != nil {
return domain.TokenPair{}, errors.Wrap(err, "HASH_FAILED", "failed to hash password", 500)
}
user := &domain.User{
TenantID: tenantID,
Username: req.Username,
Email: req.Email,
Password: string(hash),
Nickname: req.Nickname,
Status: 1,
}
if err := s.repo.Create(ctx, user); err != nil {
return domain.TokenPair{}, errors.Wrap(err, "CREATE_USER_FAILED", "failed to create user", 500)
}
return s.generateTokens(user)
}
func (s *Service) Login(ctx context.Context, req domain.LoginRequest) (domain.TokenPair, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.TokenPair{}, errors.ErrTenantRequired
}
user, err := s.repo.FindByEmail(ctx, tenantID, req.Email)
if err != nil {
return domain.TokenPair{}, errors.ErrUnauthorized
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return domain.TokenPair{}, errors.ErrPasswordWrong
}
if user.Status != 1 {
return domain.TokenPair{}, errors.ErrForbidden
}
tokens, err := s.generateTokens(user)
if err != nil {
return domain.TokenPair{}, err
}
now := time.Now()
user.LastLogin = &now
_ = s.repo.Update(ctx, user)
return tokens, nil
}
func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (domain.TokenPair, error) {
claims, err := ParseToken(refreshToken, s.jwtCfg.Secret)
if err != nil {
return domain.TokenPair{}, errors.ErrRefreshToken
}
if claims.Type != "refresh" {
return domain.TokenPair{}, errors.ErrRefreshToken
}
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.TokenPair{}, errors.ErrTenantRequired
}
user, err := s.repo.FindByID(ctx, tenantID, claims.UserID)
if err != nil {
return domain.TokenPair{}, errors.ErrUnauthorized
}
return s.generateTokens(user)
}
func (s *Service) ChangePassword(ctx context.Context, userID uint, req domain.ChangePasswordRequest) error {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return errors.ErrTenantRequired
}
user, err := s.repo.FindByID(ctx, tenantID, userID)
if err != nil {
return errors.ErrUserNotFound
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
return errors.ErrPasswordWrong
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 12)
if err != nil {
return errors.Wrap(err, "HASH_FAILED", "failed to hash password", 500)
}
user.Password = string(hash)
return s.repo.Update(ctx, user)
}
func (s *Service) GetProfile(ctx context.Context, userID uint) (domain.UserDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.UserDTO{}, errors.ErrTenantRequired
}
user, err := s.repo.FindByID(ctx, tenantID, userID)
if err != nil {
return domain.UserDTO{}, errors.ErrUserNotFound
}
return domain.ToUserDTO(user), nil
}
func (s *Service) JWTSecret() string {
return s.jwtCfg.Secret
}
func (s *Service) ListUsers(ctx context.Context, page, pageSize int, search string, status *int) ([]domain.UserDTO, int64, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return nil, 0, errors.ErrTenantRequired
}
users, total, err := s.repo.List(ctx, tenantID, page, pageSize, search, status)
if err != nil {
return nil, 0, errors.Wrap(err, "LIST_USERS_FAILED", "failed to list users", 500)
}
dtos := make([]domain.UserDTO, len(users))
for i, u := range users {
dtos[i] = domain.ToUserDTO(&u)
}
return dtos, total, nil
}
func (s *Service) GetUser(ctx context.Context, id uint) (domain.UserDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.UserDTO{}, errors.ErrTenantRequired
}
user, err := s.repo.FindByID(ctx, tenantID, id)
if err != nil {
return domain.UserDTO{}, errors.ErrUserNotFound
}
return domain.ToUserDTO(user), nil
}
func (s *Service) CreateUser(ctx context.Context, req domain.CreateUserRequest) (domain.UserDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.UserDTO{}, errors.ErrTenantRequired
}
if _, err := s.repo.FindByEmail(ctx, tenantID, req.Email); err == nil {
return domain.UserDTO{}, errors.ErrUserExists
} else if err != gorm.ErrRecordNotFound {
return domain.UserDTO{}, errors.Wrap(err, "QUERY_USER_FAILED", "failed to check existing user", 500)
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
if err != nil {
return domain.UserDTO{}, errors.Wrap(err, "HASH_FAILED", "failed to hash password", 500)
}
status := req.Status
if status == 0 {
status = 1
}
user := &domain.User{
TenantID: tenantID,
Username: req.Username,
Email: req.Email,
Password: string(hash),
Nickname: req.Nickname,
Status: status,
}
if err := s.repo.Create(ctx, user); err != nil {
return domain.UserDTO{}, errors.Wrap(err, "CREATE_USER_FAILED", "failed to create user", 500)
}
return domain.ToUserDTO(user), nil
}
func (s *Service) UpdateUser(ctx context.Context, id uint, req domain.UpdateUserRequest) (domain.UserDTO, error) {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return domain.UserDTO{}, errors.ErrTenantRequired
}
user, err := s.repo.FindByID(ctx, tenantID, id)
if err != nil {
return domain.UserDTO{}, errors.ErrUserNotFound
}
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Email != "" {
user.Email = req.Email
}
if req.Status != nil {
user.Status = *req.Status
}
if err := s.repo.Update(ctx, user); err != nil {
return domain.UserDTO{}, errors.Wrap(err, "UPDATE_USER_FAILED", "failed to update user", 500)
}
return domain.ToUserDTO(user), nil
}
func (s *Service) DeleteUser(ctx context.Context, id uint) error {
tenantID, ok := tenant.FromContext(ctx)
if !ok {
return errors.ErrTenantRequired
}
if _, err := s.repo.FindByID(ctx, tenantID, id); err != nil {
return errors.ErrUserNotFound
}
if err := s.repo.Delete(ctx, tenantID, id); err != nil {
return errors.Wrap(err, "DELETE_USER_FAILED", "failed to delete user", 500)
}
return nil
}
func (s *Service) UserCount(ctx context.Context, tenantID string) (int64, error) {
return s.repo.Count(ctx, tenantID)
}
func (s *Service) UserCountAll(ctx context.Context) (int64, error) {
return s.repo.CountAll(ctx)
}
func (s *Service) ListAllUsers(ctx context.Context, page, pageSize int) ([]domain.UserDTO, int64, error) {
users, total, err := s.repo.ListAll(ctx, page, pageSize)
if err != nil {
return nil, 0, errors.Wrap(err, "LIST_USERS_FAILED", "failed to list users", 500)
}
dtos := make([]domain.UserDTO, len(users))
for i, u := range users {
dtos[i] = domain.ToUserDTO(&u)
}
return dtos, total, nil
}
func (s *Service) generateTokens(user *domain.User) (domain.TokenPair, error) {
access, err := GenerateToken(user.ID, user.TenantID, "access",
time.Duration(s.jwtCfg.AccessExpiryMinutes)*time.Minute,
s.jwtCfg.Secret, s.jwtCfg.Issuer,
)
if err != nil {
return domain.TokenPair{}, errors.Wrap(err, "TOKEN_GEN_FAILED", "failed to generate token", 500)
}
refresh, err := GenerateToken(user.ID, user.TenantID, "refresh",
time.Duration(s.jwtCfg.RefreshExpiryDays)*24*time.Hour,
s.jwtCfg.Secret, s.jwtCfg.Issuer,
)
if err != nil {
return domain.TokenPair{}, errors.Wrap(err, "TOKEN_GEN_FAILED", "failed to generate refresh token", 500)
}
return domain.TokenPair{
AccessToken: access,
RefreshToken: refresh,
ExpiresIn: s.jwtCfg.AccessExpiryMinutes * 60,
}, nil
}