Skip to content

Upload invoice PDFs into a private Directus folder - #287

Merged
Jan0707 merged 4 commits into
mainfrom
invoice-files-private-folder
Oct 21, 2026
Merged

Jan0707 merged 4 commits into
mainfrom
invoice-files-private-folder

Conversation

@claude

@claude claude Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Requested by Richard Hartmann · Slack thread

Before: invoice PDFs (Rechnung, Rechnungsberichtigung, Stornorechnung) land in the public file library and can be downloaded by anyone with the link (/assets/<file-id>).

After: new invoices land in a dedicated folder that the Public role cannot read.

How

Deployment

  1. File Library → create a folder (e.g. Rechnungen); its id is the last URL segment. No subfolders.
  2. Set INVOICE_FILES_FOLDER=<folder-id> in the Directus environment.
  3. Deploy / restart Directus.
  4. Settings → Access Policies → Public → directus_files → Read → Use Custom → Item Permissions:
    { "_or": [{ "folder": { "_null": true } }, { "folder": { "_neq": "<folder-id>" } }] }
    The _null branch is required: Directus 11 runs _neq as SQL NOT folder = '<id>', which excludes files without a folder, so _neq alone would hide all root-level files (website images). Apply the same filter to any other policy that grants Public read on directus_files.
  5. Move existing invoice PDFs into the folder (File Library → search Rechnung → select → Move to Folder). File ids do not change, so existing references stay valid.
  6. Check in an incognito window: /assets/<invoice-file-id> returns 403, /files lists no invoices, a regular public image still loads.

Generated by Claude Code

Invoice PDFs (Rechnung, Rechnungsberichtigung, Stornorechnung) were uploaded
into the general file library, which the Public role can read, so anyone with
a file id could download them via /assets/<id>.

New invoice documents are now uploaded into the folder named by the
INVOICE_FILES_FOLDER env var; the Public policy excludes that folder (setup
steps in docs/tickets.md). If the variable is unset, invoicing keeps working,
but a warning is logged and a Slack alert is sent once per process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
programmierbar-website Ready Ready Preview Oct 21, 2026 8:59am UTC

Request Review

@claude

claude Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

The failing check is Vercel (preview deployment). This PR is not the cause: it changes only directus-cms/ and docs/, and its nuxt-app/ and shared-code/ trees are identical to main at 551765f.

  • main at 551765f deployed fine yesterday (2026-10-20 15:47 UTC).
  • Running nuxt build (Node 24) locally on that identical tree fails today, in prerendering against the live CMS: 14 routes return 500, e.g. Cannot read properties of null (reading 'first_name') / (reading 'split') on podcast pages.
  • The cause is CMS data, not code: the public API now returns draft and test podcasts that have no speaker or cover image, and the build prerenders them.

So a fresh main deploy would fail the same way.


Generated by Claude Code

@Jan0707

Jan0707 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

@claude What happens if the provided folder id is invalid and the folder cannot be found / used as a target destination ?

@claude

claude Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Update: this describes 01b3520. Since 10476fb, an invalid folder no longer aborts anything. The upload is retried without the folder (stored publicly, warning plus Slack alert), and both the paid hook and invoice-lifecycle carry on normally. Only a failing retry still behaves as described below.

What happens if the provided folder id is invalid and the folder cannot be found / used as a target destination?

I checked this against the @directus/api 35.2.0 source and our call sites. Short answer: the upload fails cleanly, nothing is written, and the error is only logged.

  • Why nothing is written: FilesService.uploadOne inserts the directus_files row first, then writes to storage. directus_files.folder has a foreign key to directus_folders, so a bad id makes the insert fail. Storage is never touched, so there is no orphaned file or row.
    • A valid but unknown UUID is a foreign key violation and throws InvalidForeignKeyError.
    • A malformed id fails Postgres' uuid cast and throws the raw DB error.
  • Paid hook: the invoice is generated before tickets are created, so the outer try/catch catches the error. It is logged with logger.error only: no tickets, no confirmation email, no Slack alert. The order stays paid without an invoice.
  • invoice-lifecycle (regenerate / correction / cancellation): responds 500 Failed to run invoice <action> and logs the error. No document row is created or changed.

