fix(custom-widgets): configure secrets during import

This commit is contained in:
Thomas Camlong committed 2026-07-20 17:21:51 +02:00
1 parent a26846fa31
commit e4388b0712
16 files changed
+331 -93

No files matched your search

@@ -4,6 +4,7 @@ import { z } from "zod/v4";
import { decryptSecret } from "@homarr/common/server";
import { eq, or } from "@homarr/db";
import { boards, customWidgetDefinitions } from "@homarr/db/schema";
import { getCustomWidgetSecretRequirements } from "@homarr/custom-widgets/core";
import { permissionRequiredProcedure, protectedProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "../board/board-access";
@@ -21,9 +22,11 @@ export const managementQueryProcedures = {
.query(async ({ ctx }) => {
const definitions = await ctx.db.query.customWidgetDefinitions.findMany({
orderBy: (table, { asc }) => asc(table.name),
with: { secrets: true },
});
return definitions.map((definition) => {
const widget = parseStoredCustomWidgetDefinition(definition);
const configuredSecrets = new Set(definition.secrets.map((secret) => `${secret.sourceId}:${secret.kind}`));
return {
id: definition.id,
name: widget.name,
@@ -37,6 +40,9 @@ export const managementQueryProcedures = {
authType: auth.type,
})),
requestCount: widget.requests.length,
missingSecrets: getCustomWidgetSecretRequirements(widget.sources).filter(
(requirement) => !configuredSecrets.has(`${requirement.sourceId}:${requirement.kind}`),
),
defaultOptions: widget.defaultOptions,
updatedAt: definition.updatedAt,
enabled: definition.enabled,
@@ -1,11 +1,9 @@
import { TRPCError } from "@trpc/server";
import { getCustomWidgetRequiredSecretKinds } from "@homarr/custom-widgets/core";
export function requiredSecretKinds(authType: string) {
if (authType === "basic") return ["username", "password"] as const;
if (authType === "bearer" || authType === "apiKeyHeader" || authType === "apiKeyQuery") {
return ["apiKey"] as const;
}
return [] as const;
return getCustomWidgetRequiredSecretKinds(authType as Parameters<typeof getCustomWidgetRequiredSecretKinds>[0]);
}
export function assertSecretSources(
@@ -2,12 +2,14 @@ import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
import { encryptSecret } from "@homarr/common/server";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { eq } from "@homarr/db";
import { customWidgetDefinitions } from "@homarr/db/schema";
import { customWidgetImportSchema } from "@homarr/custom-widgets/core";
import { customWidgetDefinitions, customWidgetSecrets } from "@homarr/db/schema";
import { customWidgetImportSchema, customWidgetSecretsInputSchema } from "@homarr/custom-widgets/core";
import { permissionRequiredProcedure } from "../../trpc";
import { assertSecretSources } from "./secret-policy";
import { parseStoredCustomWidgetDefinition, serializeCustomWidgetDefinition } from "./stored-definition";
const manageProcedure = permissionRequiredProcedure.requiresPermission("custom-widget-manage");
@@ -22,14 +24,34 @@ export const transferProcedures = {
return parseStoredCustomWidgetDefinition(definition);
}),
import: manageProcedure.input(customWidgetImportSchema).mutation(async ({ ctx, input }) => {
const id = createId();
await ctx.db.insert(customWidgetDefinitions).values({
id,
...serializeCustomWidgetDefinition(input),
creatorId: ctx.session.user.id,
});
logger.info("Imported custom widget definition", { id, name: input.name });
return { id };
}),
import: manageProcedure
.input(z.object({ widget: customWidgetImportSchema, secrets: customWidgetSecretsInputSchema.default([]) }))
.mutation(async ({ ctx, input }) => {
if (input.secrets.length > 0 && !ctx.session.user.permissions.includes("custom-widget-secret-write")) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Writing imported widget secrets requires dedicated permission",
});
}
assertSecretSources(input.widget.sources, input.secrets);
const id = createId();
await ctx.db.insert(customWidgetDefinitions).values({
id,
...serializeCustomWidgetDefinition(input.widget),
creatorId: ctx.session.user.id,
});
if (input.secrets.length > 0) {
await ctx.db.insert(customWidgetSecrets).values(
input.secrets.map((secret) => ({
definitionId: id,
sourceId: secret.sourceId,
kind: secret.kind,
encryptedValue: encryptSecret(secret.value),
updatedAt: new Date(),
})),
);
}
logger.info("Imported custom widget definition", { id, name: input.widget.name });
return { id };
}),
};