Wire expected_revision, blob PDF framing, mustChangePassword session rewrite, client_key project create, no-project send disable, and right-rail persistence. Co-authored-by: Cursor <cursoragent@cursor.com>
110 lines
4.1 KiB
TypeScript
110 lines
4.1 KiB
TypeScript
/**
|
||
* 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));
|
||
}
|
||
|
||
/**
|
||
* Top-level must_change_password is the sole authority when present.
|
||
* Never OR-merge with nested user.mustChangePassword — that revived a cleared
|
||
* flag after FirstLoginPasswordModal only patched the top-level field.
|
||
*/
|
||
function resolveMustChangePassword(
|
||
o: Record<string, unknown>,
|
||
nestedUser: { mustChangePassword?: boolean },
|
||
): boolean {
|
||
if (typeof o.must_change_password === 'boolean') return o.must_change_password;
|
||
if (typeof o.mustChangePassword === 'boolean') return o.mustChangePassword as boolean;
|
||
return !!nestedUser.mustChangePassword;
|
||
}
|
||
|
||
/** 映射 /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 = resolveMustChangePassword(o, user);
|
||
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 = resolveMustChangePassword(o, user);
|
||
return {
|
||
user: { ...user, mustChangePassword },
|
||
mustChangePassword,
|
||
env: pickString(o, 'env', 'env') || 'dev',
|
||
capabilities: Array.isArray(o.capabilities) ? (o.capabilities as unknown[]).map(String) : [],
|
||
};
|
||
}
|