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:
co-authored by
Claude Opus 5
parent
8f90347659
commit
1d02598786
+13
-4
@@ -35,12 +35,21 @@ const ADMIN_EMAILS = (process.env.ADMIN_EMAILS ?? "")
|
||||
.filter(Boolean)
|
||||
|
||||
export function isAdminUser(
|
||||
u: { id?: string; email?: string; role?: string | null } | null | undefined
|
||||
u:
|
||||
| { id?: string; email?: string; emailVerified?: boolean; role?: string | null }
|
||||
| null
|
||||
| undefined
|
||||
): boolean {
|
||||
if (!u) return false
|
||||
if (u.role === "admin") return true
|
||||
// ADMIN_USER_IDS is the unconditional bootstrap path: an id can only come from
|
||||
// a row we created, so it is not attacker-selectable.
|
||||
if (u.id && ADMIN_USER_IDS.includes(u.id)) return true
|
||||
if (u.email && ADMIN_EMAILS.includes(u.email.toLowerCase())) return true
|
||||
// ADMIN_EMAILS is matched on a user-supplied string, so it additionally
|
||||
// requires a VERIFIED address. Otherwise, in any environment where
|
||||
// REQUIRE_EMAIL_VERIFICATION is off, simply signing up with a listed address
|
||||
// would grant admin without ever controlling the mailbox.
|
||||
if (u.email && u.emailVerified && ADMIN_EMAILS.includes(u.email.toLowerCase())) return true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -52,7 +61,7 @@ export function isAdminUser(
|
||||
export async function getAdminSession() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
const user = session?.user ?? null
|
||||
if (!isAdminUser(user as { role?: string | null })) return null
|
||||
if (!isAdminUser(user)) return null
|
||||
const profile = await db.query.profiles.findFirst({ where: eq(profiles.id, user!.id) })
|
||||
return { user: user!, profile: profile ?? null }
|
||||
}
|
||||
@@ -65,7 +74,7 @@ export async function requireAdmin() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
const user = session?.user ?? null
|
||||
if (!user) redirect("/login")
|
||||
if (!isAdminUser(user as { role?: string | null })) redirect("/dashboard")
|
||||
if (!isAdminUser(user)) redirect("/dashboard")
|
||||
const profile = await db.query.profiles.findFirst({ where: eq(profiles.id, user.id) })
|
||||
return { user, profile: profile ?? null }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user