fix: bootstrap admin CSRF tokens
This commit is contained in:
1 parent
a57c73055f
commit
b3245a18ea
8 files changed
+270
-45
No files matched your search
@@ -17,11 +17,11 @@ import {
|
||||
parseAdminNavConfig,
|
||||
} from "@/lib/admin-nav-config";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { setCsrfCookie } from "@/lib/foundation/security";
|
||||
import { readCsrfCookieToken } from "@/lib/foundation/security";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
// Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be
|
||||
// Request-time auth, existing CSRF cookie, and optional 2FA gate cannot be
|
||||
// statically rendered. Child admin pages inherit this — leaf force-dynamic is redundant.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -31,12 +31,7 @@ export default async function AdminLayout({
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const staff = await requireStaff();
|
||||
let csrfToken = "";
|
||||
try {
|
||||
csrfToken = await setCsrfCookie();
|
||||
} catch {
|
||||
csrfToken = "";
|
||||
}
|
||||
const csrfToken = await readCsrfCookieToken();
|
||||
if (await siteSettings.getBool("force_staff_2fa", false)) {
|
||||
const u = await db
|
||||
.select({ twoFactorConfirmedAt: User.twoFactorConfirmedAt })
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
setCsrfCookie: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api-handler", () => ({
|
||||
withAdmin:
|
||||
(_options: unknown, handler: (...args: never[]) => unknown) =>
|
||||
(...args: never[]) =>
|
||||
handler(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/foundation/security", () => ({
|
||||
setCsrfCookie: mocks.setCsrfCookie,
|
||||
}));
|
||||
|
||||
import { GET } from "./route";
|
||||
|
||||
describe("admin CSRF bootstrap route", () => {
|
||||
beforeEach(() => {
|
||||
mocks.setCsrfCookie.mockReset();
|
||||
});
|
||||
|
||||
it("returns the token written by the route handler without caching", async () => {
|
||||
const token = "a".repeat(64);
|
||||
mocks.setCsrfCookie.mockResolvedValue(token);
|
||||
|
||||
const response = await GET(
|
||||
new NextRequest("http://localhost/api/admin/csrf"),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("cache-control")).toBe("no-store");
|
||||
expect(await response.json()).toEqual({ ok: true, token });
|
||||
});
|
||||
|
||||
it("fails closed when the cookie cannot be written", async () => {
|
||||
mocks.setCsrfCookie.mockResolvedValue("");
|
||||
|
||||
const response = await GET(
|
||||
new NextRequest("http://localhost/api/admin/csrf"),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(await response.json()).toEqual({
|
||||
error: "Unable to initialize CSRF token",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { apiError, apiOk } from "@/lib/api-response";
|
||||
import { setCsrfCookie } from "@/lib/foundation/security";
|
||||
|
||||
export const GET = withAdmin({ requireCsrf: false }, async () => {
|
||||
const token = await setCsrfCookie();
|
||||
if (!token) return apiError("Unable to initialize CSRF token", 500);
|
||||
|
||||
const response = apiOk({ token });
|
||||
response.headers.set("Cache-Control", "no-store");
|
||||
return response;
|
||||
});
|
||||
@@ -17,7 +17,7 @@ import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { ThemeSwitcher } from "@/components/theme-switcher";
|
||||
import { requireMod } from "@/lib/admin/guard";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { setCsrfCookie } from "@/lib/foundation/security";
|
||||
import { readCsrfCookieToken } from "@/lib/foundation/security";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
@@ -25,12 +25,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ModLayout({ children }: { children: ReactNode }) {
|
||||
const staff = await requireMod();
|
||||
let csrfToken = "";
|
||||
try {
|
||||
csrfToken = await setCsrfCookie();
|
||||
} catch {
|
||||
csrfToken = "";
|
||||
}
|
||||
const csrfToken = await readCsrfCookieToken();
|
||||
if (await siteSettings.getBool("force_staff_2fa", false)) {
|
||||
const [u] = await db
|
||||
.select({ twoFactorConfirmedAt: User.twoFactorConfirmedAt })
|
||||
|
||||
Reference in new issue
Block a user