refactor: consolidate custom widget admin routes

This commit is contained in:
ajnart committed 2026-07-30 17:51:52 +02:00
1 parent 48438fc003
commit 74fa51a461
8 files changed
+72 -73

No files matched your search

@@ -0,0 +1,42 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { CUSTOM_WIDGET_MCP_AUTHORING_PROMPT } from "@homarr/custom-widgets/authoring-prompt";
import { getCustomWidgetComponentCatalog, getCustomWidgetSkill } from "@homarr/custom-widgets/authoring-resources";
import { getCustomWidgetJsonSchema } from "@homarr/custom-widgets/core";
import { adminRoute } from "../admin";
interface RouteContext {
params: Promise<{ resource: string }>;
}
const resources = new Map<string, () => unknown>([
["schema", getCustomWidgetJsonSchema],
["components", getCustomWidgetComponentCatalog],
["skill", getCustomWidgetSkill],
[
"authoring-prompt",
() => ({
version: 2,
prompt: CUSTOM_WIDGET_MCP_AUTHORING_PROMPT,
resources: [
"homarr://custom-widgets/schema",
"homarr://custom-widgets/components",
"homarr://custom-widgets/skill",
],
httpResources: ["/api/custom-widgets/schema", "/api/custom-widgets/components", "/api/custom-widgets/skill"],
}),
],
]);
const getAuthoringResource = async (_request: NextRequest, context: RouteContext): Promise<Response> => {
const { resource } = await context.params;
const loadResource = resources.get(resource);
if (!loadResource) {
return NextResponse.json({ error: "Custom Widget authoring resource not found." }, { status: 404 });
}
return NextResponse.json(loadResource());
};
export const GET = adminRoute(getAuthoringResource);
@@ -8,7 +8,7 @@ const mocks = vi.hoisted(() => ({
vi.mock("@homarr/auth/next", () => ({ auth: mocks.auth }));
import { requireCustomWidgetAdmin } from "./admin";
import { adminRoute } from "./admin";
beforeEach(() => {
mocks.auth.mockResolvedValue({ user: { permissions: ["admin"] } });
@@ -16,7 +16,11 @@ beforeEach(() => {
describe("Custom Widget HTTP resource access", () => {
test("allows authenticated administrators", async () => {
await expect(requireCustomWidgetAdmin()).resolves.toBeNull();
const handler = vi.fn(async () => Response.json({ ok: true }));
const response = await adminRoute(handler)();
expect(response.status).toBe(200);
expect(handler).toHaveBeenCalledOnce();
});
test.each([
@@ -24,7 +28,10 @@ describe("Custom Widget HTTP resource access", () => {
["authenticated non-admins", { user: { permissions: ["board-modify-all"] } }],
])("denies %s", async (_label, session) => {
mocks.auth.mockResolvedValue(session);
const response = await requireCustomWidgetAdmin();
expect(response?.status).toBe(403);
const handler = vi.fn(async () => Response.json({ ok: true }));
const response = await adminRoute(handler)();
expect(response.status).toBe(403);
expect(handler).not.toHaveBeenCalled();
});
});
@@ -2,10 +2,14 @@ import { NextResponse } from "next/server";
import { auth } from "@homarr/auth/next";
export const requireCustomWidgetAdmin = async (): Promise<NextResponse | null> => {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
return NextResponse.json({ error: "Administrator access is required." }, { status: 403 });
}
return null;
};
type RouteHandler<Arguments extends unknown[]> = (...args: Arguments) => Response | Promise<Response>;
export const adminRoute =
<Arguments extends unknown[]>(handler: RouteHandler<Arguments>) =>
async (...args: Arguments): Promise<Response> => {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
return NextResponse.json({ error: "Administrator access is required." }, { status: 403 });
}
return handler(...args);
};
@@ -1,20 +0,0 @@
import { NextResponse } from "next/server";
import { CUSTOM_WIDGET_MCP_AUTHORING_PROMPT } from "@homarr/custom-widgets/authoring-prompt";
import { requireCustomWidgetAdmin } from "../admin";
export async function GET() {
const denied = await requireCustomWidgetAdmin();
if (denied) return denied;
return NextResponse.json({
version: 2,
prompt: CUSTOM_WIDGET_MCP_AUTHORING_PROMPT,
resources: [
"homarr://custom-widgets/schema",
"homarr://custom-widgets/components",
"homarr://custom-widgets/skill",
],
httpResources: ["/api/custom-widgets/schema", "/api/custom-widgets/components", "/api/custom-widgets/skill"],
});
}
@@ -1,11 +0,0 @@
import { NextResponse } from "next/server";
import { getCustomWidgetComponentCatalog } from "@homarr/custom-widgets/authoring-resources";
import { requireCustomWidgetAdmin } from "../admin";
export async function GET() {
const denied = await requireCustomWidgetAdmin();
if (denied) return denied;
return NextResponse.json(getCustomWidgetComponentCatalog());
}
@@ -13,16 +13,14 @@ import { customWidgetSourceSchema } from "@homarr/custom-widgets/core";
import { invalidateCustomWidgetResponseCache } from "@homarr/custom-widgets/server";
import { db } from "@homarr/db";
import { requireCustomWidgetAdmin } from "../../admin";
import { adminRoute } from "../../admin";
import { readConfigurationRequestBody } from "../body";
interface RouteContext {
params: Promise<{ token: string }>;
}
export async function GET(_request: NextRequest, context: RouteContext) {
const denied = await requireCustomWidgetAdmin();
if (denied) return denied;
const getConfigurationRequest = async (_request: NextRequest, context: RouteContext): Promise<Response> => {
const { token } = await context.params;
const request = await getCustomWidgetConfigurationRequest(token);
if (!request)
@@ -38,11 +36,9 @@ export async function GET(_request: NextRequest, context: RouteContext) {
},
{ headers: { "Cache-Control": "no-store" } },
);
}
};
export async function POST(request: NextRequest, context: RouteContext) {
const denied = await requireCustomWidgetAdmin();
if (denied) return denied;
const completeConfigurationRequest = async (request: NextRequest, context: RouteContext): Promise<Response> => {
const { token } = await context.params;
const pending = await getCustomWidgetConfigurationRequest(token);
if (!pending || pending.status !== "pending") {
@@ -128,4 +124,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
} finally {
await releaseCustomWidgetConfigurationRequest(token);
}
}
};
export const GET = adminRoute(getConfigurationRequest);
export const POST = adminRoute(completeConfigurationRequest);
@@ -1,11 +0,0 @@
import { NextResponse } from "next/server";
import { getCustomWidgetJsonSchema } from "@homarr/custom-widgets/core";
import { requireCustomWidgetAdmin } from "../admin";
export async function GET() {
const denied = await requireCustomWidgetAdmin();
if (denied) return denied;
return NextResponse.json(getCustomWidgetJsonSchema());
}
@@ -1,11 +0,0 @@
import { NextResponse } from "next/server";
import { getCustomWidgetSkill } from "@homarr/custom-widgets/authoring-resources";
import { requireCustomWidgetAdmin } from "../admin";
export async function GET() {
const denied = await requireCustomWidgetAdmin();
if (denied) return denied;
return NextResponse.json(getCustomWidgetSkill());
}