package user import ( "encoding/json" "errors" "time" "github.com/shopspring/decimal" "go.uber.org/zap" "gorm.io/gorm" "openteam/server/internal/config" "openteam/server/internal/pkg/jwt" "openteam/server/internal/pkg/password" "openteam/server/internal/store" ) var ( ErrUserExists = errors.New("username or email already exists") ErrBadCredentials = errors.New("invalid username/email or password") ErrUserDisabled = errors.New("account disabled") ErrInvalidRefresh = errors.New("invalid refresh token") ) type Service struct { db *gorm.DB cfg *config.Config log *zap.Logger } func NewService(db *gorm.DB, cfg *config.Config, log *zap.Logger) *Service { return &Service{db: db, cfg: cfg, log: log} } type RegisterInput struct { Username string `json:"username" binding:"required,min=3,max=64"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8,max=128"` InviteCode string `json:"inviteCode"` } type TokenPair struct { AccessToken string `json:"accessToken"` TokenType string `json:"tokenType"` ExpiresIn int64 `json:"expiresIn"` RefreshToken string `json:"refreshToken"` } // Register creates a user. Returns a token pair plus the created user. func (s *Service) Register(in RegisterInput) (*store.User, *TokenPair, error) { if s.cfg.Registration.Mode == "invite" { // Invite mode: validate the invite code before allowing signup. ok, err := s.validateInviteCode(in.InviteCode) if err != nil { return nil, nil, err } if !ok { return nil, nil, errors.New("invalid invite code") } } hash, err := password.Hash(in.Password) if err != nil { return nil, nil, err } u := &store.User{ Username: in.Username, Email: in.Email, PasswordHash: hash, Role: "user", Balance: decimalZero(), Status: "active", InviteCode: in.InviteCode, } if err := s.db.Create(u).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, nil, ErrUserExists } return nil, nil, err } pair, err := s.issuePair(u) if err != nil { return nil, nil, err } return u, pair, nil } type LoginInput struct { Account string `json:"account" binding:"required"` // username or email Password string `json:"password" binding:"required"` } // Login authenticates by username or email and returns a token pair. func (s *Service) Login(in LoginInput) (*store.User, *TokenPair, error) { var u store.User err := s.db.Where("username = ? OR email = ?", in.Account, in.Account).First(&u).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, ErrBadCredentials } if err != nil { return nil, nil, err } ok, err := password.Verify(in.Password, u.PasswordHash) if err != nil || !ok { return nil, nil, ErrBadCredentials } if u.Status != "active" { return nil, nil, ErrUserDisabled } now := time.Now() u.LastLoginAt = &now s.db.Model(&u).Update("last_login_at", now) pair, err := s.issuePair(&u) if err != nil { return nil, nil, err } return &u, pair, nil } // Refresh issues a new access token from a valid refresh token. func (s *Service) Refresh(refreshToken string) (*TokenPair, error) { claims, err := jwt.Parse(s.cfg.Auth.JWTSecret, refreshToken) if err != nil || claims.Type != "refresh" { return nil, ErrInvalidRefresh } var u store.User if err := s.db.First(&u, claims.UserID).Error; err != nil { return nil, ErrInvalidRefresh } if u.Status != "active" { return nil, ErrUserDisabled } return s.issuePair(&u) } // GetByID loads a user. func (s *Service) GetByID(id int64) (*store.User, error) { var u store.User if err := s.db.First(&u, id).Error; err != nil { return nil, err } return &u, nil } // GetPublic returns a user DTO without secrets. func GetPublic(u *store.User) map[string]any { return map[string]any{ "id": u.ID, "username": u.Username, "email": u.Email, "role": u.Role, "balance": u.Balance.String(), "status": u.Status, "createdAt": u.CreatedAt, "lastLoginAt": u.LastLoginAt, } } func (s *Service) validateInviteCode(code string) (bool, error) { var conf store.SystemConfig if err := s.db.First(&conf, "key = ?", "invite_codes").Error; err != nil { return false, nil } var codes map[string]bool if len(conf.Value) > 0 { if err := json.Unmarshal(conf.Value, &codes); err != nil { return false, err } } return codes[code], nil } func (s *Service) issuePair(u *store.User) (*TokenPair, error) { access, err := jwt.SignAccess(s.cfg.Auth.JWTSecret, u.ID, u.Username, u.Role, s.cfg.Auth.AccessTokenTTL) if err != nil { return nil, err } refresh, err := jwt.SignRefresh(s.cfg.Auth.JWTSecret, u.ID, s.cfg.Auth.RefreshTokenTTL) if err != nil { return nil, err } return &TokenPair{ AccessToken: access, TokenType: "Bearer", ExpiresIn: int64(s.cfg.Auth.AccessTokenTTL.Seconds()), RefreshToken: refresh, }, nil } func decimalZero() decimal.Decimal { return decimal.NewFromInt(0) }