Add SMS consent logging for Twilio compliance

Log phone, IP, and user agent to sms_consents table before sending
verification SMS. Add disclosure text to login form.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 09:46:43 -04:00
parent 3afef25b4c
commit 0719269bce
7 changed files with 50 additions and 0 deletions
+5
View File
@@ -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) log.Printf("failed to send verification email to %s: %v", identifier, err)
} }
} else { } else {
s.q.CreateSmsConsent(r.Context(), db.CreateSmsConsentParams{
Phone: identifier,
IpAddress: r.RemoteAddr,
UserAgent: r.UserAgent(),
})
if err := sendVerificationSMS(identifier, code); err != nil { if err := sendVerificationSMS(identifier, code); err != nil {
log.Printf("failed to send verification SMS to %s: %v", identifier, err) log.Printf("failed to send verification SMS to %s: %v", identifier, err)
} }
+8
View File
@@ -54,6 +54,14 @@ type Slot struct {
SortOrder int64 SortOrder int64
} }
type SmsConsent struct {
ID int64
Phone string
IpAddress string
UserAgent string
CreatedAt time.Time
}
type User struct { type User struct {
ID int64 ID int64
Phone sql.NullString Phone sql.NullString
+3
View File
@@ -132,3 +132,6 @@ SELECT * FROM rsvps WHERE id = ?;
-- name: UpdateRsvp :exec -- name: UpdateRsvp :exec
UPDATE rsvps SET name = ?, note = ?, plus_one = ? WHERE id = ?; UPDATE rsvps SET name = ?, note = ?, plus_one = ? WHERE id = ?;
-- name: CreateSmsConsent :exec
INSERT INTO sms_consents (phone, ip_address, user_agent) VALUES (?, ?, ?);
+15
View File
@@ -181,6 +181,21 @@ func (q *Queries) CreateSlot(ctx context.Context, arg CreateSlotParams) (Slot, e
return i, err 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 const createUserByEmail = `-- name: CreateUserByEmail :one
INSERT INTO users (email, name) VALUES (?, '') RETURNING id, phone, email, name, created_at INSERT INTO users (email, name) VALUES (?, '') RETURNING id, phone, email, name, created_at
` `
+8
View File
@@ -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)`, `DELETE FROM rsvps WHERE id NOT IN (SELECT MIN(id) FROM rsvps GROUP BY event_id, name COLLATE NOCASE)`,
// Plus-one tracking for RSVPs. // Plus-one tracking for RSVPs.
`ALTER TABLE rsvps ADD COLUMN plus_one INTEGER NOT NULL DEFAULT 0`, `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 { for _, m := range migrations {
_, err := database.Exec(m) _, err := database.Exec(m)
+8
View File
@@ -59,6 +59,14 @@ CREATE TABLE IF NOT EXISTS verification_codes (
used INTEGER NOT NULL DEFAULT 0 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_slots_event ON slots(event_id);
CREATE INDEX IF NOT EXISTS idx_claims_slot ON claims(slot_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); CREATE INDEX IF NOT EXISTS idx_rsvps_event ON rsvps(event_id);
+3
View File
@@ -18,6 +18,9 @@
<label>Email or phone</label> <label>Email or phone</label>
<input type="text" name="identifier" placeholder="you@example.com or (555) 123-4567" required autofocus> <input type="text" name="identifier" placeholder="you@example.com or (555) 123-4567" required autofocus>
</div> </div>
<p style="font-family:'DM Mono',monospace;font-size:0.7rem;color:#777;margin-top:12px;line-height:1.5;">
By entering a phone number, you consent to receive a one-time verification code via SMS. Msg &amp; data rates may apply. No marketing messages will be sent.
</p>
<button class="btn-submit" type="submit">Send code &#8599;</button> <button class="btn-submit" type="submit">Send code &#8599;</button>
</form> </form>
{{else}} {{else}}