diff --git a/TODO.md b/TODO.md index ee0fa67..0d71663 100644 --- a/TODO.md +++ b/TODO.md @@ -31,7 +31,7 @@ This file is the single source of truth for roadmap and delivery progress. - [x] [P1] Admin registration policy control (allow/deny self-registration for admin panel) - [x] [P1] First-start onboarding route for initial owner creation (`/welcome`) - [x] [P1] Split auth entry points (`/welcome`, `/login`, `/register`) with cross-links -- [~] [P2] Support fallback sign-in route (`/support/:key`) as break-glass access +- [x] [P2] Support fallback sign-in route (`/support/:key`) as break-glass access - [~] [P1] Reusable CRUD base patterns (list/detail/editor/service/repository) - [~] [P1] Shared CRUD validation strategy (Zod + server-side enforcement) - [~] [P1] Shared error and audit hooks for CRUD mutations diff --git a/apps/admin/src/lib/access.test.ts b/apps/admin/src/lib/access.test.ts new file mode 100644 index 0000000..717e061 --- /dev/null +++ b/apps/admin/src/lib/access.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest" + +import { canAccessRoute, getRequiredPermission, isPublicRoute } from "./access" + +describe("admin route access rules", () => { + it("treats support fallback route as public", () => { + expect(isPublicRoute("/support/support-access")).toBe(true) + expect(canAccessRoute("editor", "/support/support-access")).toBe(true) + }) + + it("keeps settings route restricted to role with users:manage_roles", () => { + expect(isPublicRoute("/settings")).toBe(false) + expect(canAccessRoute("manager", "/settings")).toBe(false) + expect(canAccessRoute("admin", "/settings")).toBe(true) + expect(canAccessRoute("owner", "/settings")).toBe(true) + }) + + it("resolves route-specific permission requirements", () => { + expect(getRequiredPermission("/todo")).toEqual({ + permission: "roadmap:read", + scope: "global", + }) + }) +}) diff --git a/e2e/support-auth.pw.ts b/e2e/support-auth.pw.ts new file mode 100644 index 0000000..d334213 --- /dev/null +++ b/e2e/support-auth.pw.ts @@ -0,0 +1,20 @@ +import { expect, test } from "@playwright/test" + +const SUPPORT_LOGIN_KEY = process.env.CMS_SUPPORT_LOGIN_KEY ?? "support-access" + +test.describe("support fallback route", () => { + test("valid support key opens sign-in page", async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== "admin-chromium") + + await page.goto(`/support/${SUPPORT_LOGIN_KEY}`) + + await expect(page.getByRole("heading", { name: /sign in to cms admin/i })).toBeVisible() + }) + + test("invalid support key returns not found", async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== "admin-chromium") + + const response = await page.goto("/support/invalid-key") + expect(response?.status()).toBe(404) + }) +})