- config 新增 auth_login 段(login_config.py): - username_login.require_password 开启后,用户名直登必须带 ?password=,口令留空默认 123ewq - token_login 用上游 token 调 getTokenInfo 换用户名再登录,用户名不在库自动注册;含 verify_ssl/ca_cert_path 处理 HTTPS 自签证书 - auth.py:口令门、/login/token 端点、共享 _get_or_create_user_by_username 与 _fetch_username_from_token;登录类报错文案改中文 - auth_middleware:/login/token 加入免鉴权白名单 - 前端 api.ts/LoginPage:透传 ?password=、?authToken=;修复错误体为对象时显示 [object Object] 的问题 - 文档:backend/CLAUDE.md + docs/AUTH_LOGIN.md 接口说明 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import { apiFetch } from "@/core/api/fetch-client";
|
|
import { getBackendBaseURL } from "@/core/config";
|
|
|
|
import type { LoginByUsernameResponse } from "./index";
|
|
|
|
export interface UserListItem {
|
|
id: string;
|
|
email: string;
|
|
system_role: "admin" | "user";
|
|
skill_count: number;
|
|
agent_count: number;
|
|
}
|
|
|
|
export async function listAllUsers(): Promise<UserListItem[]> {
|
|
const res = await apiFetch(`${getBackendBaseURL()}/api/v1/auth/users`);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
return res.json() as Promise<UserListItem[]>;
|
|
}
|
|
|
|
// Normalize a FastAPI error body into a human-readable string. `detail` may be
|
|
// a plain string, an object like AuthErrorResponse ({ code, message }), or an
|
|
// array of validation errors — anything but a string would otherwise render as
|
|
// the dreaded "[object Object]".
|
|
function extractErrorMessage(body: unknown, fallback = "Login failed."): string {
|
|
const detail = (body as { detail?: unknown } | null)?.detail ?? body;
|
|
if (typeof detail === "string") return detail;
|
|
if (Array.isArray(detail)) {
|
|
const msg = detail
|
|
.map((d) => (d && typeof d === "object" ? (d as { msg?: string }).msg : String(d)))
|
|
.filter(Boolean)
|
|
.join("; ");
|
|
return msg || fallback;
|
|
}
|
|
if (detail && typeof detail === "object") {
|
|
const obj = detail as { message?: string; msg?: string };
|
|
return obj.message ?? obj.msg ?? fallback;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
export async function loginByUsername(
|
|
username: string,
|
|
password?: string | null,
|
|
): Promise<LoginByUsernameResponse> {
|
|
// When the username-login password gate is enabled server-side, the shared
|
|
// secret must be passed as ?password=. Omitted when no password is provided
|
|
// so the passwordless flow keeps working unchanged.
|
|
const base = `${getBackendBaseURL()}/api/v1/auth/login/username`;
|
|
const url =
|
|
password != null && password !== ""
|
|
? `${base}?password=${encodeURIComponent(password)}`
|
|
: base;
|
|
const response = await apiFetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ username }),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => null);
|
|
throw new Error(extractErrorMessage(error));
|
|
}
|
|
return response.json() as Promise<LoginByUsernameResponse>;
|
|
}
|
|
|
|
export async function loginByToken(
|
|
token: string,
|
|
): Promise<LoginByUsernameResponse> {
|
|
// Token-exchange login: the gateway resolves the upstream token to a username
|
|
// and signs the matching local account in.
|
|
const response = await apiFetch(
|
|
`${getBackendBaseURL()}/api/v1/auth/login/token?token=${encodeURIComponent(token)}`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => null);
|
|
throw new Error(extractErrorMessage(error));
|
|
}
|
|
return response.json() as Promise<LoginByUsernameResponse>;
|
|
}
|
|
|