364 lines
13 KiB
Go
364 lines
13 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// User represents a user account in the system.
|
|
// This is the core user entity containing authentication and profile information.
|
|
//
|
|
// What is a user?
|
|
// - Account with unique email
|
|
// - Associated with a tenant (multi-tenancy)
|
|
// - Has roles for authorization
|
|
// - Contains profile information
|
|
// - Tracks authentication state
|
|
//
|
|
// User lifecycle:
|
|
// 1. Created: During registration
|
|
// 2. Pending: Email verification required (optional)
|
|
// 3. Active: Can log in and use system
|
|
// 4. Suspended: Temporarily disabled
|
|
// 5. Deleted: Soft deleted (deleted_at set)
|
|
//
|
|
// Multi-tenancy:
|
|
// - Each user belongs to one tenant (organization)
|
|
// - Tenant ID used for data isolation
|
|
// - Cross-tenant access not allowed
|
|
// - Important for SaaS applications
|
|
//
|
|
// Security considerations:
|
|
// - Password never exposed (json:"-" tag)
|
|
// - Soft delete preserves data integrity
|
|
// - Email is unique identifier
|
|
// - Role-based access control
|
|
type User struct {
|
|
// ID is the unique identifier for this user
|
|
// Type: UUID v4 (128-bit random identifier)
|
|
// Generated: Automatically by database on insert
|
|
// Primary key: Used for all lookups and relationships
|
|
ID uuid.UUID `json:"id" bson:"id"`
|
|
|
|
// TenantID identifies which organization this user belongs to
|
|
// Type: UUID v4 (foreign key to tenants table)
|
|
// Multi-tenancy: Isolates data between organizations
|
|
// Required: Every user must belong to a tenant
|
|
// Used for: Filtering queries, enforcing data isolation
|
|
TenantID uuid.UUID `json:"tenant_id" bson:"tenant_id"`
|
|
|
|
// Email is the user's email address (unique identifier for login)
|
|
// Type: String (validated format, max 254 chars)
|
|
// Unique: Within system (can't have duplicate accounts)
|
|
// Normalized: Stored as lowercase for consistent matching
|
|
// Used for: Login, communication, uniqueness
|
|
// Privacy: PII, must be protected
|
|
Email string `json:"email" db:"email"`
|
|
|
|
// PasswordHash is the bcrypt hash of the user's password
|
|
// Type: Optional string (can be null for social login)
|
|
// Format: "$2a$10$..." (bcrypt hash with salt)
|
|
// Security: NEVER expose in API responses (json:"-" means omit)
|
|
// Hashing: Bcrypt with cost factor 10
|
|
// Why optional: Users with social login may not have password
|
|
PasswordHash *string `json:"_" db:"password_hash"`
|
|
|
|
// FirstName is the user's first name
|
|
// Type: Optional string (can be null)
|
|
// Used for: Personalization, display
|
|
// Privacy: PII, must be protected
|
|
FirstName *string `json:"first_name" db:"first_name"`
|
|
|
|
// LastName is the user's last name
|
|
// Type: Optional string (can be null)
|
|
// Used for: Personalization, display
|
|
// Privacy: PII, must be protected
|
|
LastName *string `json:"last_name" db:"last_name"`
|
|
|
|
// FullName is the computed full name (first + last)
|
|
// Type: String (computed by database trigger or application)
|
|
// Generated: From first_name and last_name
|
|
// Used for: Display purposes, searching
|
|
// Database: Might be a generated column or manually updated
|
|
FullName string `json:"full_name" db:"full_name"`
|
|
|
|
// AvatarURL is the URL to the user's profile picture
|
|
// Type: Optional string (can be null)
|
|
// Storage: URL points to file in object storage (MinIO/S3)
|
|
// Default: System can provide default avatar if null
|
|
// Privacy: Publicly accessible or requires auth
|
|
AvatarURL *string `json:"avatar_url" db:"avatar_url"`
|
|
|
|
// Phone is the user's phone number
|
|
// Type: Optional string (can be null)
|
|
// Format: Should be E.164 format (+1234567890)
|
|
// Used for: 2FA, notifications, contact
|
|
// Privacy: PII, must be protected
|
|
// Verification: Should require phone verification
|
|
Phone *string `json:"phone" db:"phone"`
|
|
|
|
// Role defines the user's permissions level
|
|
// Type: String (enum-like values)
|
|
// Common values: "admin", "user", "manager", "viewer"
|
|
// Used for: Authorization checks, feature access
|
|
// Default: Usually "user" for new accounts
|
|
// Important: Always check role before allowing operations
|
|
Role string `json:"role" db:"role"`
|
|
|
|
// Status indicates the current state of the account
|
|
// Type: String (enum-like values)
|
|
// Possible values: "active", "pending", "suspended", "deleted"
|
|
// Active: Can log in normally
|
|
// Pending: Awaiting email verification
|
|
// Suspended: Temporarily disabled (can't log in)
|
|
// Deleted: Soft deleted (should have deleted_at set)
|
|
Status string `json:"status" db:"status"`
|
|
|
|
// EmailVerified indicates if email has been verified
|
|
// Type: Boolean (default false)
|
|
// Purpose: Confirm user owns the email address
|
|
// Workflow: User clicks link in verification email
|
|
// Requirement: Some systems require verification before full access
|
|
EmailVerified bool `json:"email_verified" db:"email_verified"`
|
|
|
|
// EmailVerifiedAt is when the email was verified
|
|
// Type: Optional timestamp (null if not verified)
|
|
// Set when: User clicks verification link
|
|
// Used for: Audit trail, resend logic
|
|
EmailVerifiedAt *time.Time `json:"email_verified_at" db:"email_verified_at"`
|
|
|
|
// IsOnboarded indicates if user completed onboarding
|
|
// Type: Boolean (default false)
|
|
// Purpose: Track if user saw welcome/tutorial
|
|
// Used for: Showing onboarding flow
|
|
// Set to true: After user completes onboarding steps
|
|
IsOnboarded bool `json:"is_onboarded" db:"is_onboarded"`
|
|
|
|
// LastLoginAt is when the user last logged in
|
|
// Type: Optional timestamp (null if never logged in)
|
|
// Updated: After successful authentication
|
|
// Used for: Security monitoring, activity tracking
|
|
// Display: "Last login: 2 hours ago"
|
|
LastLoginAt *time.Time `json:"last_login_at" db:"last_login_at"`
|
|
|
|
// LastLoginIP is the IP address of last login
|
|
// Type: Optional string (null if never logged in)
|
|
// Format: IPv4 or IPv6
|
|
// Used for: Security monitoring, anomaly detection
|
|
// Privacy: PII under GDPR, may need anonymization
|
|
LastLoginIP *string `json:"last_login_ip" db:"last_login_ip"`
|
|
|
|
// CreatedAt is when the user account was created
|
|
// Type: Timestamp (automatically set by database)
|
|
// Used for: Account age, analytics, sorting
|
|
// Database: Set by DEFAULT NOW() in schema
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
|
|
// UpdatedAt is when the user record was last modified
|
|
// Type: Timestamp (automatically updated)
|
|
// Updated: Any time user record changes
|
|
// Used for: Audit trail, change tracking
|
|
// Database: Updated by trigger or application
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
|
|
// DeletedAt is when the user was soft deleted
|
|
// Type: Optional timestamp (null if not deleted)
|
|
// Soft delete: Record preserved but marked as deleted
|
|
// Used for: Data integrity, audit trail
|
|
// Omitted from JSON if null (json:"deleted_at,omitempty")
|
|
// Queries: Filter WHERE deleted_at IS NULL
|
|
DeletedAt *time.Time `json:"deleted_at,omitempty" db:"deleted_at"`
|
|
}
|
|
|
|
// CreateUserInput contains the data needed to create a new user account.
|
|
// This is a DTO (Data Transfer Object) for the registration/user creation flow.
|
|
//
|
|
// Why separate input struct?
|
|
// - Separates client input from database-generated fields
|
|
// - Clear interface for what's required vs what's generated
|
|
// - Type safety (can't set ID, timestamps, etc.)
|
|
// - Validation happens on this struct
|
|
//
|
|
// When used:
|
|
// - User registration
|
|
// - Admin creating user
|
|
// - Invitation flow
|
|
type CreateUserInput struct {
|
|
// TenantID is which organization the user belongs to
|
|
// Required: Every user must belong to a tenant
|
|
// Set by: System (based on signup domain, invitation, etc.)
|
|
TenantID uuid.UUID
|
|
|
|
// Email is the user's email address
|
|
// Required: Used for login and communication
|
|
// Validation: Must be valid email format, must be unique
|
|
// Normalized: Will be lowercased before storage
|
|
Email string
|
|
|
|
// Password is the plaintext password
|
|
// Required: Must meet strength requirements
|
|
// Security: Will be hashed with bcrypt before storage
|
|
// Validation: Checked for length, complexity, common patterns
|
|
// NEVER logged or stored plaintext
|
|
Password string
|
|
|
|
// FirstName is the user's first name
|
|
// Optional: Can be null (but recommended)
|
|
// Used for: Personalization, display
|
|
FirstName *string
|
|
|
|
// LastName is the user's last name
|
|
// Optional: Can be null (but recommended)
|
|
// Used for: Personalization, display
|
|
LastName *string
|
|
|
|
// Role is the user's permission level
|
|
// Required: Must be set (often defaults to "user")
|
|
// Values: "admin", "user", "manager", etc.
|
|
// Set by: System (based on registration type) or admin
|
|
Role string
|
|
|
|
// Status is the initial account status
|
|
// Required: Usually "pending" or "active"
|
|
// "pending": Requires email verification
|
|
// "active": Can log in immediately
|
|
// Set by: System based on email verification policy
|
|
Status string
|
|
}
|
|
|
|
// UserResponse is a sanitized user object for API responses.
|
|
// This DTO removes sensitive fields before sending to client.
|
|
//
|
|
// Why separate response struct?
|
|
// - Security: Never expose password_hash or internal IDs
|
|
// - API contract: Clear definition of what clients receive
|
|
// - Flexibility: Can add computed fields without changing User model
|
|
// - Versioning: Can have different responses for API versions
|
|
//
|
|
// What's excluded from User:
|
|
// - PasswordHash: NEVER expose password hashes
|
|
// - DeletedAt: Internal field, not relevant to client
|
|
// - Internal IDs: Some may be excluded depending on use case
|
|
//
|
|
// When used:
|
|
// - Login response
|
|
// - Profile endpoint
|
|
// - User list endpoints
|
|
// - Any API response containing user data
|
|
type UserResponse struct {
|
|
// ID is the user's unique identifier
|
|
ID uuid.UUID `json:"id"`
|
|
|
|
// TenantID for multi-tenancy
|
|
TenantID uuid.UUID `json:"tenant_id"`
|
|
|
|
// Email for display and communication
|
|
Email string `json:"email"`
|
|
|
|
// FirstName for personalization
|
|
// Note: JSON tag shows "name" but field is FirstName (might be typo)
|
|
FirstName *string `json:"name"`
|
|
|
|
// LastName for full name display
|
|
LastName *string `json:"last_name"`
|
|
|
|
// FullName computed from first + last
|
|
FullName string `json:"full_name"`
|
|
|
|
// AvatarURL for profile picture
|
|
AvatarURL *string `json:"avatar_url"`
|
|
|
|
// Phone for contact
|
|
Phone *string `json:"phone"`
|
|
|
|
// Role for client-side permission checks
|
|
Role string `json:"role"`
|
|
|
|
// Status to show account state
|
|
Status string `json:"status"`
|
|
|
|
// EmailVerified to prompt verification if needed
|
|
EmailVerified bool `json:"email_verified"`
|
|
|
|
// IsOnboarded to show onboarding if needed
|
|
IsOnboarded bool `json:"is_onboarded"`
|
|
|
|
// LastLoginAt for security awareness
|
|
LastLoginAt *time.Time `json:"last_login_at"`
|
|
|
|
// CreatedAt for account age
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// Note: Excludes PasswordHash, DeletedAt, UpdatedAt, LastLoginIP
|
|
}
|
|
|
|
// ToResponse converts a User model to a UserResponse DTO.
|
|
// This method sanitizes the user object before sending to client.
|
|
//
|
|
// Why this method?
|
|
// - Encapsulation: Conversion logic lives with the model
|
|
// - Reusability: Can be called anywhere needed
|
|
// - Type safety: Returns correct response type
|
|
// - Maintainability: One place to update response structure
|
|
//
|
|
// Usage:
|
|
//
|
|
// user := getUserFromDB()
|
|
// response := user.ToResponse()
|
|
// return c.JSON(http.StatusOK, response)
|
|
//
|
|
// What it does:
|
|
// - Copies public fields from User to UserResponse
|
|
// - Excludes sensitive fields (password_hash)
|
|
// - Excludes internal fields (deleted_at, updated_at, last_login_ip)
|
|
//
|
|
// Returns:
|
|
// - UserResponse with safe fields populated
|
|
func (u *User) ToResponse() *UserResponse {
|
|
return &UserResponse{
|
|
ID: u.ID,
|
|
TenantID: u.TenantID,
|
|
Email: u.Email,
|
|
FirstName: u.FirstName,
|
|
LastName: u.LastName,
|
|
FullName: u.FullName,
|
|
AvatarURL: u.AvatarURL,
|
|
Phone: u.Phone,
|
|
Role: u.Role,
|
|
Status: u.Status,
|
|
EmailVerified: u.EmailVerified,
|
|
IsOnboarded: u.IsOnboarded,
|
|
LastLoginAt: u.LastLoginAt,
|
|
CreatedAt: u.CreatedAt,
|
|
// Intentionally excluded:
|
|
// - PasswordHash (security)
|
|
// - LastLoginIP (privacy)
|
|
// - DeletedAt (internal)
|
|
// - UpdatedAt (internal)
|
|
// - EmailVerifiedAt (redundant with EmailVerified boolean)
|
|
}
|
|
}
|
|
|
|
// Note about database migration:
|
|
// The comment at the end of the original file mentions:
|
|
// "Current DB structure need to updated the migration script to reflect the current UserEntity"
|
|
//
|
|
// This suggests the database schema may be out of sync with this model.
|
|
// The old structure mentioned was:
|
|
// id | tenant_id | email | password_hash | name | avatar_url | role | is_active |
|
|
// email_verified_at | last_login_at | created_at | updated_at | deleted_at
|
|
//
|
|
// Differences from current model:
|
|
// 1. "name" field vs "first_name" + "last_name" + "full_name"
|
|
// 2. "is_active" field vs "status" field (enum)
|
|
// 3. Missing "phone" field
|
|
// 4. Missing "email_verified" boolean
|
|
// 5. Missing "is_onboarded" boolean
|
|
// 6. Missing "last_login_ip" field
|
|
//
|
|
// Action required:
|
|
// - Create database migration to update schema
|
|
// - Or update model to match current database (then migrate later)
|
|
// - Ensure model and database stay in sync
|