package db import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) type User struct { ID uuid.UUID `json:"id"` Email string `json:"email"` Name *string `json:"name,omitempty"` PasswordHash *string `json:"-"` EmailVerified bool `json:"email_verified"` Provider string `json:"provider"` ProviderID *string `json:"provider_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` LastLoginAt *time.Time `json:"last_login_at,omitempty"` } type MagicLink struct { Token string `json:"token"` UserID uuid.UUID `json:"user_id"` Email string `json:"email"` Used bool `json:"used"` ExpiresAt time.Time `json:"expires_at"` CreatedAt time.Time `json:"created_at"` } func (db *DB) GetUserByEmail(ctx context.Context, email string) (*User, error) { var user User var name, passwordHash, providerID *string var lastLoginAt *time.Time err := db.QueryRow(ctx, ` SELECT id, email, name, password_hash, email_verified, provider, provider_id, created_at, updated_at, last_login_at FROM users WHERE email = $1 `, email).Scan( &user.ID, &user.Email, &name, &passwordHash, &user.EmailVerified, &user.Provider, &providerID, &user.CreatedAt, &user.UpdatedAt, &lastLoginAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } user.Name = name user.PasswordHash = passwordHash user.ProviderID = providerID user.LastLoginAt = lastLoginAt return &user, nil } func (db *DB) GetUserByID(ctx context.Context, id uuid.UUID) (*User, error) { var user User var name, passwordHash, providerID *string var lastLoginAt *time.Time err := db.QueryRow(ctx, ` SELECT id, email, name, password_hash, email_verified, provider, provider_id, created_at, updated_at, last_login_at FROM users WHERE id = $1 `, id).Scan( &user.ID, &user.Email, &name, &passwordHash, &user.EmailVerified, &user.Provider, &providerID, &user.CreatedAt, &user.UpdatedAt, &lastLoginAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } user.Name = name user.PasswordHash = passwordHash user.ProviderID = providerID user.LastLoginAt = lastLoginAt return &user, nil } func (db *DB) GetUserByProviderID(ctx context.Context, provider, providerID string) (*User, error) { var user User var name, passwordHash *string var lastLoginAt *time.Time err := db.QueryRow(ctx, ` SELECT id, email, name, password_hash, email_verified, provider, provider_id, created_at, updated_at, last_login_at FROM users WHERE provider = $1 AND provider_id = $2 `, provider, providerID).Scan( &user.ID, &user.Email, &name, &passwordHash, &user.EmailVerified, &user.Provider, &user.ProviderID, &user.CreatedAt, &user.UpdatedAt, &lastLoginAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } user.Name = name user.PasswordHash = passwordHash user.LastLoginAt = lastLoginAt return &user, nil } func (db *DB) CreateUser(ctx context.Context, user *User) (*User, error) { if user.ID == uuid.Nil { user.ID = uuid.Must(uuid.NewV7()) } now := time.Now() user.CreatedAt = now user.UpdatedAt = now _, err := db.pool.Exec(ctx, ` INSERT INTO users (id, email, name, password_hash, email_verified, provider, provider_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8) `, user.ID, user.Email, user.Name, user.PasswordHash, user.EmailVerified, user.Provider, user.ProviderID, now) if err != nil { return nil, fmt.Errorf("create user: %w", err) } return user, nil } func (db *DB) UpdateUser(ctx context.Context, user *User) error { user.UpdatedAt = time.Now() _, err := db.pool.Exec(ctx, ` UPDATE users SET email = $2, name = $3, password_hash = $4, email_verified = $5, provider = $6, provider_id = $7, updated_at = $8, last_login_at = $9 WHERE id = $1 `, user.ID, user.Email, user.Name, user.PasswordHash, user.EmailVerified, user.Provider, user.ProviderID, user.UpdatedAt, user.LastLoginAt) return err } func (db *DB) UpdateLastLogin(ctx context.Context, userID uuid.UUID) error { _, err := db.pool.Exec(ctx, ` UPDATE users SET last_login_at = NOW(), updated_at = NOW() WHERE id = $1 `, userID) return err } func (db *DB) CreateMagicLink(ctx context.Context, token string, email string, userID uuid.UUID, expiresAt time.Time) error { _, err := db.pool.Exec(ctx, ` INSERT INTO magic_links (token, user_id, email, expires_at, created_at) VALUES ($1, $2, $3, $4, NOW()) `, token, userID, email, expiresAt) return err } func (db *DB) GetMagicLink(ctx context.Context, token string) (*MagicLink, error) { var ml MagicLink var userID uuid.UUID err := db.QueryRow(ctx, ` SELECT token, user_id, email, used, expires_at, created_at FROM magic_links WHERE token = $1 `, token).Scan(&ml.Token, &userID, &ml.Email, &ml.Used, &ml.ExpiresAt, &ml.CreatedAt) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } ml.UserID = userID return &ml, nil } func (db *DB) MarkMagicLinkUsed(ctx context.Context, token string) error { _, err := db.pool.Exec(ctx, `UPDATE magic_links SET used = true WHERE token = $1`, token) return err } func (db *DB) PutKV(ctx context.Context, key string, value any) error { payload, err := json.Marshal(value) if err != nil { return fmt.Errorf("marshal kv value: %w", err) } _, err = db.pool.Exec(ctx, ` INSERT INTO stripe_kv (key, value, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW() `, key, payload) return err } func (db *DB) GetKV(ctx context.Context, key string, dest any) (bool, error) { var payload []byte err := db.QueryRow(ctx, ` SELECT value FROM stripe_kv WHERE key = $1 `, key).Scan(&payload) if err == pgx.ErrNoRows { return false, nil } if err != nil { return false, err } if err := json.Unmarshal(payload, dest); err != nil { return false, fmt.Errorf("unmarshal kv value: %w", err) } return true, nil }