Files
deerflow-code/frontend-web/src/core/auth/api.ts
T
吉鑫升andClaude Opus 4.8 c484481964 登录鉴权:新增用户名口令门 + token 换登录两种模式
- 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>
2026-06-02 17:39:56 +08:00

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>;
}