Files
omega-server/local/model/user.go
Fran Jurmanović 016728532c init bootstrap
2025-07-06 15:02:09 +02:00

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
}