319 lines
9.3 KiB
Go
319 lines
9.3 KiB
Go
package model
|
|
|
|
import (
|
|
"errors"
|
|
"omega-server/local/utl/password"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// User represents a user in the system
|
|
type User struct {
|
|
BaseModel
|
|
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
|
|
Username string `json:"username" gorm:"unique;not null;type:varchar(100)"`
|
|
Name string `json:"name" gorm:"not null;type:varchar(255)"`
|
|
PasswordHash string `json:"-" gorm:"not null;type:text"`
|
|
Active bool `json:"active" gorm:"default:true"`
|
|
EmailVerified bool `json:"emailVerified" gorm:"default:false"`
|
|
EmailVerificationToken string `json:"-" gorm:"type:varchar(255)"`
|
|
PasswordResetToken string `json:"-" gorm:"type:varchar(255)"`
|
|
PasswordResetExpires *time.Time `json:"-"`
|
|
LastLogin *time.Time `json:"lastLogin"`
|
|
LoginAttempts int `json:"-" gorm:"default:0"`
|
|
LockedUntil *time.Time `json:"-"`
|
|
TwoFactorEnabled bool `json:"twoFactorEnabled" gorm:"default:false"`
|
|
TwoFactorSecret string `json:"-" gorm:"type:varchar(255)"`
|
|
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
|
|
AuditLogs []AuditLog `json:"-" gorm:"foreignKey:UserID"`
|
|
}
|
|
|
|
// UserCreateRequest represents the request to create a new user
|
|
type UserCreateRequest struct {
|
|
Email string `json:"email" validate:"required,email"`
|
|
Username string `json:"username" validate:"required,min=3,max=50"`
|
|
Name string `json:"name" validate:"required,min=2,max=100"`
|
|
Password string `json:"password" validate:"required,min=8"`
|
|
RoleIDs []string `json:"roleIds"`
|
|
}
|
|
|
|
// UserUpdateRequest represents the request to update a user
|
|
type UserUpdateRequest struct {
|
|
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
|
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"`
|
|
Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"`
|
|
Active *bool `json:"active,omitempty"`
|
|
RoleIDs []string `json:"roleIds,omitempty"`
|
|
}
|
|
|
|
// UserLoginRequest represents a login request
|
|
type UserLoginRequest struct {
|
|
Email string `json:"email" validate:"required,email"`
|
|
Password string `json:"password" validate:"required"`
|
|
}
|
|
|
|
// UserLoginResponse represents a login response
|
|
type UserLoginResponse struct {
|
|
Token string `json:"token"`
|
|
RefreshToken string `json:"refreshToken"`
|
|
ExpiresAt time.Time `json:"expiresAt"`
|
|
User UserInfo `json:"user"`
|
|
}
|
|
|
|
// UserInfo represents public user information
|
|
type UserInfo struct {
|
|
ID string `json:"id"`
|
|
Email string `json:"email"`
|
|
Username string `json:"username"`
|
|
Name string `json:"name"`
|
|
Active bool `json:"active"`
|
|
EmailVerified bool `json:"emailVerified"`
|
|
LastLogin *time.Time `json:"lastLogin"`
|
|
Roles []RoleInfo `json:"roles"`
|
|
Permissions []string `json:"permissions"`
|
|
DateCreated time.Time `json:"dateCreated"`
|
|
}
|
|
|
|
// ChangePasswordRequest represents a password change request
|
|
type ChangePasswordRequest struct {
|
|
CurrentPassword string `json:"currentPassword" validate:"required"`
|
|
NewPassword string `json:"newPassword" validate:"required,min=8"`
|
|
}
|
|
|
|
// ResetPasswordRequest represents a password reset request
|
|
type ResetPasswordRequest struct {
|
|
Email string `json:"email" validate:"required,email"`
|
|
}
|
|
|
|
// ResetPasswordConfirmRequest represents a password reset confirmation
|
|
type ResetPasswordConfirmRequest struct {
|
|
Token string `json:"token" validate:"required"`
|
|
NewPassword string `json:"newPassword" validate:"required,min=8"`
|
|
}
|
|
|
|
// BeforeCreate is called before creating a user
|
|
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
|
u.BaseModel.BeforeCreate()
|
|
|
|
// Normalize email and username
|
|
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
|
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
|
|
u.Name = strings.TrimSpace(u.Name)
|
|
|
|
return u.Validate()
|
|
}
|
|
|
|
// BeforeUpdate is called before updating a user
|
|
func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
|
u.BaseModel.BeforeUpdate()
|
|
|
|
// Normalize fields if they're being updated
|
|
if u.Email != "" {
|
|
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
|
}
|
|
if u.Username != "" {
|
|
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
|
|
}
|
|
if u.Name != "" {
|
|
u.Name = strings.TrimSpace(u.Name)
|
|
}
|
|
|
|
return u.Validate()
|
|
}
|
|
|
|
// Validate validates user data
|
|
func (u *User) Validate() error {
|
|
if u.Email == "" {
|
|
return errors.New("email is required")
|
|
}
|
|
|
|
if !isValidEmail(u.Email) {
|
|
return errors.New("invalid email format")
|
|
}
|
|
|
|
if u.Username == "" {
|
|
return errors.New("username is required")
|
|
}
|
|
|
|
if len(u.Username) < 3 || len(u.Username) > 50 {
|
|
return errors.New("username must be between 3 and 50 characters")
|
|
}
|
|
|
|
if !isValidUsername(u.Username) {
|
|
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
|
|
}
|
|
|
|
if u.Name == "" {
|
|
return errors.New("name is required")
|
|
}
|
|
|
|
if len(u.Name) < 2 || len(u.Name) > 100 {
|
|
return errors.New("name must be between 2 and 100 characters")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetPassword sets the user's password hash
|
|
func (u *User) SetPassword(plainPassword string) error {
|
|
if err := validatePassword(plainPassword); err != nil {
|
|
return err
|
|
}
|
|
|
|
hash, err := password.HashPassword(plainPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u.PasswordHash = hash
|
|
return nil
|
|
}
|
|
|
|
// CheckPassword verifies the user's password
|
|
func (u *User) CheckPassword(plainPassword string) bool {
|
|
return password.CheckPasswordHash(plainPassword, u.PasswordHash)
|
|
}
|
|
|
|
// VerifyPassword verifies the user's password (alias for CheckPassword)
|
|
func (u *User) VerifyPassword(plainPassword string) bool {
|
|
return u.CheckPassword(plainPassword)
|
|
}
|
|
|
|
// IsLocked checks if the user account is locked
|
|
func (u *User) IsLocked() bool {
|
|
if u.LockedUntil == nil {
|
|
return false
|
|
}
|
|
return time.Now().Before(*u.LockedUntil)
|
|
}
|
|
|
|
// Lock locks the user account for the specified duration
|
|
func (u *User) Lock(duration time.Duration) {
|
|
lockUntil := time.Now().Add(duration)
|
|
u.LockedUntil = &lockUntil
|
|
}
|
|
|
|
// Unlock unlocks the user account
|
|
func (u *User) Unlock() {
|
|
u.LockedUntil = nil
|
|
u.LoginAttempts = 0
|
|
}
|
|
|
|
// IncrementLoginAttempts increments the login attempt counter
|
|
func (u *User) IncrementLoginAttempts() {
|
|
u.LoginAttempts++
|
|
}
|
|
|
|
// ResetLoginAttempts resets the login attempt counter
|
|
func (u *User) ResetLoginAttempts() {
|
|
u.LoginAttempts = 0
|
|
}
|
|
|
|
// UpdateLastLogin updates the last login timestamp
|
|
func (u *User) UpdateLastLogin() {
|
|
now := time.Now()
|
|
u.LastLogin = &now
|
|
}
|
|
|
|
// ToUserInfo converts User to UserInfo (public information)
|
|
func (u *User) ToUserInfo() UserInfo {
|
|
userInfo := UserInfo{
|
|
ID: u.ID,
|
|
Email: u.Email,
|
|
Username: u.Username,
|
|
Name: u.Name,
|
|
Active: u.Active,
|
|
EmailVerified: u.EmailVerified,
|
|
LastLogin: u.LastLogin,
|
|
DateCreated: u.DateCreated,
|
|
Roles: make([]RoleInfo, len(u.Roles)),
|
|
Permissions: []string{},
|
|
}
|
|
|
|
// Convert roles and collect permissions
|
|
permissionSet := make(map[string]bool)
|
|
for i, role := range u.Roles {
|
|
userInfo.Roles[i] = role.ToRoleInfo()
|
|
for _, permission := range role.Permissions {
|
|
permissionSet[permission.Name] = true
|
|
}
|
|
}
|
|
|
|
// Convert permission set to slice
|
|
for permission := range permissionSet {
|
|
userInfo.Permissions = append(userInfo.Permissions, permission)
|
|
}
|
|
|
|
return userInfo
|
|
}
|
|
|
|
// HasRole checks if the user has a specific role
|
|
func (u *User) HasRole(roleName string) bool {
|
|
for _, role := range u.Roles {
|
|
if role.Name == roleName {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasPermission checks if the user has a specific permission
|
|
func (u *User) HasPermission(permissionName string) bool {
|
|
for _, role := range u.Roles {
|
|
for _, permission := range role.Permissions {
|
|
if permission.Name == permissionName {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isValidEmail validates email format
|
|
func isValidEmail(email string) bool {
|
|
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
|
return emailRegex.MatchString(email)
|
|
}
|
|
|
|
// isValidUsername validates username format
|
|
func isValidUsername(username string) bool {
|
|
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
|
return usernameRegex.MatchString(username)
|
|
}
|
|
|
|
// validatePassword validates password strength
|
|
func validatePassword(password string) error {
|
|
if len(password) < 8 {
|
|
return errors.New("password must be at least 8 characters long")
|
|
}
|
|
|
|
if len(password) > 128 {
|
|
return errors.New("password must not exceed 128 characters")
|
|
}
|
|
|
|
// Check for at least one lowercase letter
|
|
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
|
|
return errors.New("password must contain at least one lowercase letter")
|
|
}
|
|
|
|
// Check for at least one uppercase letter
|
|
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
|
|
return errors.New("password must contain at least one uppercase letter")
|
|
}
|
|
|
|
// Check for at least one digit
|
|
if matched, _ := regexp.MatchString(`\d`, password); !matched {
|
|
return errors.New("password must contain at least one digit")
|
|
}
|
|
|
|
// Check for at least one special character
|
|
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
|
|
return errors.New("password must contain at least one special character")
|
|
}
|
|
|
|
return nil
|
|
}
|