Following the "no runtime guards for once-verified setup" rule, I'm not adding validation. After the deploy, verify once:

  • run regenerate on an unsent test invoice (or wait for the next real invoice) and check that the PDF shows up in the invoice folder in the File Library;
  • open /assets/<that-file-id> in an incognito window and check it returns 403.

Separate follow-up idea, not part of this PR: send a Slack alert when the paid hook aborts, since today that failure only shows up in the logs.


Generated by Claude Code

@Jan0707

Jan0707 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

@claude directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/secure-invoice-folder.ts Seems to imply that invoices are indeed created but stored publicly if the folder is missing.

That way, the user-flow would not be broken, and we would simply need to remedy this after the fact (that's what the slack message is for)

@Jan0707
Jan0707 marked this pull request as ready for review October 21, 2026 08:46
Copilot AI balanced review requested due to automatic review settings October 21, 2026 08:46
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T08:48:57.146663Z 01b3520 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01b3520fe7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Missing folder configuration still allows customer invoices to be uploaded into the publicly readable library.

1 open issue
What changed in this PR

Routes ticket invoice PDFs into a configured Directus folder, with privacy enforced through deployment permissions.

Changes:

  • Passes the invoice folder through all invoice upload paths.
  • Adds missing-configuration alerts and folder propagation tests.
  • Documents access restrictions and migration of existing invoices.
File Description
docs/​tickets.md Documents private-folder setup and verification.
directus-cms/​extensions/​directus-extension-programmierbar-bundle/​src/​ticket-order-processing/​index.ts Passes the folder for original invoices.
directus-cms/​extensions/​directus-extension-programmierbar-bundle/​src/​ticket-order-processing/​__tests__/​index.test.ts Tests original-invoice folder propagation.
directus-cms/​extensions/​directus-extension-programmierbar-bundle/​src/​shared/​secure-invoice-folder.ts Resolves configuration and alerts when missing.
directus-cms/​extensions/​directus-extension-programmierbar-bundle/​src/​shared/​invoice-service.ts Adds folder metadata to uploads.
directus-cms/​extensions/​directus-extension-programmierbar-bundle/​src/​shared/​__tests__/​secure-invoice-folder.test.ts Tests configuration, alerts, and upload metadata.
directus-cms/​extensions/​directus-extension-programmierbar-bundle/​src/​invoice-lifecycle/​index.ts Sets the folder for lifecycle uploads.
directus-cms/​extensions/​directus-extension-programmierbar-bundle/​src/​invoice-lifecycle/​__tests__/​index.test.ts Tests folder propagation across lifecycle operations.
directus-cms/​.env.example Documents the folder environment variable.

Review effort: Balanced

If uploading an invoice PDF into INVOICE_FILES_FOLDER fails (e.g. an
unknown or malformed folder id), retry once without the folder so the
invoice flow keeps working, log a warning with the invoice number and
alert Slack, like an unset folder. Without a folder there is no retry; a
failing retry throws as before.

Both alerts are now retried with the next invoice when the Slack message
could not be delivered, instead of being suppressed for the process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
@claude

claude Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

#287 (comment)

The fallback is in 10476fb. An invalid INVOICE_FILES_FOLDER now behaves like a missing one: if the upload into the folder fails, it is retried once without the folder, so the PDF is stored publicly. A warning with the invoice number is logged, Slack is alerted with the invoice number and the folder value, and the invoice flow continues.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unrelated upload failures can publish invoices through the fallback even when private-folder configuration is correct.

2 open issues

Review effort: Balanced

The fallback to the public file library dropped the folder after any
upload error, so a transient storage or DB failure followed by a
successful retry would have published a correctly configured invoice.
Fall back only when Directus rejected the folder itself (an
InvalidForeignKeyError for `folder`, or the Postgres invalid-uuid error
for the configured value); rethrow every other error unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
Other open PRs log unrelated render warnings (e.g. a missing
Leistungsdatum) through the same logger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Invoice confidentiality depends on deployment-time Directus permissions that require human verification of anonymous access.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Invoice confidentiality depends on manually configured Directus permissions that require verification in the deployed environment.

No open issues 🎉

Review effort: Balanced

@Jan0707
Jan0707 merged commit a51e7d2 into main Oct 21, 2026
6 checks passed
@Jan0707
Jan0707 deleted the invoice-files-private-folder branch October 21, 2026 09:13

This branch was successfully deployed

1 active deployment
Preview — 278298a0 Deployed Oct 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants