package models import ( "encoding/json" "time" ) // User represents a registered user in the system. type User struct { ID string `json:"id"` Email string `json:"email"` Username string `json:"username"` // Unique handle; empty until set Name string `json:"name"` PasswordHash string `json:"-"` // Never serialized Role string `json:"role"` // "admin" or "member" AvatarURL string `json:"avatar_url,omitempty"` TOTPSecret string `json:"-"` // Never serialized TOTPEnabled bool `json:"totp_enabled"` RecoveryCodes string `json:"-"` // Never serialized Plan string `json:"plan"` // "free", "pro", or "self-hosted" PlanExpiresAt string `json:"plan_expires_at,omitempty"` StripeCustomerID string `json:"-"` // Never serialized PlanOverrides string `json:"plan_overrides,omitempty"` // JSON overrides for per-user limits OAuthProviders string `json:"-"` // JSON array of linked providers, e.g. ["github","google"] PasswordSet bool `json:"password_set"` // True if the user explicitly set a password (vs. OAuth placeholder hash) DisabledAt string `json:"disabled_at,omitempty"` // Non-empty = account disabled EmailVerifiedAt string `json:"email_verified_at,omitempty"` // Non-empty = email verified (mirror DisabledAt). Empty/NULL = unverified. PLAN-1933 / TASK-1935. LastActiveAt string `json:"last_active_at,omitempty"` // Last authenticated API request (any read or write) LastWriteAt string `json:"last_write_at,omitempty"` // Last mutating action (item/comment/attachment); see Store.TouchUserWrite. PLAN-1542 / TASK-1543. CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // IsDisabled returns true if the user account has been disabled. func (u *User) IsDisabled() bool { return u.DisabledAt != "" } // IsEmailVerified returns true if the user's email address has been verified. // Mirrors IsDisabled: a non-empty EmailVerifiedAt timestamp means verified, // empty (NULL in the DB) means unverified. PLAN-1933 / TASK-1935. func (u *User) IsEmailVerified() bool { return u.EmailVerifiedAt != "" } // GetOAuthProviders parses the JSON oauth_providers field into a string slice. func (u *User) GetOAuthProviders() []string { if u.OAuthProviders == "" { return nil } var providers []string if err := json.Unmarshal([]byte(u.OAuthProviders), &providers); err != nil { return nil } return providers } // HasOAuthProvider returns true if the user has linked the given provider. func (u *User) HasOAuthProvider(provider string) bool { for _, p := range u.GetOAuthProviders() { if p == provider { return true } } return false } // HasPassword returns true if the user has explicitly set a password that // they can sign in with. OAuth-only users have a random placeholder hash // stored in PasswordHash which can't actually be used to log in, so this // bit is tracked separately from PasswordHash being non-empty. func (u *User) HasPassword() bool { return u.PasswordSet } // UserCreate is the input for registering a new user. type UserCreate struct { Email string `json:"email"` Username string `json:"username,omitempty"` // Optional; auto-generated if empty Name string `json:"name"` Password string `json:"password"` // Plaintext, will be hashed Role string `json:"role,omitempty"` // Defaults to "member" // Unverified requests that the created user start with an UNVERIFIED email // (email_verified_at = NULL). The zero value (false) yields a VERIFIED user, // so a call site that forgets to set it fails SAFE — verified, not // write-locked (DR-3). The ONLY path that sets this true is the future // cloud self-serve signup branch (PLAN-1933 Wave 3), which does not exist // yet. Not settable via the request body (json:"-") — the handler decides. Unverified bool `json:"-"` } // UserUpdate is the input for updating user profile fields. type UserUpdate struct { Name *string `json:"name,omitempty"` Username *string `json:"username,omitempty"` Password *string `json:"password,omitempty"` // Plaintext, will be hashed AvatarURL *string `json:"avatar_url,omitempty"` } // Session represents a database-backed authentication session. type Session struct { ID string `json:"id"` UserID string `json:"user_id"` TokenHash string `json:"-"` // Never serialized DeviceInfo string `json:"device_info,omitempty"` ExpiresAt time.Time `json:"expires_at"` CreatedAt time.Time `json:"created_at"` } // WorkspaceInvitation represents a pending invitation to join a workspace. type WorkspaceInvitation struct { ID string `json:"id"` WorkspaceID string `json:"workspace_id"` Email string `json:"email"` Role string `json:"role"` InvitedBy string `json:"invited_by"` Code string `json:"code"` AcceptedAt *time.Time `json:"accepted_at,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` CreatedAt time.Time `json:"created_at"` } // IsExpired reports whether an invitation is past its expiration window. // Invitations created before the expires_at migration (nil ExpiresAt) are // treated as non-expiring so existing codes don't break on upgrade. func (inv *WorkspaceInvitation) IsExpired() bool { if inv == nil || inv.ExpiresAt == nil { return false } return time.Now().UTC().After(*inv.ExpiresAt) } // WorkspaceMember represents a user's membership in a workspace. type WorkspaceMember struct { WorkspaceID string `json:"workspace_id"` UserID string `json:"user_id"` Role string `json:"role"` // "owner", "editor", or "viewer" CollectionAccess string `json:"collection_access"` // "all" or "specific" CreatedAt time.Time `json:"created_at"` // Populated by joins (not stored) UserName string `json:"user_name,omitempty"` UserEmail string `json:"user_email,omitempty"` UserUsername string `json:"user_username,omitempty"` }