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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user