1532a9de3e
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
import "server-only";
|
|
|
|
import type { NextRequest } from "next/server";
|
|
import { AUTH_COOKIE_NAME } from "@/lib/auth";
|
|
import { decodeSessionPayload, roleForUsername } from "./auth";
|
|
import { effectiveSecurity } from "./systemConfig";
|
|
import { ensureBootstrapAdmin, type UserRole } from "./userStore";
|
|
|
|
export interface Actor {
|
|
sub: string;
|
|
role: UserRole;
|
|
}
|
|
|
|
export class ActorError extends Error {
|
|
constructor(
|
|
message: string,
|
|
readonly status: 401 | 403 = 401
|
|
) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the end-user actor for a BFF request. When WebUI authentication is
|
|
* disabled (single-user local deployments), every request acts as the
|
|
* built-in local admin — the loopback-only posture is enforced by the
|
|
* callers that need it.
|
|
*/
|
|
export function requireActor(request: NextRequest): Actor {
|
|
if (!effectiveSecurity().authEnabled) {
|
|
return { sub: "local-admin", role: "admin" };
|
|
}
|
|
const session = decodeSessionPayload(
|
|
request.cookies.get(AUTH_COOKIE_NAME)?.value ?? ""
|
|
);
|
|
if (!session || session.expiresAt <= Math.floor(Date.now() / 1000)) {
|
|
throw new ActorError("Authentication required.", 401);
|
|
}
|
|
ensureBootstrapAdmin();
|
|
const role = roleForUsername(session.username);
|
|
if (!role) {
|
|
throw new ActorError("Authentication required.", 401);
|
|
}
|
|
return { sub: session.username, role };
|
|
}
|
|
|
|
export function requireAdmin(actor: Actor): void {
|
|
if (actor.role !== "admin") {
|
|
throw new ActorError("Model configuration requires an admin account.", 403);
|
|
}
|
|
}
|