Files
spsays-bff/internal/model/user.go
T

134 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
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"`
}