104 lines
5.4 KiB
JavaScript
104 lines
5.4 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const {
|
|
issueConfirmation,
|
|
consumeConfirmation,
|
|
isDestructivePath
|
|
} = require("../src/services/destructive-confirm");
|
|
|
|
const root = path.join(__dirname, "..");
|
|
const destructivePath = /(?:^|\/)(?:delete|remove|clear|reset|renew|uninstall|cleanup|archive|revoke|unlink|unset|revert)(?:\/|$)/i;
|
|
|
|
function filesBelow(directory, extension, output = []) {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const target = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) filesBelow(target, extension, output);
|
|
else if (target.endsWith(extension)) output.push(target);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function maskEjs(source) {
|
|
return source.replace(/<%[\s\S]*?%>/g, "EJS");
|
|
}
|
|
|
|
function attribute(tag, name) {
|
|
return tag.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i"))?.[1] || "";
|
|
}
|
|
|
|
function requireConfirmationCopy(tag, file) {
|
|
for (const attributeName of ["data-confirm-mode", "data-confirm-title", "data-confirm-text", "data-confirm-label"]) {
|
|
assert(new RegExp(`\\b${attributeName}(?:\\s*=|\\s|>)`, "i").test(tag), `${path.relative(root, file)} has an unconnected destructive control missing ${attributeName}: ${tag}`);
|
|
}
|
|
assert.equal(attribute(tag, "data-confirm-mode"), "modal", `${path.relative(root, file)} must use the shared timed modal`);
|
|
}
|
|
|
|
function verifyViews() {
|
|
const files = [
|
|
...filesBelow(path.join(root, "src", "web", "views"), ".ejs"),
|
|
...filesBelow(path.join(root, "plugins"), ".ejs")
|
|
];
|
|
let controls = 0;
|
|
for (const file of files) {
|
|
const source = maskEjs(fs.readFileSync(file, "utf8"));
|
|
assert(!/name\s*=\s*["']action["'][^>]*value\s*=\s*["']delete["']/i.test(source), `${path.relative(root, file)} hides a delete behind a generic POST route; use an explicit /delete endpoint`);
|
|
for (const tag of source.match(/<(?:form|button)\b[^>]*>/gi) || []) {
|
|
const action = attribute(tag, "action") || attribute(tag, "formaction");
|
|
const confirmAction = attribute(tag, "data-confirm-action");
|
|
if (!destructivePath.test(action) && !destructivePath.test(confirmAction)) continue;
|
|
requireConfirmationCopy(tag, file);
|
|
controls += 1;
|
|
}
|
|
}
|
|
assert(controls >= 30, `Expected broad destructive-control coverage, found only ${controls}`);
|
|
return controls;
|
|
}
|
|
|
|
function verifySharedInfrastructure() {
|
|
for (const action of [
|
|
"/admin/commands/1/delete",
|
|
"/admin/navigation/reset",
|
|
"/admin/updates/core/revert",
|
|
"/plugins/auto-vc/settings/remove"
|
|
]) {
|
|
assert.equal(isDestructivePath(action), true, `Server does not protect ${action}`);
|
|
}
|
|
|
|
const req = {
|
|
session: { user: { id: "admin-user", isAdmin: true } },
|
|
body: {},
|
|
get() { return null; }
|
|
};
|
|
const issued = issueConfirmation(req, "/admin/updates/core/revert");
|
|
req.session.destructive_confirmations[issued.token].not_before = Date.now() - 1;
|
|
assert.equal(consumeConfirmation(req, "/admin/updates/core/revert", issued.token).valid, true);
|
|
|
|
const appScript = fs.readFileSync(path.join(root, "src", "web", "public", "app.js"), "utf8");
|
|
assert(appScript.includes("submitter?.dataset?.confirmMode"), "Submit-button confirmation metadata is ignored");
|
|
assert(appScript.includes("submitter?.dataset?.confirmLabel || form.dataset.confirmLabel"), "The clicked submit button cannot override generic confirmation copy");
|
|
assert(appScript.includes('form.querySelector(\'input[name="confirmation_token"]\')'), "Timed form confirmation tokens are not attached to submitted forms");
|
|
assert(appScript.includes("const response = await fetch(action, requestOptions)"), "Async update requests do not submit through the shared confirmed form payload");
|
|
assert(!appScript.includes("window.LumiConfirm.destructiveFetch(form.action"), "Timed update forms request a second confirmation instead of reusing the form token");
|
|
assert(appScript.includes('state.confirmButton.removeEventListener("click", state.onConfirm)'), "Cancelled or expired confirmations leave stale modal submissions behind");
|
|
assert(appScript.includes("state.onConfirm = onConfirm"), "Timed form confirmations do not retain their listener for cleanup");
|
|
|
|
const toolManagerScript = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "public", "tool-manager.js"), "utf8");
|
|
for (const field of ["confirmMode", "confirmTitle", "confirmText", "confirmLabel"]) {
|
|
assert(toolManagerScript.includes(`form.dataset.${field}`), `Dynamic AI tool deletion is missing ${field}`);
|
|
}
|
|
|
|
const autoVcView = fs.readFileSync(path.join(root, "plugins", "auto-vc", "views", "auto-vc.ejs"), "utf8");
|
|
assert(autoVcView.includes('settingsForm.action = "/plugins/auto-vc/settings/remove"'), "Auto VC lobby deletion does not switch to its protected route");
|
|
|
|
const improvementView = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "views", "improvement-center.ejs"), "utf8");
|
|
assert(improvementView.includes("/reviews/<%= review.id %>/delete"));
|
|
assert(improvementView.includes("/corrections/<%= entry.id %>/delete"));
|
|
const improvementScript = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "public", "improvement-center.js"), "utf8");
|
|
assert.equal(improvementScript.includes("window.confirm"), false, "Improvement Center still uses the legacy browser confirmation");
|
|
}
|
|
|
|
const controls = verifyViews();
|
|
verifySharedInfrastructure();
|
|
console.log(`Destructive action verification passed (${controls} controls).`);
|