130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { parseSessionCookieValue } from "@/lib/auth/session";
|
|
import {
|
|
createPlatformOrganization,
|
|
createPlatformUser,
|
|
updatePlatformUser
|
|
} from "@/lib/server/account-store";
|
|
import { resetLocalAuthRateLimitForTests } from "@/lib/server/auth/local";
|
|
import type { PlatformUserRecord } from "@/lib/types";
|
|
import { POST } from "@/app/api/auth/password/route";
|
|
|
|
const SESSION_SECRET = "test-platform-session-secret-with-enough-entropy";
|
|
let runtimeDir = "";
|
|
let ordinaryUser: PlatformUserRecord;
|
|
|
|
describe("platform password auth route", () => {
|
|
beforeEach(async () => {
|
|
runtimeDir = await mkdtemp(join(tmpdir(), "zhinian-auth-"));
|
|
vi.stubEnv("ZHINIAN_DATA_DIR", runtimeDir);
|
|
vi.stubEnv("ZHINIAN_AUTH_REQUIRED", "1");
|
|
vi.stubEnv("ZHINIAN_AUTH_SESSION_SECRET", SESSION_SECRET);
|
|
vi.stubEnv("ZHINIAN_AUTH_DISABLED", "");
|
|
vi.stubEnv("ZHINIAN_DATA_BACKEND", "local");
|
|
resetLocalAuthRateLimitForTests();
|
|
|
|
const organization = await createPlatformOrganization("测试组织");
|
|
ordinaryUser = await createPlatformUser({
|
|
phone: "13800138000",
|
|
displayName: "测试用户",
|
|
password: "TestPass123",
|
|
role: "user",
|
|
organizationId: organization.id
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
resetLocalAuthRateLimitForTests();
|
|
vi.unstubAllEnvs();
|
|
await rm(runtimeDir, { force: true, recursive: true });
|
|
});
|
|
|
|
it("logs in every role with the platform phone/password session", async () => {
|
|
const response = await POST(new Request("http://127.0.0.1/api/auth/password", {
|
|
method: "POST",
|
|
headers: { "x-forwarded-for": "192.0.2.10" },
|
|
body: JSON.stringify({
|
|
phone: "138 0013-8000",
|
|
password: "TestPass123",
|
|
next: "/assets"
|
|
})
|
|
}));
|
|
|
|
expect(response.status).toBe(200);
|
|
await expect(response.json()).resolves.toMatchObject({
|
|
ok: true,
|
|
redirectTo: "/assets",
|
|
authMode: "user",
|
|
user: { id: ordinaryUser.id, phone: "13800138000", role: "user", clientId: "platform" }
|
|
});
|
|
expect(await parseSessionCookieValue(
|
|
response.cookies.get("zhinian_session")?.value,
|
|
SESSION_SECRET
|
|
)).toMatchObject({
|
|
authMode: "user",
|
|
sessionVersion: 1,
|
|
user: { id: ordinaryUser.id, role: "user", clientId: "platform" }
|
|
});
|
|
});
|
|
|
|
it("logs in a super administrator through the same endpoint", async () => {
|
|
const admin = await createPlatformUser({
|
|
phone: "13900139000",
|
|
displayName: "平台超级管理员",
|
|
password: "AdminPass123",
|
|
role: "super_admin"
|
|
});
|
|
|
|
const response = await POST(new Request("http://127.0.0.1/api/auth/password", {
|
|
method: "POST",
|
|
body: JSON.stringify({ phone: admin.phone, password: "AdminPass123", authMode: "admin" })
|
|
}));
|
|
|
|
expect(response.status).toBe(200);
|
|
await expect(response.json()).resolves.toMatchObject({
|
|
authMode: "admin",
|
|
user: { id: admin.id, role: "super_admin" }
|
|
});
|
|
});
|
|
|
|
it("locks an account after five failed passwords", async () => {
|
|
let lastResponse: Response | undefined;
|
|
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
lastResponse = await POST(new Request("http://127.0.0.1/api/auth/password", {
|
|
method: "POST",
|
|
headers: { "x-forwarded-for": "192.0.2.11" },
|
|
body: JSON.stringify({ phone: ordinaryUser.phone, password: "wrong-pass" })
|
|
}));
|
|
expect(lastResponse.status).toBe(attempt === 5 ? 423 : 401);
|
|
}
|
|
await expect(lastResponse?.json()).resolves.toMatchObject({ error: "登录失败次数过多,请 15 分钟后再试。" });
|
|
|
|
const lockedResponse = await POST(new Request("http://127.0.0.1/api/auth/password", {
|
|
method: "POST",
|
|
body: JSON.stringify({ phone: ordinaryUser.phone, password: "TestPass123" })
|
|
}));
|
|
expect(lockedResponse.status).toBe(423);
|
|
});
|
|
|
|
it("rejects disabled accounts and duplicate phone identities", async () => {
|
|
await expect(createPlatformUser({
|
|
phone: ordinaryUser.phone,
|
|
displayName: "重复账号",
|
|
password: "AnotherPass123",
|
|
role: "user",
|
|
organizationId: ordinaryUser.organizationId
|
|
})).rejects.toThrow("该手机号已创建账号。");
|
|
|
|
await updatePlatformUser(ordinaryUser.id, { status: "disabled" });
|
|
const response = await POST(new Request("http://127.0.0.1/api/auth/password", {
|
|
method: "POST",
|
|
body: JSON.stringify({ phone: ordinaryUser.phone, password: "TestPass123" })
|
|
}));
|
|
expect(response.status).toBe(403);
|
|
await expect(response.json()).resolves.toEqual({ error: "账号已停用,请联系管理员。" });
|
|
});
|
|
});
|