feat(server/auth): switch user identity to phone with email optional

Phone becomes the required, unique login identifier; email and name turn
optional. Login now accepts either via a single identifier field, refresh
re-checks tenant membership, and the dev seed provides phone numbers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 00:01:36 +08:00
parent a6b17bae62
commit ee21f512a9
11 changed files with 225 additions and 40 deletions
@@ -101,17 +101,17 @@ WHERE email = $1 AND deleted_at IS NULL
type GetUserByEmailRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
Email pgtype.Text `json:"email"`
Phone string `json:"phone"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
Name pgtype.Text `json:"name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) {
func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUserByEmailRow, error) {
row := q.db.QueryRow(ctx, getUserByEmail, email)
var i GetUserByEmailRow
err := row.Scan(
@@ -136,10 +136,10 @@ WHERE id = $1 AND deleted_at IS NULL
type GetUserByIDRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
Email pgtype.Text `json:"email"`
Phone string `json:"phone"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
Name pgtype.Text `json:"name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
@@ -162,3 +162,39 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, er
)
return i, err
}
const getUserByLoginIdentifier = `-- name: GetUserByLoginIdentifier :one
SELECT id, email, phone, password_hash, name, avatar_url, status, created_at, updated_at
FROM users
WHERE (email = $1 OR phone = $1)
AND deleted_at IS NULL
`
type GetUserByLoginIdentifierRow struct {
ID int64 `json:"id"`
Email pgtype.Text `json:"email"`
Phone string `json:"phone"`
PasswordHash string `json:"password_hash"`
Name pgtype.Text `json:"name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetUserByLoginIdentifier(ctx context.Context, loginIdentifier pgtype.Text) (GetUserByLoginIdentifierRow, error) {
row := q.db.QueryRow(ctx, getUserByLoginIdentifier, loginIdentifier)
var i GetUserByLoginIdentifierRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Phone,
&i.PasswordHash,
&i.Name,
&i.AvatarUrl,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}