package model import ( "encoding/json" "time" ) // User represents a row in the `users` table. // // PasswordHash is tagged `json:"-"` so it never leaks via JSON marshalling // even if a caller accidentally passes a User through response.JSON. It's a // pointer because OAuth-only users have no password — *PasswordHash == nil // means "this account can only sign in via a linked OAuth provider". // // UserSummary is the lightweight projection used in follower / following // lists — just enough to render a row and link to the profile. type UserSummary struct { // ID is populated only where the caller needs it (e.g. mention add/remove); // omitted from follower/like lists that link by handle. ID string `json:"id,omitempty"` Handle string `json:"handle"` Name string `json:"name"` AvatarURL *string `json:"avatar_url"` } // EmailVerifiedAt is a pointer so JSON-encoded users carry an explicit `null` // when the user has not yet verified, instead of an omitted field — the // frontend uses this to decide whether to show the "verify your email" ribbon. type User struct { ID string `json:"id"` // Email is nullable: OAuth users (typically Facebook accounts with no // verified email on file) sign up without one and can fill it in later // from their profile. Email *string `json:"email"` Handle string `json:"handle"` Name string `json:"name"` AvatarURL *string `json:"avatar_url"` Role string `json:"role"` Plan string `json:"plan"` // "free" | "premium" // Country is the account's ISO-3166-1 alpha-2 code (e.g. "IN"), // captured best-effort at signup and editable from the profile form. // nil until known. Country *string `json:"country"` // --- Devotional profile (user-editable) --- // Gender is a free-but-constrained string ("male"|"female"|"other"), // nil when unspecified. Gender *string `json:"gender"` // Sampradaya is the devotee's spiritual lineage (a lowercase code, // e.g. "gaudiya"). Optional — new accounts default to "gaudiya" but it // can be cleared, so nil means "not specified". Sampradaya *string `json:"sampradaya"` // Initiated marks whether the devotee has received initiation. Initiated bool `json:"initiated"` // InitiatedAt is the initiation date (DATE column), nil when unknown or // not initiated. InitiatedAt *time.Time `json:"initiated_at"` // DailyChantingRounds is the devotee's daily japa round vow (1–1008), // nil when unspecified. DailyChantingRounds *int `json:"daily_chanting_rounds"` // --- Notification scheduling (system-managed; no UI yet) --- // PreferredChantingTime / PeakActiveTime store a local clock time as // "HH:MM". They're written silently by the backend to time automated // notifications; the scheduling logic ships later. nil until computed. PreferredChantingTime *string `json:"preferred_chanting_time"` PeakActiveTime *string `json:"peak_active_time"` EmailVerifiedAt *time.Time `json:"email_verified_at"` PasswordHash *string `json:"-"` TokenVersion int `json:"-"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // BannedAt suspends the account when set: RequireAuth rejects the session // and LogIn refuses to issue a new one. nil = in good standing. BannedAt *time.Time `json:"banned_at,omitempty"` } // MarshalJSON adds a derived `has_password` flag so the frontend can pick // "Change password" vs "Set a password" for OAuth-only accounts, without // the password hash itself ever leaving the server. func (u User) MarshalJSON() ([]byte, error) { type alias User return json.Marshal(&struct { alias HasPassword bool `json:"has_password"` }{ alias: alias(u), HasPassword: u.PasswordHash != nil, }) } // AuthToken represents a row in the `auth_tokens` table. // Used for both password reset and email verification flows. type AuthToken struct { ID string `json:"id"` UserID string `json:"user_id"` Purpose string `json:"purpose"` // "password_reset" | "email_verification" TokenHash string `json:"-"` ExpiresAt time.Time `json:"expires_at"` UsedAt *time.Time `json:"used_at,omitempty"` CreatedAt time.Time `json:"created_at"` } const ( PurposePasswordReset = "password_reset" PurposeEmailVerification = "email_verification" // PurposeLoginOTP is a 6-digit one-time code emailed to a user whose // account has been unverified for >30 days. Verification happens // per-user (FindValidForUser), so the 1M-keyspace can't be brute-forced // offline by hash collision — only online against rate limits. PurposeLoginOTP = "login_otp" ) // OAuthAccount represents a row in the `oauth_accounts` table — one entry // per (provider, provider_user_id). A single user can have several linked // providers. type OAuthAccount struct { ID string `json:"id"` UserID string `json:"user_id"` Provider string `json:"provider"` ProviderUserID string `json:"provider_user_id"` Email *string `json:"email"` Name *string `json:"name"` AvatarURL *string `json:"avatar_url"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ShortURL represents a shortened URL. type ShortURL struct { ID string `json:"id"` Slug string `json:"slug"` TargetURL string `json:"target_url"` CreatedBy *string `json:"created_by,omitempty"` ClickCount int64 `json:"click_count"` CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` }