feat: add custom widget flow workbench

This commit is contained in:
ajnart committed 2026-09-12 22:51:46 +02:00
1 parent c32a5317ca
commit a6bdbbb4ea
259 files changed
+23875 -1293

No files matched your search

@@ -0,0 +1,44 @@
/** Keep public schema metadata honest. Full runtime validation also runs in
* Homarr's shared validator before publication, preview, and installation. */
const validateSubmissionSchema = (record) => {
if (record.getString("type") === "customCss") {
if (record.getString("widgetSchema") !== "homarr-custom-css-v1") {
throw new BadRequestError("CSS submission schema does not match its type");
}
return;
}
let widget;
try {
widget = JSON.parse(record.getString("content"));
} catch {
throw new BadRequestError("Widget content must be JSON");
}
if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
throw new BadRequestError("Widget content must be an object");
}
if (widget.$schema !== "homarr-custom-widget-v2" && widget.$schema !== "homarr-custom-widget-v3") {
throw new BadRequestError("Unsupported widget schema");
}
if (widget.$schema !== record.getString("widgetSchema")) {
throw new BadRequestError("Widget schema metadata must match its content");
}
if (widget.$schema === "homarr-custom-widget-v2" && widget.extensions !== undefined) {
throw new BadRequestError("Widget extensions require homarr-custom-widget-v3");
}
const allowedFields = [
"$schema",
"name",
"description",
"iconUrl",
"sources",
"requests",
"options",
"template",
"extensions",
];
if (Object.keys(widget).some((key) => !allowedFields.includes(key))) {
throw new BadRequestError("Widget content contains unsupported fields");
}
};
module.exports = { validateSubmissionSchema };
+9
View File
@@ -58,6 +58,8 @@ routerAdd("GET", "/workshop/{id}", workshopDetailPage);
routerAdd("GET", "/workshop/{id}/{$}", workshopDetailPage);
onRecordCreateRequest((event) => {
const { validateSubmissionSchema } = require(`${__hooks}/workshop-content.js`);
validateSubmissionSchema(event.record);
event.record.set("revision", 1);
event.record.set("expectedRevision", 0);
event.record.set("changelog", "");
@@ -68,6 +70,13 @@ onRecordCreateRequest((event) => {
onRecordUpdateRequest((event) => {
const { rejectRequest } = require(`${__hooks}/workshop-utils.js`);
const original = event.record.original();
if (
original.getString("content") !== event.record.getString("content") ||
original.getString("widgetSchema") !== event.record.getString("widgetSchema")
) {
const { validateSubmissionSchema } = require(`${__hooks}/workshop-content.js`);
validateSubmissionSchema(event.record);
}
const currentRevision = original.getInt("revision");
const expectedRevision = event.record.getInt("expectedRevision");
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1) {
@@ -0,0 +1,27 @@
/// <reference path="../pb_data/types.d.ts" />
const identityUnchanged = "@request.body.author:changed = false && @request.body.type:changed = false";
const schemaUnchanged = "@request.body.widgetSchema:changed = false";
const supportedWidgetTransition =
"type = 'customWidget' && " +
"(widgetSchema = 'homarr-custom-widget-v2' || widgetSchema = 'homarr-custom-widget-v3') && " +
"(@request.body.widgetSchema = 'homarr-custom-widget-v2' || @request.body.widgetSchema = 'homarr-custom-widget-v3')";
const moderatorUnchanged = ["title", "description", "content", "changelog", "screenshots"]
.map((field) => `@request.body.${field}:changed = false`)
.join(" && ");
const submissionRule = (authorSchemaRule) =>
`(author = @request.auth.id && ${identityUnchanged} && (${authorSchemaRule})) || ` +
`(@request.auth.isAdmin = true && author != @request.auth.id && ${identityUnchanged} && ${schemaUnchanged} && ${moderatorUnchanged})`;
migrate(
(app) => {
const submissions = app.findCollectionByNameOrId("submissions");
submissions.updateRule = submissionRule(`${schemaUnchanged} || (${supportedWidgetTransition})`);
app.save(submissions);
},
(app) => {
const submissions = app.findCollectionByNameOrId("submissions");
submissions.updateRule = submissionRule(schemaUnchanged);
app.save(submissions);
},
);