Switch to phone auth via Twilio SMS and add feature flag system

Replace email-based auth (Resend) with phone-based auth (Twilio SMS).
Add BBQ_FEATURES env var for toggling features at deploy time — when
auth is disabled, no login routes are registered and the app works
as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 08:45:15 -04:00
parent b3203a7506
commit 471cc3ad8c
15 changed files with 820 additions and 21 deletions
+23
View File
@@ -5,6 +5,7 @@
package db
import (
"database/sql"
"time"
)
@@ -25,6 +26,7 @@ type Event struct {
Location string
AdminToken string
Description string
UserID sql.NullInt64
CreatedAt time.Time
}
@@ -36,6 +38,12 @@ type Rsvp struct {
CreatedAt time.Time
}
type Session struct {
Token string
UserID int64
ExpiresAt time.Time
}
type Slot struct {
ID int64
EventID int64
@@ -44,3 +52,18 @@ type Slot struct {
MaxClaims int64
SortOrder int64
}
type User struct {
ID int64
Phone string
Name string
CreatedAt time.Time
}
type VerificationCode struct {
ID int64
Phone string
Code string
ExpiresAt time.Time
Used int64
}
+38
View File
@@ -68,3 +68,41 @@ DELETE FROM rsvps WHERE id = ?;
-- name: CountRsvps :one
SELECT COUNT(*) FROM rsvps WHERE event_id = ?;
-- name: GetUserByPhone :one
SELECT * FROM users WHERE phone = ?;
-- name: CreateUser :one
INSERT INTO users (phone, name) VALUES (?, '') RETURNING *;
-- name: UpdateUserName :exec
UPDATE users SET name = ? WHERE id = ?;
-- name: CreateSession :exec
INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?);
-- name: GetSession :one
SELECT * FROM sessions WHERE token = ? AND expires_at > CURRENT_TIMESTAMP;
-- name: DeleteSession :exec
DELETE FROM sessions WHERE token = ?;
-- name: DeleteExpiredSessions :exec
DELETE FROM sessions WHERE expires_at <= CURRENT_TIMESTAMP;
-- name: CreateVerificationCode :exec
INSERT INTO verification_codes (phone, code, expires_at) VALUES (?, ?, ?);
-- name: GetVerificationCode :one
SELECT * FROM verification_codes
WHERE phone = ? AND code = ? AND used = 0 AND expires_at > CURRENT_TIMESTAMP
ORDER BY id DESC LIMIT 1;
-- name: MarkVerificationCodeUsed :exec
UPDATE verification_codes SET used = 1 WHERE id = ?;
-- name: ListEventsByUser :many
SELECT * FROM events WHERE user_id = ? ORDER BY created_at DESC;
-- name: SetEventUser :exec
UPDATE events SET user_id = ? WHERE id = ?;
+198 -3
View File
@@ -7,6 +7,8 @@ package db
import (
"context"
"database/sql"
"time"
)
const countClaimsBySlot = `-- name: CountClaimsBySlot :one
@@ -59,7 +61,7 @@ func (q *Queries) CreateClaim(ctx context.Context, arg CreateClaimParams) (Claim
const createEvent = `-- name: CreateEvent :one
INSERT INTO events (slug, title, date, time, location, admin_token, description)
VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING id, slug, title, date, time, location, admin_token, description, created_at
RETURNING id, slug, title, date, time, location, admin_token, description, user_id, created_at
`
type CreateEventParams struct {
@@ -92,6 +94,7 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event
&i.Location,
&i.AdminToken,
&i.Description,
&i.UserID,
&i.CreatedAt,
)
return i, err
@@ -122,6 +125,21 @@ func (q *Queries) CreateRsvp(ctx context.Context, arg CreateRsvpParams) (Rsvp, e
return i, err
}
const createSession = `-- name: CreateSession :exec
INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)
`
type CreateSessionParams struct {
Token string
UserID int64
ExpiresAt time.Time
}
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) error {
_, err := q.db.ExecContext(ctx, createSession, arg.Token, arg.UserID, arg.ExpiresAt)
return err
}
const createSlot = `-- name: CreateSlot :one
INSERT INTO slots (event_id, name, emoji, max_claims, sort_order)
VALUES (?, ?, ?, ?, ?)
@@ -156,6 +174,37 @@ func (q *Queries) CreateSlot(ctx context.Context, arg CreateSlotParams) (Slot, e
return i, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (phone, name) VALUES (?, '') RETURNING id, phone, name, created_at
`
func (q *Queries) CreateUser(ctx context.Context, phone string) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, phone)
var i User
err := row.Scan(
&i.ID,
&i.Phone,
&i.Name,
&i.CreatedAt,
)
return i, err
}
const createVerificationCode = `-- name: CreateVerificationCode :exec
INSERT INTO verification_codes (phone, code, expires_at) VALUES (?, ?, ?)
`
type CreateVerificationCodeParams struct {
Phone string
Code string
ExpiresAt time.Time
}
func (q *Queries) CreateVerificationCode(ctx context.Context, arg CreateVerificationCodeParams) error {
_, err := q.db.ExecContext(ctx, createVerificationCode, arg.Phone, arg.Code, arg.ExpiresAt)
return err
}
const deleteClaim = `-- name: DeleteClaim :exec
DELETE FROM claims WHERE id = ?
`
@@ -174,6 +223,15 @@ func (q *Queries) DeleteEvent(ctx context.Context, id int64) error {
return err
}
const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec
DELETE FROM sessions WHERE expires_at <= CURRENT_TIMESTAMP
`
func (q *Queries) DeleteExpiredSessions(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteExpiredSessions)
return err
}
const deleteRsvp = `-- name: DeleteRsvp :exec
DELETE FROM rsvps WHERE id = ?
`
@@ -183,6 +241,15 @@ func (q *Queries) DeleteRsvp(ctx context.Context, id int64) error {
return err
}
const deleteSession = `-- name: DeleteSession :exec
DELETE FROM sessions WHERE token = ?
`
func (q *Queries) DeleteSession(ctx context.Context, token string) error {
_, err := q.db.ExecContext(ctx, deleteSession, token)
return err
}
const deleteSlot = `-- name: DeleteSlot :exec
DELETE FROM slots WHERE id = ?
`
@@ -193,7 +260,7 @@ func (q *Queries) DeleteSlot(ctx context.Context, id int64) error {
}
const getEventByAdminToken = `-- name: GetEventByAdminToken :one
SELECT id, slug, title, date, time, location, admin_token, description, created_at FROM events WHERE admin_token = ?
SELECT id, slug, title, date, time, location, admin_token, description, user_id, created_at FROM events WHERE admin_token = ?
`
func (q *Queries) GetEventByAdminToken(ctx context.Context, adminToken string) (Event, error) {
@@ -208,13 +275,14 @@ func (q *Queries) GetEventByAdminToken(ctx context.Context, adminToken string) (
&i.Location,
&i.AdminToken,
&i.Description,
&i.UserID,
&i.CreatedAt,
)
return i, err
}
const getEventBySlug = `-- name: GetEventBySlug :one
SELECT id, slug, title, date, time, location, admin_token, description, created_at FROM events WHERE slug = ?
SELECT id, slug, title, date, time, location, admin_token, description, user_id, created_at FROM events WHERE slug = ?
`
func (q *Queries) GetEventBySlug(ctx context.Context, slug string) (Event, error) {
@@ -229,11 +297,23 @@ func (q *Queries) GetEventBySlug(ctx context.Context, slug string) (Event, error
&i.Location,
&i.AdminToken,
&i.Description,
&i.UserID,
&i.CreatedAt,
)
return i, err
}
const getSession = `-- name: GetSession :one
SELECT token, user_id, expires_at FROM sessions WHERE token = ? AND expires_at > CURRENT_TIMESTAMP
`
func (q *Queries) GetSession(ctx context.Context, token string) (Session, error) {
row := q.db.QueryRowContext(ctx, getSession, token)
var i Session
err := row.Scan(&i.Token, &i.UserID, &i.ExpiresAt)
return i, err
}
const getSlot = `-- name: GetSlot :one
SELECT id, event_id, name, emoji, max_claims, sort_order FROM slots WHERE id = ?
`
@@ -252,6 +332,46 @@ func (q *Queries) GetSlot(ctx context.Context, id int64) (Slot, error) {
return i, err
}
const getUserByPhone = `-- name: GetUserByPhone :one
SELECT id, phone, name, created_at FROM users WHERE phone = ?
`
func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByPhone, phone)
var i User
err := row.Scan(
&i.ID,
&i.Phone,
&i.Name,
&i.CreatedAt,
)
return i, err
}
const getVerificationCode = `-- name: GetVerificationCode :one
SELECT id, phone, code, expires_at, used FROM verification_codes
WHERE phone = ? AND code = ? AND used = 0 AND expires_at > CURRENT_TIMESTAMP
ORDER BY id DESC LIMIT 1
`
type GetVerificationCodeParams struct {
Phone string
Code string
}
func (q *Queries) GetVerificationCode(ctx context.Context, arg GetVerificationCodeParams) (VerificationCode, error) {
row := q.db.QueryRowContext(ctx, getVerificationCode, arg.Phone, arg.Code)
var i VerificationCode
err := row.Scan(
&i.ID,
&i.Phone,
&i.Code,
&i.ExpiresAt,
&i.Used,
)
return i, err
}
const listClaimsByEvent = `-- name: ListClaimsByEvent :many
SELECT c.id, c.slot_id, c.name, c.note, c.created_at FROM claims c
JOIN slots s ON c.slot_id = s.id
@@ -321,6 +441,44 @@ func (q *Queries) ListClaimsBySlot(ctx context.Context, slotID int64) ([]Claim,
return items, nil
}
const listEventsByUser = `-- name: ListEventsByUser :many
SELECT id, slug, title, date, time, location, admin_token, description, user_id, created_at FROM events WHERE user_id = ? ORDER BY created_at DESC
`
func (q *Queries) ListEventsByUser(ctx context.Context, userID sql.NullInt64) ([]Event, error) {
rows, err := q.db.QueryContext(ctx, listEventsByUser, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Event
for rows.Next() {
var i Event
if err := rows.Scan(
&i.ID,
&i.Slug,
&i.Title,
&i.Date,
&i.Time,
&i.Location,
&i.AdminToken,
&i.Description,
&i.UserID,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRsvps = `-- name: ListRsvps :many
SELECT id, event_id, name, note, created_at FROM rsvps WHERE event_id = ? ORDER BY created_at
`
@@ -388,6 +546,29 @@ func (q *Queries) ListSlots(ctx context.Context, eventID int64) ([]Slot, error)
return items, nil
}
const markVerificationCodeUsed = `-- name: MarkVerificationCodeUsed :exec
UPDATE verification_codes SET used = 1 WHERE id = ?
`
func (q *Queries) MarkVerificationCodeUsed(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, markVerificationCodeUsed, id)
return err
}
const setEventUser = `-- name: SetEventUser :exec
UPDATE events SET user_id = ? WHERE id = ?
`
type SetEventUserParams struct {
UserID sql.NullInt64
ID int64
}
func (q *Queries) SetEventUser(ctx context.Context, arg SetEventUserParams) error {
_, err := q.db.ExecContext(ctx, setEventUser, arg.UserID, arg.ID)
return err
}
const updateEvent = `-- name: UpdateEvent :exec
UPDATE events SET title = ?, date = ?, time = ?, location = ? WHERE id = ?
`
@@ -445,3 +626,17 @@ func (q *Queries) UpdateSlot(ctx context.Context, arg UpdateSlotParams) error {
)
return err
}
const updateUserName = `-- name: UpdateUserName :exec
UPDATE users SET name = ? WHERE id = ?
`
type UpdateUserNameParams struct {
Name string
ID int64
}
func (q *Queries) UpdateUserName(ctx context.Context, arg UpdateUserNameParams) error {
_, err := q.db.ExecContext(ctx, updateUserName, arg.Name, arg.ID)
return err
}