feat(perms): drop dead document export permissions; seed the 12 survivors via migration

offers.export/orders.export/invoices.export gated nothing on the backend (PDF
routes enforce *.view); they only toggled frontend buttons and showed dead
role checkboxes. Migration deletes them (+ role grants) and idempotently seeds
the 12 enforced doc perms (offers/orders/invoices x view/create/edit/delete) +
admin grant, so a fresh prod DB has them (closes the seed-only drift gap).
Route reverts to orders.view; the 8 frontend *.export checks now use *.view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 21:19:38 +02:00
parent 3268cf2100
commit d445384408
10 changed files with 76 additions and 34 deletions

View File

@@ -1147,7 +1147,7 @@ export default function InvoiceDetail() {
</Box>
</Box>
<Box sx={headerActionsSx}>
{hasPermission("invoices.export") && (
{hasPermission("invoices.view") && (
<Button
onClick={() => handleViewPdf(invoice.language || "cs")}
variant="outlined"
@@ -1690,7 +1690,7 @@ export default function InvoiceDetail() {
</Box>
</Box>
<Box sx={headerActionsSx}>
{isEdit && invoice && hasPermission("invoices.export") && (
{isEdit && invoice && hasPermission("invoices.view") && (
<Button
onClick={() => handleViewPdf(invoice.language || "cs")}
variant="outlined"

View File

@@ -599,7 +599,7 @@ export default function Invoices() {
>
{inv.status === "paid" ? ViewIcon : EditIcon}
</IconButton>
{hasPermission("invoices.export") && (
{hasPermission("invoices.view") && (
<IconButton
size="small"
onClick={() => handlePdf(inv)}

View File

@@ -649,7 +649,7 @@ export default function IssuedOrderDetail() {
// Form is editable only in create mode or while draft/sent.
const editable = !isEdit || form.status === "draft" || form.status === "sent";
const canExport = hasPermission("orders.export");
const canExport = hasPermission("orders.view");
// ─── Totals (live) ───
const totals = useMemo(() => {

View File

@@ -261,7 +261,7 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) {
>
{ViewIcon}
</IconButton>
{hasPermission("orders.export") && (
{hasPermission("orders.view") && (
<IconButton
size="small"
onClick={() => handleExportPdf(o)}
@@ -356,7 +356,7 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) {
title="Zatím žádné vydané objednávky."
description={
hasPermission("orders.create")
? "Vytvořte první tlačítkem „Vytvořit objednávku vydanou“."
? "Vytvořte první tlačítkem „Vytvořit objednávku."
: undefined
}
/>

View File

@@ -1081,7 +1081,7 @@ export default function OfferDetail() {
</Box>
</Box>
<Box sx={headerActionsSx}>
{isEdit && hasPermission("offers.export") && (
{isEdit && hasPermission("offers.view") && (
<Button
onClick={handlePdf}
variant="outlined"

View File

@@ -695,7 +695,7 @@ export default function Offers() {
</IconButton>
)
)}
{hasPermission("offers.export") && (
{hasPermission("offers.view") && (
<IconButton
size="small"
onClick={() => handlePdf(q)}

View File

@@ -434,7 +434,7 @@ export default function OrderDetail() {
</Button>
)
)}
{hasPermission("orders.export") && (
{hasPermission("orders.view") && (
<Button
variant="outlined"
color="inherit"

View File

@@ -1,6 +1,6 @@
import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requireAnyPermission } from "../../middleware/auth";
import { requirePermission } from "../../middleware/auth";
import { parseId, success } from "../../utils/response";
import { localDateCzStr } from "../../utils/date";
import { htmlToPdf } from "../../utils/html-to-pdf";
@@ -821,12 +821,11 @@ ${indentCSS}
export default async function issuedOrdersPdfRoutes(fastify: FastifyInstance) {
fastify.get<{ Params: { id: string } }>(
"/:id",
// Mirror invoices-pdf (invoices.view): view-level access serves both the
// user-facing Export PDF and the ?save=1 NAS archive. Using requireAny so an
// order editor (who has orders.view) can trigger archival on save without
// also needing orders.export — otherwise the fire-and-forget archive 403s
// silently. orders.export holders keep access too.
{ preHandler: requireAnyPermission("orders.view", "orders.export") },
// Mirror invoices-pdf (invoices.view): view-level access serves BOTH the
// user-facing Export PDF and the ?save=1 NAS archive. There is no separate
// orders.export permission (it was dead — gated nothing — and was removed);
// orders.view is the single gate for both paths.
{ preHandler: requirePermission("orders.view") },
async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;