Some checks failed
CI / Build & Test (push) Failing after 1m31s
- 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
317 lines
8.3 KiB
Go
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
|
|
}
|