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
}
@@ -21,6 +21,27 @@ type AiPlatform struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type AiPointUsageLog struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
OperatorID pgtype.Int8 `json:"operator_id"`
QuotaReservationID pgtype.Int8 `json:"quota_reservation_id"`
UsageType string `json:"usage_type"`
ResourceType pgtype.Text `json:"resource_type"`
ResourceID pgtype.Int8 `json:"resource_id"`
ResourceUid pgtype.Text `json:"resource_uid"`
RequestChars int32 `json:"request_chars"`
BaseChars int32 `json:"base_chars"`
Points int32 `json:"points"`
Status string `json:"status"`
Model pgtype.Text `json:"model"`
MetadataJson []byte `json:"metadata_json"`
ErrorMessage pgtype.Text `json:"error_message"`
CompletedAt pgtype.Timestamptz `json:"completed_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type Article struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
@@ -149,6 +170,15 @@ type DesktopClient struct {
RevokedAt pgtype.Timestamptz `json:"revoked_at"`
}
type DesktopClientPrimaryLease struct {
TenantID int64 `json:"tenant_id"`
WorkspaceID int64 `json:"workspace_id"`
PrimaryClientID pgtype.UUID `json:"primary_client_id"`
LeaseExpiresAt pgtype.Timestamptz `json:"lease_expires_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type DesktopPublishJob struct {
ID int64 `json:"id"`
DesktopID pgtype.UUID `json:"desktop_id"`
@@ -708,10 +738,10 @@ type TenantQuotaLedger struct {
type User 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"`
@@ -6,6 +6,8 @@ package generated
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
type Querier interface {
@@ -90,8 +92,9 @@ type Querier interface {
GetTemplateByID(ctx context.Context, arg GetTemplateByIDParams) (GetTemplateByIDRow, error)
GetTenantMembership(ctx context.Context, userID int64) (GetTenantMembershipRow, error)
GetTenantMembershipByTenantAndUser(ctx context.Context, arg GetTenantMembershipByTenantAndUserParams) (GetTenantMembershipByTenantAndUserRow, error)
GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error)
GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUserByEmailRow, error)
GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error)
GetUserByLoginIdentifier(ctx context.Context, loginIdentifier pgtype.Text) (GetUserByLoginIdentifierRow, error)
HeartbeatDesktopClient(ctx context.Context, arg HeartbeatDesktopClientParams) (DesktopClient, error)
InsertImageAsset(ctx context.Context, arg InsertImageAssetParams) (int64, error)
InsertQuotaLedger(ctx context.Context, arg InsertQuotaLedgerParams) (int64, error)