chore: sync in-progress work across marketing, admin, API and tests

Snapshot of uncommitted work that had accumulated in the tree alongside
the Turnstile changes:

- marketing pages, SEO helpers (lib/seo.ts, lib/marketing/) and
  structured data
- admin billing actions and a per-user portfolio view, plus an admin
  error boundary
- rate limiting (lib/rate-limit.ts) applied across the /api/v1 surface
- CSP and proxy adjustments, accounting/webhook lib updates
- Playwright config and an e2e/unit test suite
- next bumped to ^16.3.4 with the lockfile regenerated
- generated AGENTS.md / CLAUDE.md

Authored by other sessions working in this tree; committed here so the
Turnstile work could be pushed without leaving the tree dirty.
Typecheck passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-09-05 16:27:16 -04:00
co-authored by Claude Opus 5
parent 8f90347659
commit 1d02598786
71 changed files with 3641 additions and 819 deletions
+174
View File
@@ -0,0 +1,174 @@
import { test, expect } from "@playwright/test"
/**
* Security-header and access-control smoke tests against a running build.
*
* Deliberately scoped to behaviour that does NOT require a seeded database, so
* this suite is runnable against any deployed instance (set E2E_BASE_URL).
* Checks that DO need one live in the "requires a database" block at the bottom
* and are skipped unless E2E_DB=1.
*
* npm run build && E2E=1 npm run test:e2e
* E2E_BASE_URL=https://staging.example.com npm run test:e2e
*/
const STATIC_PAGES = ["/terms", "/refund-policy", "/subprocessors", "/tenant-portal-info"]
const APP_PAGES = ["/login", "/signup", "/forgot-password"]
const PROTECTED = ["/dashboard", "/properties", "/tenants", "/rent", "/settings", "/admin"]
test.describe("baseline security headers", () => {
test("every response carries the standard hardening headers", async ({ request }) => {
const res = await request.get("/login")
const h = res.headers()
expect(h["x-content-type-options"]).toBe("nosniff")
expect(h["x-frame-options"]).toBe("DENY")
expect(h["referrer-policy"]).toBe("no-referrer")
expect(h["strict-transport-security"]).toContain("max-age=")
})
test("Referrer-Policy is no-referrer so portal tokens cannot leak", async ({ request }) => {
// Tenant-portal and calendar URLs carry their credential in the path, so a
// Referer header would hand it to any third-party resource.
const res = await request.get("/tenant-portal-info")
expect(res.headers()["referrer-policy"]).toBe("no-referrer")
})
test("CSP forbids framing and plugins everywhere", async ({ request }) => {
for (const path of [...STATIC_PAGES, ...APP_PAGES]) {
const csp = (await request.get(path)).headers()["content-security-policy"] ?? ""
expect(csp, `${path} CSP`).toContain("frame-ancestors 'none'")
expect(csp, `${path} CSP`).toContain("object-src 'none'")
expect(csp, `${path} CSP`).toContain("base-uri 'self'")
expect(csp, `${path} CSP`).toContain("form-action 'self'")
}
})
})
test.describe("CSP nonce scoping", () => {
// The app surface gets a strict per-request nonce. Static pages keep
// 'unsafe-inline' because they are prerendered at build time with no nonce on
// their script tags — serving them a fresh nonce would block every script.
for (const path of APP_PAGES) {
test(`${path} uses a nonce and every inline script carries it`, async ({ request }) => {
const res = await request.get(path)
const csp = res.headers()["content-security-policy"] ?? ""
const nonce = csp.match(/'nonce-([A-Za-z0-9+/=]+)'/)?.[1]
expect(nonce, `${path} should have a nonce in script-src`).toBeTruthy()
expect(csp).not.toContain("script-src 'self' 'unsafe-inline'")
const html = await res.text()
const scripts = html.match(/<script[^>]*>/g) ?? []
const unnonced = scripts.filter((s) => !s.includes(`nonce="${nonce}"`))
expect(unnonced, `${path} has script tags without the nonce`).toEqual([])
})
}
for (const path of STATIC_PAGES) {
test(`${path} keeps the permissive policy so prerendered scripts still run`, async ({
request,
}) => {
const csp = (await request.get(path)).headers()["content-security-policy"] ?? ""
expect(csp).toContain("script-src 'self' 'unsafe-inline'")
expect(csp).not.toMatch(/'nonce-/)
})
}
test("each request gets a fresh nonce", async ({ request }) => {
const nonceOf = async () =>
((await request.get("/login")).headers()["content-security-policy"] ?? "").match(
/'nonce-([A-Za-z0-9+/=]+)'/
)?.[1]
const [a, b] = [await nonceOf(), await nonceOf()]
expect(a).toBeTruthy()
expect(a).not.toBe(b)
})
})
test.describe("unauthenticated access control", () => {
for (const path of PROTECTED) {
test(`${path} redirects an anonymous visitor to /login`, async ({ page }) => {
await page.goto(path)
await expect(page).toHaveURL(/\/login/)
})
}
test("the public API rejects a request with no bearer token", async ({ request }) => {
for (const path of [
"/api/v1/tenants",
"/api/v1/properties",
"/api/v1/maintenance",
"/api/v1/payments",
"/api/v1/webhooks",
]) {
const res = await request.get(path)
expect(res.status(), `${path} should be 401`).toBe(401)
}
})
test("cron endpoints reject an unauthenticated caller", async ({ request }) => {
for (const path of ["/api/cron/daily", "/api/cron/late-fees", "/api/cron/gdpr"]) {
const res = await request.get(path, { failOnStatusCode: false })
expect([401, 403], `${path} should refuse`).toContain(res.status())
}
})
})
test.describe("public endpoints", () => {
test("health responds without touching the database", async ({ request }) => {
const res = await request.get("/api/health")
expect(res.status()).toBe(200)
expect((await res.json()).status).toBe("ok")
})
test("static legal pages render", async ({ page }) => {
for (const path of STATIC_PAGES) {
const res = await page.goto(path)
expect(res?.status(), `${path} should render`).toBe(200)
}
})
test("an unknown calendar token is not found and is never shared-cached", async ({
request,
}) => {
// The feed carries tenant names, rent amounts and addresses behind nothing
// but the token in the URL, so it must never be stored by a shared cache.
const res = await request.get("/api/calendar/definitely-not-a-real-token.ics", {
failOnStatusCode: false,
})
const cache = res.headers()["cache-control"] ?? ""
expect(cache).not.toContain("public")
})
})
test.describe("requires a database", () => {
// A PRESENTED-but-invalid key must be looked up before it can be rejected, so
// unlike the no-token case these cannot short-circuit. Set E2E_DB=1 when
// DATABASE_URL points at a migrated database.
test.skip(
!process.env.E2E_DB,
"Set E2E_DB=1 with a migrated DATABASE_URL to run database-backed checks."
)
test("the public API rejects a malformed bearer token", async ({ request }) => {
const res = await request.get("/api/v1/tenants", {
headers: { authorization: "Bearer pmn_live_totally-made-up" },
failOnStatusCode: false,
})
expect(res.status()).toBe(401)
})
test("session-gated API routes reject an anonymous caller", async ({ request }) => {
for (const path of ["/api/properties", "/api/tenants", "/api/rent", "/api/admin/users"]) {
const res = await request.get(path, { failOnStatusCode: false })
expect([401, 403], `${path} should refuse`).toContain(res.status())
}
})
test("an unknown tenant-portal token 404s rather than leaking", async ({ request }) => {
const res = await request.get("/tenant-portal/definitely-not-a-real-token", {
failOnStatusCode: false,
})
expect(res.status()).toBe(404)
})
})