Files
spsays-bff/internal/model/user.go
T
2026-06-02 22:54:41 +05:30

97 lines
3.6 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".
//
// 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"
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"`
}