Role split and owner gating
All checks were successful
Build and Push / build (push) Successful in 1m24s
All checks were successful
Build and Push / build (push) Successful in 1m24s
This commit is contained in:
@@ -1,19 +1,87 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import type { SessionUser } from "@/types";
|
||||
|
||||
/**
|
||||
* Read-only session lookup. Returns the SessionUser stashed on the
|
||||
* NextAuth session by `auth.ts::callbacks.session`, or null if there
|
||||
* is no authenticated session.
|
||||
*/
|
||||
export async function getSessionUser(): Promise<SessionUser | null> {
|
||||
const session = await auth();
|
||||
return (session as any)?.platformUser ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws if there is no authenticated session. Otherwise returns the
|
||||
* SessionUser. Use at the top of any handler that requires a logged-in
|
||||
* user regardless of role.
|
||||
*/
|
||||
export async function requireSession(): Promise<SessionUser> {
|
||||
const user = await getSessionUser();
|
||||
if (!user) throw new Error("Unauthorized");
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws unless the caller has a platform-level role
|
||||
* (platform_admin or platform_operator). Use to gate /api/admin/*
|
||||
* routes — these handle ANY customer's org and must not be accessible
|
||||
* to customer-role users.
|
||||
*/
|
||||
export async function requirePlatformRole(): Promise<SessionUser> {
|
||||
const user = await requireSession();
|
||||
if (!user.isPlatform) throw new Error("Forbidden: platform role required");
|
||||
return user;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slice 5: role predicates and gates
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Naming convention: `is*` are pure predicates over a SessionUser,
|
||||
// safe to call inline in JSX/server components. `require*` throw on
|
||||
// failure and are meant for the top of route handlers.
|
||||
|
||||
/**
|
||||
* True when the user is a platform admin/operator OR holds the
|
||||
* `owner` customer role on their org.
|
||||
*
|
||||
* This is the single check for "can mutate". Platform users always
|
||||
* win because they administer all orgs cross-cut. Customer-side, only
|
||||
* `owner` may mutate; `user` (and any future read-only customer role)
|
||||
* cannot.
|
||||
*/
|
||||
export function canMutate(user: SessionUser): boolean {
|
||||
return user.isPlatform || user.roles.includes("owner");
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the user holds the customer `owner` role on their org.
|
||||
* Excludes platform users — use {@link canMutate} when both should
|
||||
* be allowed.
|
||||
*
|
||||
* Useful for permissions that are specifically about "this customer's
|
||||
* own owner", e.g. "owner can invite users into their own org" — a
|
||||
* platform user shouldn't be casually inviting users into a customer
|
||||
* org, that's an admin-console action and goes through different
|
||||
* tooling.
|
||||
*/
|
||||
export function isCustomerOwner(user: SessionUser): boolean {
|
||||
return !user.isPlatform && user.roles.includes("owner");
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws unless `canMutate(user) === true`. Use at the top of any
|
||||
* mutating customer-side handler.
|
||||
*
|
||||
* The thrown error message is intentionally generic — handlers
|
||||
* should catch and translate to a 403 JSON response so the client
|
||||
* doesn't see a stack trace.
|
||||
*/
|
||||
export async function requireOwnerRole(): Promise<SessionUser> {
|
||||
const user = await requireSession();
|
||||
if (!canMutate(user)) {
|
||||
throw new Error("Forbidden: owner role required");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user