/* Grant a Dolibarr permission to the sandbox write agent, through the admin UI. The REST API cannot do this (the write agent is non-admin by design), and the permission set it ships with is deliberately narrow: invoices, thirdparties, payments. Anything beyond that is an explicit, auditable grant — which is what this script performs, one right at a time, on the sandbox only. Run: DOLIBARR_ADDRESS=https://erp-sandbox.arcodange.lab \ deno run -A test/grantAgentRight.ts --user-id 4 --match "proposition" */ import "load_dotenv"; import { chromium } from "playwright"; import login from "./scripts/login.ts"; import { assertSandbox } from "./scripts/guard.ts"; const argv = Deno.args; const pick = (f: string, d: string) => argv.includes(f) ? argv[argv.indexOf(f) + 1] : d; const userId = pick("--user-id", "4"); const match = pick("--match", ""); const dryRun = argv.includes("--dry-run"); const revoke = argv.includes("--revoke"); const shotDir = Deno.env.get("SHOT_DIR") ?? "/tmp"; if (!match) { console.error("--match is required"); Deno.exit(2); } const dolibarrAddress = assertSandbox(); console.log(`target: ${dolibarrAddress} (guard passed)`); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ locale: "fr-FR" }); const page = await context.newPage(); try { await login.doAdminLogin({ page, dolibarrAddress, adminCredentials: { username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined", password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined", }, }); await page.goto(`${dolibarrAddress}/user/perms.php?id=${userId}`); const who = await page.locator("h1, .titre, .nowrap").first() .textContent({ timeout: 5000 }).catch(() => "(title not found)"); console.log(`permissions page for: ${who?.trim()}`); console.log(`url: ${page.url()}`); // Rows whose label matches, that are not already granted (a granted row shows // the "remove" link instead of the "add" one). const rows = page.locator("tr", { has: page.locator("td", { hasText: new RegExp(match, "i") }), }); const total = await rows.count(); console.log(`rows matching /${match}/i: ${total}`); let granted = 0; for (let i = 0; i < total; i++) { const row = rows.nth(i); const label = (await row.textContent())?.replace(/\s+/g, " ").trim().slice(0, 90); const action = revoke ? "delrights" : "addrights"; const addLink = row.locator(`a[href*="action=${action}"]`); if (await addLink.count() === 0) { console.log(` [skip, already ${revoke ? "revoked" : "granted"} or not actionable] ${label}`); continue; } if (dryRun) { console.log(` [dry-run would ${revoke ? "revoke" : "grant"}] ${label}`); continue; } await addLink.first().click(); await page.waitForLoadState("networkidle"); granted++; console.log(` [${revoke ? "revoked" : "granted"}] ${label}`); // The DOM is rebuilt after each grant — re-resolve on the next iteration. await page.goto(`${dolibarrAddress}/user/perms.php?id=${userId}`); } console.log(`${revoke ? "revoked" : "granted"} ${granted} right(s)`); await page.screenshot({ path: `${shotDir}/perms.png`, fullPage: true }); } finally { await context.close(); await browser.close(); }