71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import type { AppContext, AuditActor, AuthContext } from "./api-context";
|
|
import { AppError } from "./app-error";
|
|
import { forbidden, unauthorized } from "./http-errors";
|
|
|
|
export const ALL_TOKEN_SCOPES = [
|
|
"read:notebooks",
|
|
"write:notebooks",
|
|
"read:memos",
|
|
"write:memos",
|
|
"read:resources",
|
|
"write:resources",
|
|
"read:tags",
|
|
"write:tags",
|
|
] as const;
|
|
|
|
export type TokenScope = (typeof ALL_TOKEN_SCOPES)[number];
|
|
|
|
export const getAuditActor = (c: AppContext): AuditActor => {
|
|
const auth = c.get("auth");
|
|
return {
|
|
actorType: auth?.actorType ?? "user",
|
|
actorId: auth?.actorId ?? null,
|
|
};
|
|
};
|
|
|
|
export const getActorLabel = (c: AppContext) => {
|
|
const auth = c.get("auth");
|
|
return auth?.actorId ? `${auth.actorType}:${auth.actorId}` : auth?.username ?? "user";
|
|
};
|
|
|
|
export const getWorkspaceId = (c: AppContext) => c.get("auth").workspaceId;
|
|
|
|
export const requireOwner = (c: AppContext) => {
|
|
const auth = c.get("auth");
|
|
return auth?.kind === "user" && auth.role === "owner"
|
|
? null
|
|
: forbidden(c, "Only the instance owner can manage users.");
|
|
};
|
|
|
|
export const requireUser = (c: AppContext) => {
|
|
const auth = c.get("auth");
|
|
return auth?.kind === "user"
|
|
? null
|
|
: forbidden(c, "Only an interactive user session can manage this resource.");
|
|
};
|
|
|
|
export const hasScopes = (auth: AuthContext, scopes: TokenScope[]) =>
|
|
auth.kind === "user" || scopes.every((scope) => auth.scopes.includes(scope));
|
|
|
|
export const requireScopes = (c: AppContext, ...scopes: TokenScope[]) => {
|
|
const auth = c.get("auth");
|
|
if (!auth) return unauthorized(c, "Authentication required.");
|
|
return hasScopes(auth, scopes)
|
|
? null
|
|
: forbidden(c, `Missing required scope: ${scopes.join(", ")}`);
|
|
};
|
|
|
|
export const assertScope = (auth: AuthContext, scope: TokenScope) => {
|
|
if (!hasScopes(auth, [scope])) {
|
|
throw new AppError("forbidden", `Missing required scope: ${scope}`, 403);
|
|
}
|
|
};
|
|
|
|
export const isTokenScope = (scope: string): scope is TokenScope =>
|
|
(ALL_TOKEN_SCOPES as readonly string[]).includes(scope);
|
|
|
|
export const normalizeTokenScopes = (scopes: string[]) => {
|
|
const normalized = Array.from(new Set(scopes.map((scope) => scope.trim()).filter(Boolean)));
|
|
return normalized.some((scope) => !isTokenScope(scope)) ? null : normalized as TokenScope[];
|
|
};
|