Support both email and phone login

Auto-detect whether the user entered an email or phone number.
Email sends via Resend, phone sends via Twilio SMS. Users table
has nullable phone and email columns; verification_codes uses a
generic identifier field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 08:53:29 -04:00
parent 471cc3ad8c
commit a0c4b28d1e
7 changed files with 177 additions and 56 deletions
+7 -6
View File
@@ -55,15 +55,16 @@ type Slot struct {
type User struct {
ID int64
Phone string
Phone sql.NullString
Email sql.NullString
Name string
CreatedAt time.Time
}
type VerificationCode struct {
ID int64
Phone string
Code string
ExpiresAt time.Time
Used int64
ID int64
Identifier string
Code string
ExpiresAt time.Time
Used int64
}
+9 -3
View File
@@ -72,9 +72,15 @@ SELECT COUNT(*) FROM rsvps WHERE event_id = ?;
-- name: GetUserByPhone :one
SELECT * FROM users WHERE phone = ?;
-- name: CreateUser :one
-- name: GetUserByEmail :one
SELECT * FROM users WHERE email = ?;
-- name: CreateUserByPhone :one
INSERT INTO users (phone, name) VALUES (?, '') RETURNING *;
-- name: CreateUserByEmail :one
INSERT INTO users (email, name) VALUES (?, '') RETURNING *;
-- name: UpdateUserName :exec
UPDATE users SET name = ? WHERE id = ?;
@@ -91,11 +97,11 @@ DELETE FROM sessions WHERE token = ?;
DELETE FROM sessions WHERE expires_at <= CURRENT_TIMESTAMP;
-- name: CreateVerificationCode :exec
INSERT INTO verification_codes (phone, code, expires_at) VALUES (?, ?, ?);
INSERT INTO verification_codes (identifier, code, expires_at) VALUES (?, ?, ?);
-- name: GetVerificationCode :one
SELECT * FROM verification_codes
WHERE phone = ? AND code = ? AND used = 0 AND expires_at > CURRENT_TIMESTAMP
WHERE identifier = ? AND code = ? AND used = 0 AND expires_at > CURRENT_TIMESTAMP
ORDER BY id DESC LIMIT 1;
-- name: MarkVerificationCodeUsed :exec
+54 -18
View File
@@ -174,16 +174,34 @@ 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
const createUserByEmail = `-- name: CreateUserByEmail :one
INSERT INTO users (email, name) VALUES (?, '') RETURNING id, phone, email, name, created_at
`
func (q *Queries) CreateUser(ctx context.Context, phone string) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, phone)
func (q *Queries) CreateUserByEmail(ctx context.Context, email sql.NullString) (User, error) {
row := q.db.QueryRowContext(ctx, createUserByEmail, email)
var i User
err := row.Scan(
&i.ID,
&i.Phone,
&i.Email,
&i.Name,
&i.CreatedAt,
)
return i, err
}
const createUserByPhone = `-- name: CreateUserByPhone :one
INSERT INTO users (phone, name) VALUES (?, '') RETURNING id, phone, email, name, created_at
`
func (q *Queries) CreateUserByPhone(ctx context.Context, phone sql.NullString) (User, error) {
row := q.db.QueryRowContext(ctx, createUserByPhone, phone)
var i User
err := row.Scan(
&i.ID,
&i.Phone,
&i.Email,
&i.Name,
&i.CreatedAt,
)
@@ -191,17 +209,17 @@ func (q *Queries) CreateUser(ctx context.Context, phone string) (User, error) {
}
const createVerificationCode = `-- name: CreateVerificationCode :exec
INSERT INTO verification_codes (phone, code, expires_at) VALUES (?, ?, ?)
INSERT INTO verification_codes (identifier, code, expires_at) VALUES (?, ?, ?)
`
type CreateVerificationCodeParams struct {
Phone string
Code string
ExpiresAt time.Time
Identifier 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)
_, err := q.db.ExecContext(ctx, createVerificationCode, arg.Identifier, arg.Code, arg.ExpiresAt)
return err
}
@@ -332,16 +350,34 @@ 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 = ?
const getUserByEmail = `-- name: GetUserByEmail :one
SELECT id, phone, email, name, created_at FROM users WHERE email = ?
`
func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error) {
func (q *Queries) GetUserByEmail(ctx context.Context, email sql.NullString) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
var i User
err := row.Scan(
&i.ID,
&i.Phone,
&i.Email,
&i.Name,
&i.CreatedAt,
)
return i, err
}
const getUserByPhone = `-- name: GetUserByPhone :one
SELECT id, phone, email, name, created_at FROM users WHERE phone = ?
`
func (q *Queries) GetUserByPhone(ctx context.Context, phone sql.NullString) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByPhone, phone)
var i User
err := row.Scan(
&i.ID,
&i.Phone,
&i.Email,
&i.Name,
&i.CreatedAt,
)
@@ -349,22 +385,22 @@ func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error
}
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
SELECT id, identifier, code, expires_at, used FROM verification_codes
WHERE identifier = ? AND code = ? AND used = 0 AND expires_at > CURRENT_TIMESTAMP
ORDER BY id DESC LIMIT 1
`
type GetVerificationCodeParams struct {
Phone string
Code string
Identifier string
Code string
}
func (q *Queries) GetVerificationCode(ctx context.Context, arg GetVerificationCodeParams) (VerificationCode, error) {
row := q.db.QueryRowContext(ctx, getVerificationCode, arg.Phone, arg.Code)
row := q.db.QueryRowContext(ctx, getVerificationCode, arg.Identifier, arg.Code)
var i VerificationCode
err := row.Scan(
&i.ID,
&i.Phone,
&i.Identifier,
&i.Code,
&i.ExpiresAt,
&i.Used,