d005080aca
Hosts can optionally cap total attendance (RSVPs + plus-ones) when creating an event. Capped events require guests to log in via the existing email code flow; RSVPs are tied to accounts and deduped per user. The event page shows spots left and replaces the form with a full notice when the cap is reached; the server rejects over-cap RSVPs and edits with 409 as a race backstop. Also threads a safeNext-validated ?next= param through the login and name-setup flow so guests land back on the event after signing in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
103 lines
3.6 KiB
Go
103 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"log"
|
|
"strings"
|
|
)
|
|
|
|
func runMigrations(database *sql.DB) {
|
|
migrations := []string{
|
|
`ALTER TABLE events ADD COLUMN description TEXT DEFAULT ''`,
|
|
`ALTER TABLE events ADD COLUMN user_id INTEGER REFERENCES users(id)`,
|
|
// Verification codes use a generic identifier column.
|
|
`ALTER TABLE verification_codes ADD COLUMN identifier TEXT NOT NULL DEFAULT ''`,
|
|
// Indexes for auth tables (created here so they run after column migrations).
|
|
`CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_verification_codes_identifier ON verification_codes(identifier)`,
|
|
// Dedupe RSVPs: keep earliest per (event_id, name).
|
|
`DELETE FROM rsvps WHERE id NOT IN (SELECT MIN(id) FROM rsvps GROUP BY event_id, name COLLATE NOCASE)`,
|
|
// Plus-one tracking for RSVPs.
|
|
`ALTER TABLE rsvps ADD COLUMN plus_one INTEGER NOT NULL DEFAULT 0`,
|
|
// Drop legacy phone/email columns from verification_codes. They were
|
|
// NOT NULL with no default, so inserts using only identifier failed.
|
|
`DROP INDEX IF EXISTS idx_verification_codes_phone`,
|
|
`DROP INDEX IF EXISTS idx_verification_codes_email`,
|
|
`ALTER TABLE verification_codes DROP COLUMN phone`,
|
|
`ALTER TABLE verification_codes DROP COLUMN email`,
|
|
// SMS consent logging for Twilio compliance.
|
|
`CREATE TABLE IF NOT EXISTS sms_consents (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
phone TEXT NOT NULL,
|
|
ip_address TEXT NOT NULL DEFAULT '',
|
|
user_agent TEXT NOT NULL DEFAULT '',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)`,
|
|
// Attendee cap (0 = uncapped); capped events require login to RSVP.
|
|
`ALTER TABLE events ADD COLUMN attendee_cap INTEGER NOT NULL DEFAULT 0`,
|
|
`ALTER TABLE rsvps ADD COLUMN user_id INTEGER REFERENCES users(id)`,
|
|
}
|
|
for _, m := range migrations {
|
|
_, err := database.Exec(m)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "duplicate column name") ||
|
|
strings.Contains(err.Error(), "no such column") {
|
|
continue
|
|
}
|
|
log.Printf("migration warning: %v", err)
|
|
}
|
|
}
|
|
|
|
rebuildLegacyUsersTable(database)
|
|
}
|
|
|
|
// rebuildLegacyUsersTable migrates users tables from the phone-auth era
|
|
// (phone NOT NULL, no email column). SQLite can't add a UNIQUE column or
|
|
// drop NOT NULL via ALTER TABLE, so the table has to be rebuilt. Detected
|
|
// by the missing email column.
|
|
func rebuildLegacyUsersTable(database *sql.DB) {
|
|
var hasEmail int
|
|
err := database.QueryRow(
|
|
`SELECT COUNT(*) FROM pragma_table_info('users') WHERE name = 'email'`,
|
|
).Scan(&hasEmail)
|
|
if err != nil || hasEmail > 0 {
|
|
return
|
|
}
|
|
|
|
// Disable FK enforcement so DROP TABLE users doesn't cascade-delete
|
|
// sessions. main.go sets MaxOpenConns(1), so these pragmas apply to
|
|
// the same connection the transaction runs on.
|
|
database.Exec(`PRAGMA foreign_keys=OFF`)
|
|
defer database.Exec(`PRAGMA foreign_keys=ON`)
|
|
|
|
tx, err := database.Begin()
|
|
if err != nil {
|
|
log.Printf("users rebuild: %v", err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
stmts := []string{
|
|
`CREATE TABLE users_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
phone TEXT UNIQUE,
|
|
email TEXT UNIQUE,
|
|
name TEXT NOT NULL DEFAULT '',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)`,
|
|
`INSERT INTO users_new (id, phone, name, created_at)
|
|
SELECT id, phone, name, created_at FROM users`,
|
|
`DROP TABLE users`,
|
|
`ALTER TABLE users_new RENAME TO users`,
|
|
}
|
|
for _, stmt := range stmts {
|
|
if _, err := tx.Exec(stmt); err != nil {
|
|
log.Printf("users rebuild: %v", err)
|
|
return
|
|
}
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
log.Printf("users rebuild: %v", err)
|
|
}
|
|
}
|