diff --git a/auth.go b/auth.go index 41712d9..0daf866 100644 --- a/auth.go +++ b/auth.go @@ -121,6 +121,11 @@ func (s *Server) handleLoginSubmit(w http.ResponseWriter, r *http.Request) { log.Printf("failed to send verification email to %s: %v", identifier, err) } } else { + s.q.CreateSmsConsent(r.Context(), db.CreateSmsConsentParams{ + Phone: identifier, + IpAddress: r.RemoteAddr, + UserAgent: r.UserAgent(), + }) if err := sendVerificationSMS(identifier, code); err != nil { log.Printf("failed to send verification SMS to %s: %v", identifier, err) } diff --git a/db/models.go b/db/models.go index 63cf420..05a787a 100644 --- a/db/models.go +++ b/db/models.go @@ -54,6 +54,14 @@ type Slot struct { SortOrder int64 } +type SmsConsent struct { + ID int64 + Phone string + IpAddress string + UserAgent string + CreatedAt time.Time +} + type User struct { ID int64 Phone sql.NullString diff --git a/db/queries.sql b/db/queries.sql index 909781c..15f958d 100644 --- a/db/queries.sql +++ b/db/queries.sql @@ -132,3 +132,6 @@ SELECT * FROM rsvps WHERE id = ?; -- name: UpdateRsvp :exec UPDATE rsvps SET name = ?, note = ?, plus_one = ? WHERE id = ?; + +-- name: CreateSmsConsent :exec +INSERT INTO sms_consents (phone, ip_address, user_agent) VALUES (?, ?, ?); diff --git a/db/queries.sql.go b/db/queries.sql.go index fba0456..267236e 100644 --- a/db/queries.sql.go +++ b/db/queries.sql.go @@ -181,6 +181,21 @@ func (q *Queries) CreateSlot(ctx context.Context, arg CreateSlotParams) (Slot, e return i, err } +const createSmsConsent = `-- name: CreateSmsConsent :exec +INSERT INTO sms_consents (phone, ip_address, user_agent) VALUES (?, ?, ?) +` + +type CreateSmsConsentParams struct { + Phone string + IpAddress string + UserAgent string +} + +func (q *Queries) CreateSmsConsent(ctx context.Context, arg CreateSmsConsentParams) error { + _, err := q.db.ExecContext(ctx, createSmsConsent, arg.Phone, arg.IpAddress, arg.UserAgent) + return err +} + const createUserByEmail = `-- name: CreateUserByEmail :one INSERT INTO users (email, name) VALUES (?, '') RETURNING id, phone, email, name, created_at ` diff --git a/migrate.go b/migrate.go index 4607c6a..4a063eb 100644 --- a/migrate.go +++ b/migrate.go @@ -22,6 +22,14 @@ func runMigrations(database *sql.DB) { `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`, + // 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 + )`, } for _, m := range migrations { _, err := database.Exec(m) diff --git a/schema.sql b/schema.sql index 1d62d59..94894cc 100644 --- a/schema.sql +++ b/schema.sql @@ -59,6 +59,14 @@ CREATE TABLE IF NOT EXISTS verification_codes ( used INTEGER NOT NULL DEFAULT 0 ); +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 +); + CREATE INDEX IF NOT EXISTS idx_slots_event ON slots(event_id); CREATE INDEX IF NOT EXISTS idx_claims_slot ON claims(slot_id); CREATE INDEX IF NOT EXISTS idx_rsvps_event ON rsvps(event_id); diff --git a/templates/login.html b/templates/login.html index 9023c26..990cb36 100644 --- a/templates/login.html +++ b/templates/login.html @@ -18,6 +18,9 @@ +

+ By entering a phone number, you consent to receive a one-time verification code via SMS. Msg & data rates may apply. No marketing messages will be sent. +

{{else}}