fix(custom-widgets): require explicit request bindings

This commit is contained in:
Thomas Camlong committed 2026-07-20 17:02:59 +02:00
1 parent 4c69ad90df
commit a26846fa31
23 files changed
+363 -52

No files matched your search

@@ -107,7 +107,7 @@ export const managementQueryProcedures = {
),
optionRequests: widget.requests
.filter((request) => request.kind === "query")
.map(({ id, parameters }) => ({ id, parameters })),
.map(({ id, parameters, optionsBinding }) => ({ id, parameters, optionsBinding })),
updatedAt: definition.updatedAt,
};
});
@@ -89,13 +89,24 @@ export const previewBaseProcedures = {
optionsSchema: session.optionsSchema,
defaultOptions: session.defaultOptions,
requests: session.requests.map(
({ id, kind, method, minimumBoardPermission, trigger, parameters, confirmation, invalidates }) => ({
({
id,
kind,
method,
minimumBoardPermission,
trigger,
parameters,
optionsBinding,
confirmation,
invalidates,
}) => ({
id,
kind,
method,
minimumBoardPermission,
trigger,
parameters,
optionsBinding,
confirmation,
invalidates,
}),
@@ -1,6 +1,8 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import { resolveCustomWidgetOptionsBinding } from "@homarr/custom-widgets/core";
import { permissionRequiredProcedure } from "../../trpc";
import {
getPreviewRequestSource,
@@ -31,8 +33,10 @@ export const previewQueryProcedures = {
if (!request) throw new TRPCError({ code: "NOT_FOUND", message: "Preview query was not found" });
const resolved = getPreviewRequestSource(session, request.sourceId);
if (!resolved) throw new TRPCError({ code: "NOT_FOUND", message: "Preview source was not found" });
const targetUrl = renderRequestTarget(resolved.source.baseUrl, request, input.params);
const body = renderRequestBody(request.bodyTemplate, input.params);
const params =
request.trigger === "load" ? resolveCustomWidgetOptionsBinding(request, session.defaultOptions) : input.params;
const targetUrl = renderRequestTarget(resolved.source.baseUrl, request, params);
const body = renderRequestBody(request.bodyTemplate, params);
const release = await acquireCustomWidgetRequestLimit({
category: "query",
userId: ctx.session.user.id,
@@ -50,7 +54,7 @@ export const previewQueryProcedures = {
auth: request.auth === "none" ? undefined : resolved.auth,
networkScope: resolved.source.networkScope,
kind: "query",
cacheKey: `custom-jsx:preview:${session.id}:${request.id}:${hashRuntimeParams(input.params)}`,
cacheKey: `custom-jsx:preview:${session.id}:${request.id}:${hashRuntimeParams(params)}`,
cacheTtlSeconds: request.cacheTtlSeconds,
});
await recordPreviewJournal(session, {
+6 -16
View File
@@ -7,6 +7,7 @@ import { eq } from "@homarr/db";
import { boards, customWidgetDefinitions, items } from "@homarr/db/schema";
import type { BoardPermission } from "@homarr/definitions";
import { validateCustomWidgetOptions } from "@homarr/custom-widgets/core";
import { resolveCustomWidgetOptionsBinding } from "@homarr/custom-widgets/core";
import type { CustomJsxRequest, CustomWidgetSource } from "@homarr/custom-widgets/core";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
@@ -173,21 +174,6 @@ const executeRequest = async (
);
};
const getLoadParams = (resolved: ResolvedDefinition, request: CustomJsxRequest) => {
const params: Record<string, string | number | boolean> = {};
for (const [name, type] of Object.entries(request.parameters)) {
const value = resolved.configuration[name];
if (typeof value !== type) {
throw new TRPCError({
code: "PRECONDITION_FAILED",
message: `Load request '${request.id}' needs option '${name}' (${type})`,
});
}
params[name] = value as string | number | boolean;
}
return params;
};
export const customApiRouter = createTRPCRouter({
getData: publicProcedure.input(itemInputSchema).query(async ({ ctx, input }) => {
const resolved = await resolvePlacedDefinitionAsync(ctx, input.itemId);
@@ -197,7 +183,8 @@ export const customApiRouter = createTRPCRouter({
const entries = await Promise.all(
loadRequests.map(async (request) => {
try {
const response = await executeRequest(ctx, resolved, request, getLoadParams(resolved, request));
const params = resolveCustomWidgetOptionsBinding(request, resolved.configuration);
const response = await executeRequest(ctx, resolved, request, params);
return [
request.id,
{
@@ -249,6 +236,9 @@ export const customApiRouter = createTRPCRouter({
queryRequest: publicProcedure.input(namedRequestInputSchema).query(async ({ ctx, input }) => {
const resolved = await resolvePlacedDefinitionAsync(ctx, input.itemId);
const request = findRequest(resolved, input.requestId, "query");
if (request.trigger !== "manual") {
throw new TRPCError({ code: "BAD_REQUEST", message: "Load queries cannot be invoked manually" });
}
const response = await executeRequest(ctx, resolved, request, input.params);
return {
ok: response.ok,