Align account UUID usage across backend and UI (#353)
This commit is contained in:
parent
8b7e313521
commit
144d9c28d6
@ -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,
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 ${
|
||||
|
||||
@ -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: '填写基础信息,或选择社交账号直接注册。',
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user