CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
补齐普通用户自助账号、中文设置与快捷键提示,修复大型扩展安装和移动端弹窗,并按服务器能力限制不兼容扩展。 Co-Authored-By: Codex <noreply@anthropic.com>
140 lines
3.9 KiB
TypeScript
140 lines
3.9 KiB
TypeScript
import type { RuntimeAvailability } from "./runtime_availability.ts";
|
||
import type { FieldError, UserInfo } from "./types.ts";
|
||
|
||
export async function api(
|
||
method: string,
|
||
path: string,
|
||
body?: unknown,
|
||
): Promise<any> {
|
||
const resp = await fetch(path, {
|
||
method,
|
||
headers: body ? { "Content-Type": "application/json" } : {},
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
});
|
||
// Only 401 — "no valid session" — means the browser should go log in.
|
||
// A 403 is "signed in, but not permitted": it falls through to the normal
|
||
// error path below so the caller renders a message. Redirecting on 403
|
||
// would loop, since the login screen bounces a valid session straight back.
|
||
if (resp.status === 401) throw { unauthorized: true };
|
||
const json = await resp.json().catch(() => ({}));
|
||
if (!resp.ok) {
|
||
const errors = json.errors ?? [
|
||
{
|
||
field: "",
|
||
message:
|
||
resp.status === 403
|
||
? "没有权限执行此操作"
|
||
: `请求失败(HTTP ${resp.status})`,
|
||
},
|
||
];
|
||
if (resp.status === 404) errors.notFound = true;
|
||
throw errors;
|
||
}
|
||
return json;
|
||
}
|
||
|
||
/**
|
||
* Admin-only endpoints live under `api/admin/`. Split from `api()` so a
|
||
* mistaken call to an admin route from a non-admin screen is visible at the
|
||
* call site rather than buried in a path string.
|
||
*/
|
||
export function adminApi(
|
||
method: string,
|
||
path: string,
|
||
body?: unknown,
|
||
): Promise<any> {
|
||
return api(method, `api/admin/${path}`, body);
|
||
}
|
||
|
||
export function formatApiError(e: unknown): string {
|
||
if (Array.isArray(e)) {
|
||
return e
|
||
.map((fe: FieldError) =>
|
||
fe.field ? `${fe.field}: ${fe.message}` : fe.message,
|
||
)
|
||
.join(", ");
|
||
}
|
||
return "请求失败";
|
||
}
|
||
|
||
/** Server-level facts for admin screens. See `RuntimeAvailability`. */
|
||
export function getServerInfo(): Promise<{ runtimeApi: RuntimeAvailability }> {
|
||
return adminApi("GET", "server-info");
|
||
}
|
||
|
||
// --- User management (backed by users.json via the admin API) ------------
|
||
|
||
export function listUsers(): Promise<Record<string, UserInfo>> {
|
||
return adminApi("GET", "users");
|
||
}
|
||
|
||
export function getUser(name: string): Promise<UserInfo> {
|
||
return adminApi("GET", `users/${encodeURIComponent(name)}`);
|
||
}
|
||
|
||
export function getSession(): Promise<{ username: string; admin: boolean }> {
|
||
return api("GET", "api/session");
|
||
}
|
||
|
||
export function getOwnAccount(): Promise<{
|
||
username: string;
|
||
user: UserInfo;
|
||
}> {
|
||
return api("GET", "api/account");
|
||
}
|
||
|
||
export function changeOwnPassword(
|
||
currentPassword: string,
|
||
newPassword: string,
|
||
): Promise<void> {
|
||
return api("POST", "api/account/password", {
|
||
currentPassword,
|
||
newPassword,
|
||
});
|
||
}
|
||
|
||
export async function createOwnToken(name: string): Promise<string> {
|
||
const result = await api("POST", "api/account/tokens", { name });
|
||
return result.token;
|
||
}
|
||
|
||
export function deleteOwnToken(name: string): Promise<void> {
|
||
return api("DELETE", `api/account/tokens/${encodeURIComponent(name)}`);
|
||
}
|
||
|
||
export function createUser(
|
||
username: string,
|
||
password: string,
|
||
admin: boolean,
|
||
): Promise<void> {
|
||
return adminApi("POST", "users", { username, password, admin });
|
||
}
|
||
|
||
export function deleteUser(name: string): Promise<void> {
|
||
return adminApi("DELETE", `users/${encodeURIComponent(name)}`);
|
||
}
|
||
|
||
export function setUserPassword(name: string, password: string): Promise<void> {
|
||
return adminApi("POST", `users/${encodeURIComponent(name)}/password`, {
|
||
password,
|
||
});
|
||
}
|
||
|
||
export function setUserAdmin(name: string, admin: boolean): Promise<void> {
|
||
return adminApi("PUT", `users/${encodeURIComponent(name)}`, { admin });
|
||
}
|
||
|
||
export async function createToken(user: string, name: string): Promise<string> {
|
||
const r = await adminApi("POST", `users/${encodeURIComponent(user)}/tokens`, {
|
||
name,
|
||
});
|
||
return r.token;
|
||
}
|
||
|
||
export function deleteToken(user: string, name: string): Promise<void> {
|
||
return adminApi(
|
||
"DELETE",
|
||
`users/${encodeURIComponent(user)}/tokens/${encodeURIComponent(name)}`,
|
||
);
|
||
}
|