Add secure administrator operations console

Provide TOTP-authenticated, role-controlled user and credit workflows with paginated audit data and SQL-backed statistics so operations can manage growth safely.
This commit is contained in:
Rocky
2026-08-17 15:20:34 +08:00
parent 676bfd2451
commit 1a9c518f96
76 changed files with 14602 additions and 3 deletions
+192
View File
@@ -0,0 +1,192 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { adminApi, ApiError, setCsrfToken } from "../api/client";
describe("adminApi", () => {
afterEach(() => {
vi.unstubAllGlobals();
setCsrfToken();
});
it("会话恢复不依赖 csrfToken,且所有请求都携带同源凭据", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
authenticated: true,
operatorName: "owner",
role: "SUPER_ADMIN",
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetchMock);
const session = await adminApi.session();
expect(fetchMock).toHaveBeenCalledOnce();
expect(session).toEqual({
authenticated: true,
operatorName: "owner",
role: "SUPER_ADMIN",
});
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
credentials: "include",
});
});
it("积分赠送携带 CSRF 与幂等请求头", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({ transactionId: "tx-1", balanceAfter: 120 }),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetchMock);
setCsrfToken("csrf-test");
await adminApi.grantCredits({
userId: "user-1",
amount: 20,
reason: "客服补偿",
idempotencyKey: "grant-1",
});
const request = fetchMock.mock.calls[0]?.[1] as RequestInit;
const headers = request.headers as Headers;
expect(headers.get("X-CSRF-Token")).toBe("csrf-test");
expect(headers.get("Idempotency-Key")).toBe("grant-1");
expect(request.credentials).toBe("include");
});
it("登录请求不依赖已有会话 CSRF", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
operatorName: "owner",
role: "SUPER_ADMIN",
csrfToken: "new-csrf",
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetchMock);
const response = await adminApi.login(
"owner",
"a-strong-password",
"123456",
);
const request = fetchMock.mock.calls[0]?.[1] as RequestInit;
expect((request.headers as Headers).has("X-CSRF-Token")).toBe(false);
expect(response.csrfToken).toBe("new-csrf");
});
it("流水与管理员列表把 cursor 安全传入查询参数", async () => {
const fetchMock = vi.fn().mockImplementation(async () => {
return new Response(JSON.stringify({ items: [] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
vi.stubGlobal("fetch", fetchMock);
await adminApi.ledger("user/with space", "ledger+/=");
await adminApi.operators("operator+/=");
expect(fetchMock.mock.calls[0]?.[0]).toBe(
"/v1/admin/users/user%2Fwith%20space/ledger?cursor=ledger%2B%2F%3D",
);
expect(fetchMock.mock.calls[1]?.[0]).toBe(
"/v1/admin/operators?cursor=operator%2B%2F%3D",
);
});
it("缺少 CSRF 时在发送变更请求前失败", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
await expect(adminApi.logout()).rejects.toMatchObject({
code: "CSRF_TOKEN_MISSING",
status: 403,
});
expect(fetchMock).not.toHaveBeenCalled();
});
it("服务错误转换为稳定 ApiError", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ code: "RATE_LIMITED" }), {
status: 429,
headers: { "Content-Type": "application/json" },
}),
),
);
const promise = adminApi.overview("30d");
await expect(promise).rejects.toBeInstanceOf(ApiError);
await expect(promise).rejects.toMatchObject({
code: "RATE_LIMITED",
message: "操作过于频繁,请稍后再试",
status: 429,
});
});
it("幂等冲突显示可操作的审计提示", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ code: "IDEMPOTENCY_CONFLICT" }), {
status: 409,
headers: { "Content-Type": "application/json" },
}),
),
);
setCsrfToken("csrf-test");
await expect(
adminApi.grantCredits({
userId: "user-1",
amount: 20,
reason: "客服补偿",
idempotencyKey: "grant-conflict-1",
}),
).rejects.toMatchObject({
code: "IDEMPOTENCY_CONFLICT",
message: "该赠送请求与已有记录冲突,请核对审计日志",
status: 409,
});
});
it("创建管理员携带 CSRF 并返回一次性 TOTP 配置", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
operatorId: "operator-1",
totpSecret: "JBSWY3DPEHPK3PXP",
otpauthUri: "otpauth://totp/OSG:operator",
}),
{ status: 201, headers: { "Content-Type": "application/json" } },
),
);
vi.stubGlobal("fetch", fetchMock);
setCsrfToken("csrf-test");
const result = await adminApi.createOperator({
username: "support",
password: "a-strong-password",
role: "SUPPORT",
});
const request = fetchMock.mock.calls[0]?.[1] as RequestInit;
expect((request.headers as Headers).get("X-CSRF-Token")).toBe("csrf-test");
expect(request.body).toBe(
JSON.stringify({
username: "support",
password: "a-strong-password",
role: "SUPPORT",
}),
);
expect(result.totpSecret).toBe("JBSWY3DPEHPK3PXP");
});
});