bcca7686ee
REQ-001: SSO session via WorkOS (getAuthorizationUrl + exchangeCodeForSession) REQ-002: tenant provisioning on first signup (admin role) REQ-003: invitations via single-use acceptance link REQ-004: RBAC role assignment (admin/operator/viewer) REQ-005: RBAC enforcement at API gateway from first endpoint (/api/me) ---ci--- phase: 2 milestone: v0.1 status: execute ---/ci---
82 lines
2.8 KiB
TypeScript
82 lines
2.8 KiB
TypeScript
/**
|
|
* control-plane lib/auth — Next.js adapter for @coreci/auth middleware.
|
|
*
|
|
* Wraps the framework-agnostic `createAuthMiddleware` so a Route Handler can
|
|
* call `authenticateRequest(request)` and get back a typed AuthResult that
|
|
* either yields the user (authorized) or a ready-made Next.js Response (401 /
|
|
* 403) the handler can return directly.
|
|
*
|
|
* REQ-005 critical-path: every /api/* route calls this BEFORE doing work.
|
|
*/
|
|
|
|
import type { NextRequest } from "next/server";
|
|
import { authenticate, type AuthResult, type AuthRequest } from "@coreci/auth";
|
|
import { getDb } from "./db.js";
|
|
|
|
/** SESSION_SIGNING_KEY — infra bootstrap cred (G-010 tier a). Set via env. */
|
|
function signingKey(): string {
|
|
const k = process.env.SESSION_SIGNING_KEY;
|
|
if (!k) throw new Error("SESSION_SIGNING_KEY not configured");
|
|
return k;
|
|
}
|
|
|
|
/** Convert a NextRequest into the framework-agnostic AuthRequest shape. */
|
|
function toAuthRequest(req: NextRequest): AuthRequest {
|
|
const url = req.nextUrl;
|
|
return {
|
|
method: req.method,
|
|
path: url.pathname,
|
|
getCookie(name: string): string | undefined {
|
|
const c = req.cookies.get(name);
|
|
return c?.value;
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Authenticate + enforce RBAC for a Next.js Route Handler.
|
|
* Returns the AuthResult; the caller branches on `.status`.
|
|
*/
|
|
export async function authenticateRequest(req: NextRequest): Promise<AuthResult> {
|
|
const db = await getDb();
|
|
return authenticate(db, signingKey(), toAuthRequest(req));
|
|
}
|
|
|
|
/** Helper: turn an unauthorized/forbidden AuthResult into a Next.js Response. */
|
|
export function authFailureResponse(result: AuthResult): Response {
|
|
if (result.status === "unauthorized") {
|
|
return new Response(
|
|
JSON.stringify({ error: "unauthorized", reason: result.reason }),
|
|
{ status: 401, headers: { "content-type": "application/json" } },
|
|
);
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "forbidden",
|
|
required: result.decision.required,
|
|
unknownRole: result.decision.unknownRole,
|
|
}),
|
|
{ status: 403, headers: { "content-type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Require authorization for a route. Returns the user on success, or a Response
|
|
* (already-serialized 401/403) on failure. The canonical route handler shape:
|
|
*
|
|
* export async function GET(req) {
|
|
* const auth = await requireAuth(req, "GET", "/api/me");
|
|
* if (auth instanceof Response) return auth;
|
|
* const user = auth.user; // {id, tenantId, role}
|
|
* ...
|
|
* }
|
|
*/
|
|
export async function requireAuth(
|
|
req: NextRequest,
|
|
): Promise<{ user: { id: string; tenantId: string; role: "admin" | "operator" | "viewer" } } | Response> {
|
|
const result = await authenticateRequest(req);
|
|
if (result.status === "authorized") {
|
|
return { user: { id: result.user.userId, tenantId: result.user.tenantId, role: result.user.role } };
|
|
}
|
|
return authFailureResponse(result);
|
|
} |