Skip to content

Commit 047c58a

Browse files
ulsklycUlas Kalayci
authored andcommitted
fix(documents): linked receipts follow the documents right (ulsklyc#1424)
* fix(documents): linked receipts follow the documents right Budget entries, shared expenses, settlement proofs and inventory items delivered the name and id of every linked document to anyone who could read the owning module, also with `documents: none` and to tokens without documents:read. The documents-read decision (member right and token scope) now lives once, as mayReadDocuments()/documentViewer() in services/document-links.js, and housekeeping uses it too. Without it links come masked (id and name null), the item history drops them, a settlement's proof_document_id is null, and any non-empty attachment_document_ids or proof_document_id is the same 403 for every id, before visibility and deletion lock. The detail views show "Attached" instead of a link into nothing. Refs ulsklyc#1358 * test(housekeeping): unknown, deleted and locked receipt ids refuse alike A new receipt link without documents access must answer the same 403 for an unknown or deleted id and for a document in the deletion window as for an existing one. Goes red when the refusal in assertDocumentLinkTargetsAvailable() moves behind visibility and lock. Refs ulsklyc#1358 --------- Co-authored-by: Ulas Kalayci <ulas.kalayci@icloud.com>
1 parent acd62dc commit 047c58a

44 files changed

Lines changed: 667 additions & 136 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
181181
`documents:read` scope for them, and `PUT /api/v1/housekeeping/visits/{id}` answers 403 when it
182182
would replace a receipt you cannot see or link one without access to documents. (#1358)
183183

184+
- **Receipts on budget entries, shared expenses and inventory items no longer name documents you
185+
may not read.** Their API sent the file name and document number of every linked receipt to
186+
anyone who could open the budget or the inventory, also to members without access to documents
187+
and to API tokens without a documents scope. Without access to documents a receipt now only says
188+
that it is there: the detail view shows "Attached" where the name was, and the inventory no
189+
longer shows a link that leads nowhere or lists the document in an item's history. Linking a
190+
receipt or a payment proof needs access to documents, and existing receipts stay when such a
191+
member saves the entry. For API clients `attachments[].document_id`, `name`, `original_name`,
192+
`mime_type` and `file_size` are `null` without access to the documents module (for API tokens a
193+
`documents:read` scope), a settlement's `proof_document_id` is `null` unless you may read that
194+
document, and a non-empty `attachment_document_ids` or a `proof_document_id` is answered with the
195+
same 403 for every id. (#1358)
196+
184197
- **An edited shared expense keeps counting after the editor's account is deleted.** When a group
185198
owner or admin edited someone else's expense and that editor's account was later deleted, the
186199
expense stayed in the list but silently dropped out of every balance. Edits now leave the expense

docs/SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,7 @@ Recurring entries are materialised on demand for the month being viewed. **Non-v
16251625

16261626
**Monthly summary & expenses-only view:** the Budget tab heads each month with three summary cards - income, expenses and the net balance (income − expenses). When a month records only expenses (no income), the balance card renders neutral instead of red, because a bare `−expenses` net misreads as being "in the red" (#504). A per-device **Expenses only** toggle (persisted client-side in `localStorage`, no server preference) collapses the summary to the single expenses card and hides income and the net, for pure expense tracking; the transaction list, category chart and CSV export are unaffected.
16271627

1628-
**Receipts (migration v112, #583):** an entry can carry documents from the Documents module as receipts — link an existing document or upload a new file straight from the entry modal. Receipts live in `budget_entry_attachments` (`entry_id`, `document_id`, `created_by`, `UNIQUE(entry_id, document_id)`), so one purchase may hold several (till receipt plus invoice plus warranty). The file itself always belongs to the Documents module: deleting the entry drops the link, not the document; deleting the document drops the link and leaves the entry. **Document visibility keeps applying** — a receipt filed as private stays invisible to everyone else even when it hangs on a shared entry, and there is no admin bypass. You can only link what you may see, and saving an entry only removes the links you can see, so another member's private receipt survives your edit. Receipts belong to the single entry, not to a recurring series: updating a series leaves them untouched (each month's bill has its own receipt). The API takes `attachment_document_ids` on create/update — omitting the field leaves existing receipts alone — and returns the visible ones as `attachments`.
1628+
**Receipts (migration v112, #583):** an entry can carry documents from the Documents module as receipts — link an existing document or upload a new file straight from the entry modal. Receipts live in `budget_entry_attachments` (`entry_id`, `document_id`, `created_by`, `UNIQUE(entry_id, document_id)`), so one purchase may hold several (till receipt plus invoice plus warranty). The file itself always belongs to the Documents module: deleting the entry drops the link, not the document; deleting the document drops the link and leaves the entry. **Document visibility keeps applying** — a receipt filed as private stays invisible to everyone else even when it hangs on a shared entry, and there is no admin bypass. You can only link what you may see, and saving an entry only removes the links you can see, so another member's private receipt survives your edit. Receipts belong to the single entry, not to a recurring series: updating a series leaves them untouched (each month's bill has its own receipt). The API takes `attachment_document_ids` on create/update — omitting the field leaves existing receipts alone — and returns the visible ones as `attachments`. **Name and id belong to the Documents module (#1358):** without read access there (member right `documents: none`, or an API token without `documents:read`) each link comes masked - `document_id`, `name`, `original_name`, `mime_type` and `file_size` are `null` - and a non-empty `attachment_document_ids` is the same 403 for every id, while an empty list keeps everything. The same rule, from `mayReadDocuments()` in `server/services/document-links.js`, covers shared-expense receipts and payment proofs, inventory documents and the housekeeping receipt.
16291629

16301630
**Personal vs. shared budgets (migration v88):** every budget entry (and loan and subscription) carries an immutable `owner_id` (= the creator) and a `visibility` of `shared` (all members) or `private` (owner only). A household-wide **budget mode** setting (`budget_mode` in `sync_config`, `shared` by default, admin-gated) decides whether visibility is enforced at all: in `shared` mode everyone sees everything (the prior, fully backward-compatible behaviour); in `personal` mode the Budget page gains a **My budget / Household** view switcher — *My budget* shows what you own, *Household* shows the shared pot (`visibility = 'shared'`). Enforcement is **server-side on every read path** (entry list, summary, statistics, CSV export, accounts balances, loans, subscriptions, dashboard widget) with **no admin bypass** — a private entry stays hidden even from an admin. Write access to an object requires ownership (owner or creator), also with no admin bypass. New entries default to `private` in personal mode and `shared` in shared mode. This is the lean variant of the split-budget request (#476/#505): a shared entry is one whole row with a "Household" badge, without materialised per-person split rows.
16311631

public/components/document-attach.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,21 @@ export function renderDocumentAttachField({
144144
*/
145145
export function attachmentLinksNode(attachments = []) {
146146
if (pathAccess('/documents') === 'none') return null;
147-
const docs = (attachments || []).filter((a) => a?.document_id);
148-
if (!docs.length) return null;
147+
const list = (attachments || []).filter(Boolean);
148+
const docs = list.filter((a) => a.document_id);
149+
// Ein Beleg, den der Server nicht nennt (#1358): die Zeile kommt mit
150+
// `document_id: null` und ohne Namen. Wie beim Beleg eines Einsatzes steht
151+
// dann ein ruhiges "Vorhanden" statt eines Links, der ins Leere ginge.
152+
const hidden = list.length > docs.length;
153+
if (!docs.length && !hidden) return null;
149154
const wrap = document.createElement('div');
150155
wrap.className = 'detail-chips';
156+
if (hidden) {
157+
const present = document.createElement('span');
158+
present.className = 'detail-attachment detail-attachment--present';
159+
present.textContent = t('documentAttach.presentHidden');
160+
wrap.appendChild(present);
161+
}
151162
for (const doc of docs) {
152163
const name = doc.name || doc.original_name || '';
153164
const link = document.createElement('a');

public/locales/ar.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5348,7 +5348,8 @@
53485348
"confirmSelection": "إضافة المحدد",
53495349
"openAction": "فتح {{name}}",
53505350
"limitReached": "بحد أقصى {{count}} مستندات.",
5351-
"limitReached_one": "مستند واحد بحد أقصى."
5351+
"limitReached_one": "مستند واحد بحد أقصى.",
5352+
"presentHidden": "مرفق"
53525353
},
53535354
"join": {
53545355
"title": "إعداد حسابك",

public/locales/cs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5348,7 +5348,8 @@
53485348
"confirmSelection": "Přidat vybrané",
53495349
"openAction": "Otevřít {{name}}",
53505350
"limitReached": "Nejvýše dokumentů: {{count}}.",
5351-
"limitReached_one": "Nejvýše dokumentů: {{count}}."
5351+
"limitReached_one": "Nejvýše dokumentů: {{count}}.",
5352+
"presentHidden": "Přiložen"
53525353
},
53535354
"join": {
53545355
"title": "Nastavení účtu",

public/locales/de.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5348,7 +5348,8 @@
53485348
"confirmSelection": "Übernehmen",
53495349
"openAction": "{{name}} öffnen",
53505350
"limitReached": "Höchstens {{count}} Dokumente möglich.",
5351-
"limitReached_one": "Höchstens {{count}} Dokument möglich."
5351+
"limitReached_one": "Höchstens {{count}} Dokument möglich.",
5352+
"presentHidden": "Vorhanden"
53525353
},
53535354
"join": {
53545355
"title": "Konto einrichten",

public/locales/el.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5348,7 +5348,8 @@
53485348
"confirmSelection": "Προσθήκη επιλογής",
53495349
"openAction": "Άνοιγμα {{name}}",
53505350
"limitReached": "Το πολύ {{count}} έγγραφα.",
5351-
"limitReached_one": "Το πολύ {{count}} έγγραφο."
5351+
"limitReached_one": "Το πολύ {{count}} έγγραφο.",
5352+
"presentHidden": "Επισυνάπτεται"
53525353
},
53535354
"join": {
53545355
"title": "Ρύθμιση λογαριασμού",

public/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5348,7 +5348,8 @@
53485348
"confirmSelection": "Add selected",
53495349
"openAction": "Open {{name}}",
53505350
"limitReached": "At most {{count}} documents.",
5351-
"limitReached_one": "At most {{count}} document."
5351+
"limitReached_one": "At most {{count}} document.",
5352+
"presentHidden": "Attached"
53525353
},
53535354
"join": {
53545355
"title": "Set up your account",

public/locales/es.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5348,7 +5348,8 @@
53485348
"confirmSelection": "Añadir selección",
53495349
"openAction": "Abrir {{name}}",
53505350
"limitReached": "Como máximo {{count}} documentos.",
5351-
"limitReached_one": "Como máximo {{count}} documento."
5351+
"limitReached_one": "Como máximo {{count}} documento.",
5352+
"presentHidden": "Adjunto"
53525353
},
53535354
"join": {
53545355
"title": "Configura tu cuenta",

public/locales/fa.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5348,7 +5348,8 @@
53485348
"confirmSelection": "افزودن انتخاب‌شده‌ها",
53495349
"openAction": "باز کردن {{name}}",
53505350
"limitReached": "حداکثر {{count}} سند.",
5351-
"limitReached_one": "حداکثر {{count}} سند."
5351+
"limitReached_one": "حداکثر {{count}} سند.",
5352+
"presentHidden": "پیوست شده"
53525353
},
53535354
"join": {
53545355
"title": "راه‌اندازی حساب",

0 commit comments

Comments
 (0)