137 lines
5.4 KiB
Go
137 lines
5.4 KiB
Go
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"`
|
||
}
|
||
|
||
// 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"`
|
||
}
|