fix(custom-widgets): finish v2 hardening

This commit is contained in:
Thomas Camlong committed 2026-07-20 23:39:57 +02:00
1 parent 7c66c912e7
commit 1c02678a7e
97 files changed
+2833 -12393

No files matched your search

@@ -13,6 +13,7 @@ import {
} from "@homarr/custom-widgets/core";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
import { insertCustomWidgetDefinition } from "./definition-insert";
import { managementQueryProcedures } from "./management-queries";
import { metadataProcedures } from "./metadata-procedures";
import { previewActionProcedures } from "./preview-action-procedures";
@@ -48,35 +49,7 @@ export const customWidgetRouter = createTRPCRouter({
}
const definition = customWidgetDefinitionSchema.parse(candidate);
assertSecretSources(definition.sources, secrets);
const id = createId();
const definitionRow = {
id,
...serializeCustomWidgetDefinition(definition),
creatorId: ctx.session.user.id,
};
const updatedAt = new Date();
const secretRows = secrets.map((secret) => ({
definitionId: id,
sourceId: secret.sourceId,
kind: secret.kind,
encryptedValue: encryptSecret(secret.value),
updatedAt,
}));
await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) {
await db.transaction(async (transaction) => {
await transaction.insert(schema.customWidgetDefinitions).values(definitionRow);
if (secretRows.length > 0) await transaction.insert(schema.customWidgetSecrets).values(secretRows);
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction.insert(customWidgetDefinitions).values(definitionRow).run();
if (secretRows.length > 0) transaction.insert(customWidgetSecrets).values(secretRows).run();
});
},
});
const id = await insertCustomWidgetDefinition(ctx.db, definition, ctx.session.user.id, secrets);
logger.info("Created custom widget definition", { id, name: definition.name });
return { id };
}),
@@ -0,0 +1,43 @@
import { createId } from "@homarr/common";
import { encryptSecret } from "@homarr/common/server";
import type { Database } from "@homarr/db";
import { handleTransactionsAsync } from "@homarr/db";
import { customWidgetDefinitions, customWidgetSecrets } from "@homarr/db/schema";
import type { CustomWidgetCreateInput, HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core";
import { serializeCustomWidgetDefinition } from "./stored-definition";
export async function insertCustomWidgetDefinition(
db: Database,
definition: HomarrCustomWidgetV2,
creatorId: string,
secrets: CustomWidgetCreateInput["secrets"],
) {
const id = createId();
const definitionRow = { id, ...serializeCustomWidgetDefinition(definition), creatorId };
const updatedAt = new Date();
const secretRows = secrets.map((secret) => ({
definitionId: id,
sourceId: secret.sourceId,
kind: secret.kind,
encryptedValue: encryptSecret(secret.value),
updatedAt,
}));
await handleTransactionsAsync(db, {
async handleAsync(database, schema) {
await database.transaction(async (transaction) => {
await transaction.insert(schema.customWidgetDefinitions).values(definitionRow);
if (secretRows.length > 0) await transaction.insert(schema.customWidgetSecrets).values(secretRows);
});
},
handleSync(database) {
database.transaction((transaction) => {
transaction.insert(customWidgetDefinitions).values(definitionRow).run();
if (secretRows.length > 0) transaction.insert(customWidgetSecrets).values(secretRows).run();
});
},
});
return id;
}
@@ -22,6 +22,7 @@ export const metadataProcedures = {
"homarr://custom-widgets/components",
"homarr://custom-widgets/skill",
],
httpResources: ["/api/custom-widgets/schema", "/api/custom-widgets/components", "/api/custom-widgets/skill"],
})),
getSkill: publicProcedure
@@ -1,16 +1,15 @@
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, handleTransactionsAsync } from "@homarr/db";
import { customWidgetDefinitions, customWidgetSecrets } from "@homarr/db/schema";
import { eq } from "@homarr/db";
import { customWidgetDefinitions } from "@homarr/db/schema";
import { customWidgetImportSchema, customWidgetSecretsInputSchema } from "@homarr/custom-widgets/core";
import { permissionRequiredProcedure } from "../../trpc";
import { insertCustomWidgetDefinition } from "./definition-insert";
import { assertSecretSources } from "./secret-policy";
import { parseStoredCustomWidgetDefinition, serializeCustomWidgetDefinition } from "./stored-definition";
import { parseStoredCustomWidgetDefinition } from "./stored-definition";
const manageProcedure = permissionRequiredProcedure.requiresPermission("custom-widget-manage");
const logger = createLogger({ module: "custom-widget" });
@@ -34,35 +33,7 @@ export const transferProcedures = {
});
}
assertSecretSources(input.widget.sources, input.secrets);
const id = createId();
const definitionRow = {
id,
...serializeCustomWidgetDefinition(input.widget),
creatorId: ctx.session.user.id,
};
const updatedAt = new Date();
const secretRows = input.secrets.map((secret) => ({
definitionId: id,
sourceId: secret.sourceId,
kind: secret.kind,
encryptedValue: encryptSecret(secret.value),
updatedAt,
}));
await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) {
await db.transaction(async (transaction) => {
await transaction.insert(schema.customWidgetDefinitions).values(definitionRow);
if (secretRows.length > 0) await transaction.insert(schema.customWidgetSecrets).values(secretRows);
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction.insert(customWidgetDefinitions).values(definitionRow).run();
if (secretRows.length > 0) transaction.insert(customWidgetSecrets).values(secretRows).run();
});
},
});
const id = await insertCustomWidgetDefinition(ctx.db, input.widget, ctx.session.user.id, input.secrets);
logger.info("Imported custom widget definition", { id, name: input.widget.name });
return { id };
}),