Papierkorb statt endgültigem Löschen, und Zugriffsregeln für jede Bild-Route

Löschen in der Bibliothek entfernte Zeile und Ergebnisdatei sofort. Ein
Fehlklick, und ein Ergebnis war weg — samt dem, was es gekostet hat.
Jetzt landet es im Papierkorb:

- weg aus Bibliothek, Freigabelinks, ZIPs, Auslieferung, NAS-Spiegel,
  Druckquellen und Druckverlauf; beim Wiederherstellen unverändert zurück
- „Rückgängig" im Hinweis, Papierkorb-Ansicht mit Resttagen,
  Wiederherstellen, endgültig löschen, leeren
- die Wartung räumt nach settings.trash_days (Standard 30, Admin ->
  Speicher) endgültig ab; 0 schaltet den Papierkorb aus
- ein Bild, das noch in der Warteschlange wartet, wird nicht mehr
  erzeugt (und nicht bezahlt)
- endgültig gelöscht wird nur, was kein anderes Bild mehr nutzt —
  Alternativen teilen ihre Quelle mit dem Original, auch Kombinieren-
  Quellen in source_paths (jsonb)

Beim Durchgehen jeder Abfrage auf items fiel auf: Nur die Datei-Route
prüfte, wer ein Bild sehen darf. Umbenennen, Löschen, Wiederholen,
Alternative, Weiterbearbeiten und die Auswahl-ZIP nahmen jede Kennung von
jedem Konto. Jetzt gilt eine Regel (src/lib/itemaccess.ts): sehen, was
einem gehört oder in der geteilten Bibliothek liegt, ändern nur Eigenes;
Admins alles. Für Till ändert sich nichts. Lea kann fremde Bilder in der
geteilten Bibliothek weiter sehen, aber nicht mehr löschen.

Dazu Favicon und Homebildschirm-Symbol, Hilfe und Changelog-Seite.

Geprüft gegen echtes Postgres mit zwei Konten und echten Dateien:
35 Ende-zu-Ende-Prüfungen bestanden, Oberfläche im Browser (Löschen,
Rückgängig, Papierkorb, Wiederherstellen, 390 px ohne Querlauf).
128 Tests, Typprüfung 0 Fehler.
This commit is contained in:
till committed 2026-10-10 06:38:42 +00:00
1 parent ee887cc29c
commit 28a6049548
33 files changed
+456 -40

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { objectKeysOf, daysLeft } from '../src/lib/trash.ts';
import { canRead, canWrite } from '../src/lib/itemaccess.ts';
test('Every stored file of an image is found, each once', () => {
assert.deepEqual(
objectKeysOf({ source_path: 'sources/a.png', result_path: 'results/a.png', thumb_path: 'thumbs/a.webp',
source_paths: ['sources/a.png', 'sources/b.png'] }),
['sources/a.png', 'results/a.png', 'thumbs/a.webp', 'sources/b.png']);
assert.deepEqual(objectKeysOf({ source_path: null, result_path: '', thumb_path: undefined, source_paths: null }), []);
});
test('Days left in the trash count down and never go negative', () => {
const now = new Date('2026-10-10T12:00:00Z');
assert.equal(daysLeft('2026-10-10T12:00:00Z', 30, now), 30);
assert.equal(daysLeft('2026-10-09T12:00:00Z', 30, now), 29);
assert.equal(daysLeft('2026-10-09T11:00:00Z', 30, now), 29); // 28.96 days -> rounded up
assert.equal(daysLeft('2026-09-10T12:00:00Z', 30, now), 0); // exactly due
assert.equal(daysLeft('2026-01-01T00:00:00Z', 30, now), 0); // long overdue
assert.equal(daysLeft('2026-10-10T12:00:00Z', 0, now), 0); // trash switched off
});
test('Reading: own, admin, or a shared library — never another person\'s private session', () => {
const me = { uid: 'u1', role: 'user' }, admin = { uid: 'a', role: 'admin' };
const mine = { created_by: 'u1', private: false }, theirs = { created_by: 'u2', private: false };
const theirsPrivate = { created_by: 'u2', private: true };
assert.equal(canRead(me, mine, false), true);
assert.equal(canRead(me, theirs, false), false);
assert.equal(canRead(me, theirs, true), true);
assert.equal(canRead(me, theirsPrivate, true), false);
assert.equal(canRead(admin, theirsPrivate, false), true);
});
test('Writing: own or admin only — a shared library is for looking, not for deleting', () => {
const me = { uid: 'u1', role: 'user' };
assert.equal(canWrite(me, { created_by: 'u1' }), true);
assert.equal(canWrite(me, { created_by: 'u2' }), false);
assert.equal(canWrite({ uid: null, role: 'user' }, { created_by: null }), false);
assert.equal(canWrite({ uid: 'x', role: 'admin' }, { created_by: 'u2' }), true);
});