Files
Agent-System/web/src/api/authUserMap.ts
T
weichenandCursor cf28e53308 fix(web): map Go UserAccount snake_case fields at auth API boundary
Normalize role_id/employee_id/must_change_password/created_at into
camelCase for login, me, and admin user mutations so admin UI gates work.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 11:37:03 +00:00

98 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Go UserAccount JSON 使用 snake_case(role_id / employee_id / must_change_password / created_at…)。
* 前端 UserAccount / UserProfile 使用 camelCase。在 API 边界统一映射,兼容偶发旧驼峰字段。
*/
import type { AccountStatus, LoginResult, MeResult, UserAccount } from '../types/auth';
function pickString(raw: Record<string, unknown>, snake: string, camel: string): string {
const a = raw[snake];
const b = raw[camel];
if (typeof a === 'string' && a.length > 0) return a;
if (typeof b === 'string') return b;
if (typeof a === 'string') return a;
return '';
}
function pickBool(raw: Record<string, unknown>, snake: string, camel: string): boolean {
if (typeof raw[snake] === 'boolean') return raw[snake] as boolean;
if (typeof raw[camel] === 'boolean') return raw[camel] as boolean;
return false;
}
function pickNumber(raw: Record<string, unknown>, key: string): number {
const v = raw[key];
if (typeof v === 'number' && Number.isFinite(v)) return v;
if (typeof v === 'string' && v.trim() !== '' && !Number.isNaN(Number(v))) return Number(v);
return 0;
}
/** 将服务端(或混合)用户对象规范为前端 camelCase UserAccount。 */
export function mapUserAccount(raw: unknown): UserAccount {
if (!raw || typeof raw !== 'object') {
throw new Error('无效的用户账户响应');
}
const o = raw as Record<string, unknown>;
const statusRaw = pickString(o, 'status', 'status') || 'active';
const status = statusRaw as AccountStatus;
return {
id: pickNumber(o, 'id'),
username: pickString(o, 'username', 'username'),
name: pickString(o, 'name', 'name'),
status,
mustChangePassword: pickBool(o, 'must_change_password', 'mustChangePassword'),
roleId: pickString(o, 'role_id', 'roleId'),
position: pickString(o, 'position', 'position'),
employeeId: pickString(o, 'employee_id', 'employeeId'),
department: pickString(o, 'department', 'department'),
email: pickString(o, 'email', 'email') || undefined,
avatar: pickString(o, 'avatar', 'avatar') || undefined,
createdAt: pickString(o, 'created_at', 'createdAt') || undefined,
updatedAt: pickString(o, 'updated_at', 'updatedAt') || undefined,
};
}
export function mapUserAccountList(raw: unknown): UserAccount[] {
if (!Array.isArray(raw)) return [];
return raw.map((item) => mapUserAccount(item));
}
/** 映射 /auth/login 响应(token + user + must_change_password + env)。 */
export function mapLoginResult(raw: unknown): LoginResult {
if (!raw || typeof raw !== 'object') {
throw new Error('无效的登录响应');
}
const o = raw as Record<string, unknown>;
const user = mapUserAccount(o.user);
const mustChangePassword =
pickBool(o, 'must_change_password', 'mustChangePassword') || !!user.mustChangePassword;
const capabilities = Array.isArray(o.capabilities)
? (o.capabilities as unknown[]).map(String)
: undefined;
return {
token: pickString(o, 'token', 'token'),
user: { ...user, mustChangePassword },
mustChangePassword,
env: pickString(o, 'env', 'env') || 'dev',
capabilities,
};
}
/** 映射 /auth/me 响应。 */
export function mapMeResult(raw: unknown): MeResult {
if (!raw || typeof raw !== 'object') {
throw new Error('无效的会话响应');
}
const o = raw as Record<string, unknown>;
const user = mapUserAccount(o.user);
const mustChangePassword =
pickBool(o, 'must_change_password', 'mustChangePassword') || !!user.mustChangePassword;
return {
user: { ...user, mustChangePassword },
mustChangePassword,
env: pickString(o, 'env', 'env') || 'dev',
capabilities: Array.isArray(o.capabilities) ? (o.capabilities as unknown[]).map(String) : [],
};
}