Align account UUID usage across backend and UI (#353)

This commit is contained in:
shenlan 2025-10-01 17:42:50 +08:00 committed by GitHub
parent 8b7e313521
commit 144d9c28d6
8 changed files with 74 additions and 19 deletions

View File

@ -276,8 +276,10 @@ func (h *handler) removeSession(token string) {
}
func sanitizeUser(user *store.User) gin.H {
identifier := strings.TrimSpace(user.ID)
return gin.H{
"id": user.ID,
"id": identifier,
"uuid": identifier,
"name": user.Name,
"username": user.Name,
"email": user.Email,

View File

@ -54,6 +54,14 @@ func TestRegisterEndpoint(t *testing.T) {
t.Fatalf("expected email %q, got %#v", payload["email"], response.User["email"])
}
if id, ok := response.User["id"].(string); !ok || id == "" {
t.Fatalf("expected user id in response, got %#v", response.User["id"])
} else {
if uuid, ok := response.User["uuid"].(string); !ok || uuid != id {
t.Fatalf("expected uuid to match id, got id=%q uuid=%#v", id, response.User["uuid"])
}
}
if response.Message == "" {
t.Fatalf("expected success message in response")
}
@ -116,6 +124,14 @@ func TestLoginEndpoint(t *testing.T) {
t.Fatalf("failed to decode login response: %v", err)
}
if id, ok := loginResponse.User["id"].(string); !ok || id == "" {
t.Fatalf("expected user id in login response, got %#v", loginResponse.User["id"])
} else {
if uuid, ok := loginResponse.User["uuid"].(string); !ok || uuid != id {
t.Fatalf("expected login uuid to match id, got id=%q uuid=%#v", id, loginResponse.User["uuid"])
}
}
if loginResponse.Message == "" {
t.Fatalf("expected login success message")
}

View File

@ -99,8 +99,8 @@ func (s *postgresStore) CreateUser(ctx context.Context, user *User) error {
}
query := `INSERT INTO users (username, email, password)
VALUES ($1, $2, $3)
RETURNING id, coalesce(created_at, now())`
VALUES ($1, $2, $3)
RETURNING uuid, coalesce(created_at, now())`
var idValue any
var createdAt time.Time
@ -141,8 +141,8 @@ func (s *postgresStore) GetUserByEmail(ctx context.Context, email string) (*User
return nil, ErrUserNotFound
}
query := `SELECT id, username, email, password, coalesce(created_at, now())
FROM users WHERE lower(email) = $1 LIMIT 1`
query := `SELECT uuid, username, email, password, coalesce(created_at, now())
FROM users WHERE lower(email) = $1 LIMIT 1`
row := s.db.QueryRowContext(ctx, query, normalized)
return scanUser(row)
@ -154,16 +154,16 @@ func (s *postgresStore) GetUserByName(ctx context.Context, name string) (*User,
return nil, ErrUserNotFound
}
query := `SELECT id, username, email, password, coalesce(created_at, now())
FROM users WHERE lower(username) = lower($1) LIMIT 1`
query := `SELECT uuid, username, email, password, coalesce(created_at, now())
FROM users WHERE lower(username) = lower($1) LIMIT 1`
row := s.db.QueryRowContext(ctx, query, normalized)
return scanUser(row)
}
func (s *postgresStore) GetUserByID(ctx context.Context, id string) (*User, error) {
query := `SELECT id, username, email, password, coalesce(created_at, now())
FROM users WHERE id = $1`
query := `SELECT uuid, username, email, password, coalesce(created_at, now())
FROM users WHERE uuid = $1`
row := s.db.QueryRowContext(ctx, query, id)
return scanUser(row)
@ -244,6 +244,8 @@ func formatIdentifier(value any) (string, error) {
return v, nil
case []byte:
return string(v), nil
case fmt.Stringer:
return v.String(), nil
case int64:
return strconv.FormatInt(v, 10), nil
case int32:

View File

@ -7,7 +7,8 @@ const ACCOUNT_SERVICE_URL = getAccountServiceBaseUrl()
const SESSION_COOKIE_NAME = 'account_session'
type AccountUser = {
id: string
id?: string
uuid?: string
name?: string
username?: string
email: string
@ -44,7 +45,19 @@ export async function GET(request: NextRequest) {
return res
}
return NextResponse.json({ user: data.user as AccountUser })
const rawUser = data.user as AccountUser
const identifier =
typeof rawUser.uuid === 'string' && rawUser.uuid.trim().length > 0
? rawUser.uuid.trim()
: typeof rawUser.id === 'string'
? rawUser.id.trim()
: undefined
const normalizedUser = identifier
? { ...rawUser, id: identifier, uuid: identifier }
: rawUser
return NextResponse.json({ user: normalizedUser })
}
export async function DELETE(request: NextRequest) {

View File

@ -33,21 +33,22 @@ export default function UserOverview() {
const [copied, setCopied] = useState(false)
const displayName = useMemo(() => resolveDisplayName(user), [user])
const uuid = user?.id ?? '—'
const uuid = user?.uuid ?? user?.id ?? '—'
const username = user?.username ?? '—'
const email = user?.email ?? '—'
const handleCopy = useCallback(async () => {
if (!user?.id) {
const identifier = user?.uuid ?? user?.id
if (!identifier) {
return
}
try {
if (typeof navigator !== 'undefined' && navigator.clipboard && 'writeText' in navigator.clipboard) {
await navigator.clipboard.writeText(user.id)
await navigator.clipboard.writeText(identifier)
} else {
const textarea = document.createElement('textarea')
textarea.value = user.id
textarea.value = identifier
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
@ -61,7 +62,7 @@ export default function UserOverview() {
} catch (error) {
console.warn('Failed to copy UUID', error)
}
}, [user?.id])
}, [user?.id, user?.uuid])
return (
<div className="space-y-6">

View File

@ -201,6 +201,11 @@ export default function RegisterContent() {
<h2 className="text-2xl font-semibold text-gray-900 sm:text-3xl">{t.form.title}</h2>
<p className="text-sm text-gray-600">{t.form.subtitle}</p>
</div>
{t.uuidNote ? (
<div className="rounded-2xl border border-dashed border-purple-200 bg-purple-50/80 p-4 text-sm text-purple-700">
{t.uuidNote}
</div>
) : null}
{alert ? (
<div
className={`rounded-xl border px-4 py-3 text-sm font-medium ${

View File

@ -113,6 +113,7 @@ type AuthRegisterTranslation = {
subtitle: string
highlights: AuthHighlight[]
bottomNote: string
uuidNote: string
form: {
title: string
subtitle: string
@ -402,6 +403,8 @@ export const translations: Record<'en' | 'zh', Translation> = {
},
],
bottomNote: 'No credit card required. Premium capabilities are available with a 14-day trial.',
uuidNote:
'Every account receives a globally unique UUID. After registration, sign in to the user center to view and copy it for future integrations.',
form: {
title: 'Create your account',
subtitle: 'Share a few details or continue with a social login.',
@ -655,6 +658,7 @@ export const translations: Record<'en' | 'zh', Translation> = {
},
],
bottomNote: '无需信用卡,免费体验版可试用高级功能 14 天。',
uuidNote: '注册完成后,系统会为你分配一个全局唯一的 UUID可在用户中心查看并复制用于后续服务对接。',
form: {
title: '创建账号',
subtitle: '填写基础信息,或选择社交账号直接注册。',

View File

@ -12,6 +12,7 @@ import { create } from 'zustand'
type User = {
id: string
uuid: string
email: string
name?: string
username: string
@ -54,7 +55,7 @@ async function fetchSessionUser(): Promise<User | null> {
}
const payload = (await response.json()) as {
user?: { id: string; email: string; name?: string; username?: string } | null
user?: { id?: string; uuid?: string; email: string; name?: string; username?: string } | null
}
const sessionUser = payload?.user
@ -62,13 +63,24 @@ async function fetchSessionUser(): Promise<User | null> {
return null
}
const { id, email, name, username } = sessionUser
const { id, uuid, email, name, username } = sessionUser
const identifier =
typeof uuid === 'string' && uuid.trim().length > 0
? uuid.trim()
: typeof id === 'string'
? id.trim()
: ''
if (!identifier) {
return null
}
const normalizedName = typeof name === 'string' && name.trim().length > 0 ? name.trim() : undefined
const normalizedUsername =
typeof username === 'string' && username.trim().length > 0 ? username.trim() : normalizedName
return {
id,
id: identifier,
uuid: identifier,
email,
name: normalizedName,
username: normalizedUsername ?? email,