From d487a0095537ad7e8a70fde154275cd5537c8f4c Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 9 Oct 2026 12:21:31 +0200 Subject: [PATCH] fix(quick-261009-dkv): Teilen-Dialog, Freigabe-Ansichten und Dateiansicht nach der Pruefung - Link aendern: nicht angebotene Berechtigung bleibt unberuehrt (Aktuell: ...), Zugriff nur bei ausdruecklicher Wahl gesendet - Fehler je Abschnitt zugeordnet und geloescht; Aendern nur bei aenderbarer Freigabe; Kopiert-Anzeige laeuft ab - Freigabe-Ansichten: stabiler Rueckruf, nur das juengste Laden gilt; Weitergaben mit Hinweis - Ablaufgrenzen nach Serverdatum, Passwort-Erzeugung bis 256 Zeichen - Dateiansicht blendet Neuer Ordner, Hochladen, Ablegen, Umbenennen, Verschieben und Loeschen nach den Buchstaben der Nextcloud aus - Import-Reihenfolge nach biome check, Anleitungen und Changelog (10 statt 15 Freigaben, Suche ab einem oder zwei Zeichen) Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 +- .../components/FileBrowser.test.tsx | 151 +++++++++++++- .../components/FileBrowser.tsx | 99 +++++++--- .../nextcloud-files/components/FileList.tsx | 2 +- .../components/LinkShareForm.tsx | 35 +++- .../components/SelectionBar.tsx | 33 ++-- .../components/ShareDialog.test.tsx | 184 ++++++++++++++++++ .../components/ShareDialog.tsx | 56 ++++-- .../components/SharesView.test.tsx | 113 ++++++++++- .../nextcloud-files/components/SharesView.tsx | 53 +++-- .../nextcloud-files/components/Toolbar.tsx | 50 +++-- .../nextcloud-files-page.test.tsx | 21 ++ .../(portal)/modules/nextcloud-files/page.tsx | 3 +- .../nextcloud-files/entry-permissions.test.ts | 46 +++++ .../nextcloud-files/entry-permissions.ts | 40 ++++ .../nextcloud-files/share-policy.test.ts | 46 ++++- .../nextcloud-files/share-policy.ts | 27 ++- apps/web/src/lib/nextcloud-files-api.ts | 9 + apps/web/src/messages/de.json | 15 +- apps/web/src/messages/en.json | 15 +- docs/anleitung-administration.md | 2 +- docs/anleitung-anwender.md | 8 +- docs/anleitung-betrieb.md | 4 +- docs/anleitung-entwicklung.md | 27 ++- 24 files changed, 913 insertions(+), 130 deletions(-) create mode 100644 apps/web/src/components/nextcloud-files/entry-permissions.test.ts create mode 100644 apps/web/src/components/nextcloud-files/entry-permissions.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae7dc6..bae4537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,9 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T - Domains, Registrieren: Verfügbarkeit prüfen, Kontakte wählen (die Nameserver übernimmt Tessera unverändert und in derselben Reihenfolge aus den Standardwerten in AutoDNS; sind dort keine hinterlegt, ist die Registrierung gesperrt), Zusammenfassung mit System und Preis ansehen und mit „Jetzt verbindlich registrieren“ bestätigen. Tessera schützt vor Doppelbestellungen: Jede Registrierung wird höchstens ein einziges Mal an AutoDNS geschickt, auch bei Doppelklick oder zwei geöffneten Fenstern, und nie automatisch wiederholt. Ist unklar, ob AutoDNS die Bestellung angenommen hat, steht der Auftrag auf „Ergebnis ungeklärt“, bis Tessera ihn mit AutoDNS abgeglichen hat. Den Stand jeder Registrierung sehen Sie im Reiter „Aufträge“; er wird beim Öffnen und danach alle 30 Sekunden mit AutoDNS abgeglichen. - Neues Modul „Dateien“ in der Gruppe „Infrastruktur“: Ihre Dateien aus der Firmen-Nextcloud direkt in Tessera ansehen, hochladen, herunterladen und ordnen. Ein Administrator aktiviert das Modul im Marktplatz; wer es nutzen soll, bekommt zusätzlich die Freigabe. Die Adresse der Nextcloud trägt nur ein Administrator im Reiter „Einstellungen“ ein, denn alle Benutzer schicken ihr Nextcloud-Passwort dorthin; „Verbindung prüfen“ testet sie. Benutzer mit der Freigabe „Verwalten“ sehen die Adresse dort und können die Verbindung prüfen, sie aber nicht ändern. Wird die Adresse später geändert, müssen sich alle Benutzer neu anmelden. - Dateien, Anmeldung: Jeder meldet sich mit seinem eigenen Nextcloud-Konto an. Tessera speichert dabei nicht Ihr Passwort, sondern lässt sich von Nextcloud einen eigenen Zugang ausstellen, den Sie jederzeit in Nextcloud widerrufen können. Konten mit Zwei-Faktor-Anmeldung melden sich über „Im Browser anmelden“: Sie bestätigen den Zugriff auf der Nextcloud-Seite, Tessera wartet so lange. Der Anmeldebildschirm zeigt Name, Logo und Farbe Ihrer Nextcloud, damit Sie sehen, wo Sie sich anmelden. „Abmelden“ entfernt den Zugang auch in Nextcloud; Ihre Dateien bleiben unverändert. Damit nicht ein paar Fehlversuche die Nextcloud für alle sperren, begrenzt Tessera Fehlversuche selbst und hält alle Anfragen an, sobald die Nextcloud „zu viele Anfragen“ meldet. -- Dateien, Arbeiten mit Dateien: Ordner durchsuchen mit Pfadleiste, Liste oder Raster (Tessera merkt sich Ihre Wahl), Farbkacheln je Dateityp, echte Vorschaubilder und Anzeige des belegten Speicherplatzes. Neuer Ordner, Umbenennen, Verschieben (mit Ordnerauswahl) und Löschen – gelöscht wird in den Papierkorb Ihrer Nextcloud, nach einer Rückfrage; alles auch für mehrere Einträge gleichzeitig, per Maus, Zeilenmenü, Rechtsklick oder Tastatur (Enter, Rücktaste, F2, Entf, Strg+A). +- Dateien, Arbeiten mit Dateien: Ordner durchsuchen mit Pfadleiste, Liste oder Raster (Tessera merkt sich Ihre Wahl), Farbkacheln je Dateityp, echte Vorschaubilder und Anzeige des belegten Speicherplatzes. Neuer Ordner, Umbenennen, Verschieben (mit Ordnerauswahl) und Löschen – gelöscht wird in den Papierkorb Ihrer Nextcloud, nach einer Rückfrage; alles auch für mehrere Einträge gleichzeitig, per Maus, Zeilenmenü, Rechtsklick oder Tastatur (Enter, Rücktaste, F2, Entf, Strg+A). Was Ihnen ein Ordner nicht erlaubt, zeigt Tessera gar nicht erst an: In einem Ordner, den andere nur zum Ansehen mit Ihnen geteilt haben, fehlen „Neuer Ordner“ und „Hochladen“, und bei den Einträgen fehlen Umbenennen, Verschieben und Löschen. - Dateien, Hochladen und Herunterladen: Dateien und ganze Ordner per Ziehen und Ablegen (auch direkt auf einen Ordner) oder über „Hochladen“ hochladen; große Dateien gehen in Stücken hoch. Die Übertragungsleiste am unteren Rand zeigt den Fortschritt jeder Datei, lässt Abbrechen und Wiederholen zu und nennt Fehler in einfachen Worten. Tessera überschreibt nie stillschweigend: Gibt es den Namen schon, wählen Sie „Ersetzen“, „Beide behalten“ oder „Überspringen“. Dateien werden direkt heruntergeladen, Ordner und Mehrfachauswahl als ZIP-Datei. -- Dateien, Teilen: Im Zeilenmenü einer Datei oder eines Ordners finden Sie jetzt „Teilen“. Dort suchen Sie Kolleginnen, Kollegen und Gruppen aus Ihrer Nextcloud und geben die Datei zum „Ansehen“ oder „Bearbeiten“ frei; die Berechtigung ändern Sie später, und eine Freigabe nehmen Sie mit „Entfernen“ wieder zurück. Außerdem erstellen Sie öffentliche Links mit „Ansehen“ oder „Bearbeiten“, bei Ordnern auch mit „Nur hochladen“, und kopieren sie mit „Link kopieren“. Passwort und Ablaufdatum richten sich nach den Regeln der Firmen-Nextcloud: Verlangt sie ein Passwort, fragt Tessera danach und kann auf Wunsch eines erzeugen. Tessera speichert das Passwort nie und zeigt es nur, solange der Dialog offen ist. Zum Schutz der Nextcloud legt Tessera höchstens 15 neue Freigaben innerhalb von 10 Minuten an. +- Dateien, Teilen: Im Zeilenmenü einer Datei oder eines Ordners finden Sie jetzt „Teilen“. Dort suchen Sie Kolleginnen, Kollegen und Gruppen aus Ihrer Nextcloud und geben die Datei zum „Ansehen“ oder „Bearbeiten“ frei; die Berechtigung ändern Sie später, und eine Freigabe nehmen Sie mit „Entfernen“ wieder zurück. Außerdem erstellen Sie öffentliche Links mit „Ansehen“ oder „Bearbeiten“, bei Ordnern auch mit „Nur hochladen“, und kopieren sie mit „Link kopieren“. Passwort und Ablaufdatum richten sich nach den Regeln der Firmen-Nextcloud: Verlangt sie ein Passwort, fragt Tessera danach und kann auf Wunsch eines erzeugen. Tessera speichert das Passwort nie und zeigt es nur, solange der Dialog offen ist. Zum Schutz der Nextcloud legt Tessera höchstens 10 neue Freigaben innerhalb von 10 Minuten an. - Dateien, Übersicht der Freigaben: Die Reiter „Von mir geteilt“ und „Mit mir geteilt“ zeigen alle Freigaben auf einen Blick. Sie öffnen Einträge direkt im zugehörigen Ordner, ändern oder entfernen Ihre eigenen Freigaben, verlassen Freigaben anderer und nehmen offene Freigaben an oder lehnen sie ab. Geteilte Einträge sind in der Dateiliste markiert. Freigaben per E-Mail und an andere Server zeigt Tessera nicht an; sie bleiben der Nextcloud vorbehalten. ### Geändert diff --git a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx index 8e1cd16..7b98270 100644 --- a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx +++ b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx @@ -70,7 +70,7 @@ function entry(path: string, over: Partial = {}): NcEntry { }; } const folder = (path: string, over: Partial = {}) => - entry(path, { type: 'folder', size: 4096, ...over }); + entry(path, { type: 'folder', size: 4096, permissions: 'RGDNVCK', ...over }); function listing(path: string, entries: NcEntry[], over: Partial = {}): NcListing { return { @@ -783,3 +783,152 @@ describe('FileBrowser — Teilen (quick-261009-dkv)', () => { await waitFor(() => expect(onExpired).toHaveBeenCalledTimes(1)); }); }); + +describe('FileBrowser — Rechte des geöffneten Ordners (Messwerte der Nextcloud 34)', () => { + const dropFile = () => { + const file = new File(['x'], 'neu.txt'); + return { + file, + dt: { + types: ['Files'], + files: [file], + items: [{ kind: 'file', getAsFile: () => file }], + dropEffect: 'none', + }, + }; + }; + + /** Eingehender Nur-Ansehen-Ordner: er selbst `SGDN`, sein Inhalt `SG`. */ + function readOnlyFolder(empty = false) { + mockList.mockImplementation(async (p: string) => + listing( + p, + empty + ? [] + : [folder('/Ansicht', { permissions: 'SG' }), entry('/Plan.pdf', { permissions: 'SG' })], + { permissions: 'SGDN' }, + ), + ); + } + + it('eigener Ordner (RGDNVCK): Neuer Ordner und Hochladen sind da, kein Hinweis "Nur ansehen"', async () => { + mockList.mockImplementation(async (p: string) => + listing(p, [folder('/Projekte')], { permissions: 'RGDNVCK' }), + ); + await mount(); + expect(screen.getByRole('button', { name: 'Neuer Ordner' })).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Hochladen' })).toBeTruthy(); + expect(screen.queryByText('Nur ansehen')).toBeNull(); + }); + + it('Antwort ohne Buchstaben des Ordners: nichts wird ausgeblendet (unbekannt ist nicht verboten)', async () => { + await mount(); + expect(screen.getByRole('button', { name: 'Neuer Ordner' })).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Hochladen' })).toBeTruthy(); + }); + + it('Nur-Ansehen-Ordner (SGDN): kein Neuer Ordner, kein Hochladen, dafuer der Hinweis "Nur ansehen"', async () => { + readOnlyFolder(); + render(); + await screen.findByRole('row', { name: 'Plan.pdf' }); + expect(screen.queryByRole('button', { name: 'Neuer Ordner' })).toBeNull(); + expect(screen.queryByRole('button', { name: 'Hochladen' })).toBeNull(); + expect(screen.getByText('Nur ansehen')).toBeTruthy(); + }); + + it('leerer Nur-Ansehen-Ordner: keine Einladung zum Hochladen und kein Hochladen-Knopf', async () => { + readOnlyFolder(true); + render(); + expect(await screen.findByText('Dieser Ordner ist leer.')).toBeTruthy(); + expect(screen.getByText('Hier lässt sich nichts hinzufügen.')).toBeTruthy(); + expect(screen.queryByText('Dateien hierher ziehen oder hochladen.')).toBeNull(); + expect(screen.queryByRole('button', { name: 'Hochladen' })).toBeNull(); + }); + + it('Ablegen in einen Nur-Ansehen-Ordner: keine Markierung, kein Hochladen, ein Hinweis; der Browser oeffnet die Datei nicht', async () => { + readOnlyFolder(); + render(); + await screen.findByRole('row', { name: 'Plan.pdf' }); + const { dt } = dropFile(); + fireEvent.dragEnter(window, { dataTransfer: dt }); + expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull(); + const over = createEvent.dragOver(window, { dataTransfer: dt }); + fireEvent(window, over); + expect(over.defaultPrevented).toBe(true); + expect(dt.dropEffect).toBe('none'); + const drop = createEvent.drop(window, { dataTransfer: dt }); + fireEvent(window, drop); + expect(drop.defaultPrevented).toBe(true); + await act(async () => {}); + expect(mockUpload).not.toHaveBeenCalled(); + expect(screen.getByText('In diesen Ordner dürfen Sie nichts ablegen.')).toBeTruthy(); + }); + + it('Ablegen auf eine Ordnerzeile ohne Anlegerecht (SG) wird abgewiesen, auf eine mit (RGDNVCK) nicht', async () => { + mockList.mockImplementation(async (p: string) => + listing( + p, + [folder('/Fest', { permissions: 'SG' }), folder('/Offen', { permissions: 'RGDNVCK' })], + { permissions: 'RGDNVCK' }, + ), + ); + render(); + await screen.findByRole('row', { name: 'Fest' }); + const { dt } = dropFile(); + const fest = screen.getByRole('row', { name: 'Fest' }); + fireEvent.dragEnter(fest, { dataTransfer: dt }); + fireEvent.dragOver(fest, { dataTransfer: dt }); + expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull(); + fireEvent.drop(fest, { dataTransfer: dt }); + await act(async () => {}); + expect(mockUpload).not.toHaveBeenCalled(); + + const offen = screen.getByRole('row', { name: 'Offen' }); + fireEvent.dragEnter(offen, { dataTransfer: dt }); + fireEvent.dragOver(offen, { dataTransfer: dt }); + expect(screen.getByTestId('nc-files-drop-overlay').textContent).toContain('In „Offen“'); + fireEvent.drop(offen, { dataTransfer: dt }); + await waitFor(() => expect(mockUpload).toHaveBeenCalledTimes(1)); + expect(mockUpload.mock.calls[0][1]).toBe('/Offen/neu.txt'); + }); + + it('Eintraege ohne Rechte (SG): im Menue fehlen Umbenennen, Verschieben und Loeschen, Herunterladen bleibt', async () => { + readOnlyFolder(); + render(); + const row = await screen.findByRole('row', { name: 'Plan.pdf' }); + fireEvent.contextMenu(row, { clientX: 100, clientY: 100 }); + const names = screen.getAllByRole('menuitem').map((m) => m.textContent); + expect(names).toContain('Herunterladen'); + for (const gone of ['Umbenennen', 'Verschieben', 'Löschen']) { + expect(names).not.toContain(gone); + } + }); + + it('Eintraege mit allen Rechten behalten Umbenennen, Verschieben und Loeschen', async () => { + await mount(); + fireEvent.contextMenu(screen.getByRole('row', { name: 'Bericht.pdf' }), { + clientX: 100, + clientY: 100, + }); + const names = screen.getAllByRole('menuitem').map((m) => m.textContent); + for (const there of ['Umbenennen', 'Verschieben', 'Löschen']) { + expect(names).toContain(there); + } + }); + + it('Auswahlleiste ohne Rechte: Herunterladen ja, Verschieben und Loeschen nein; Entf und F2 tun nichts', async () => { + readOnlyFolder(); + render(); + const row = await screen.findByRole('row', { name: 'Plan.pdf' }); + fireEvent.click(row); + const bar = await screen.findByRole('toolbar'); + expect(within(bar).getByRole('button', { name: /Herunterladen/ })).toBeTruthy(); + expect(within(bar).queryByRole('button', { name: /Verschieben/ })).toBeNull(); + expect(within(bar).queryByRole('button', { name: /Löschen/ })).toBeNull(); + fireEvent.keyDown(row, { key: 'Delete' }); + fireEvent.keyDown(row, { key: 'F2' }); + await act(async () => {}); + expect(screen.queryByRole('dialog')).toBeNull(); + expect(screen.queryByRole('alertdialog')).toBeNull(); + }); +}); diff --git a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx index 0b1eefe..9dc8c69 100644 --- a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx +++ b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx @@ -3,6 +3,13 @@ import { useLocale, useTranslations } from 'next-intl'; import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { collectDrop, hasFiles, snapshotDrop } from '@/components/nextcloud-files/drop-entries'; +import { + canCreateFolderIn, + canDelete, + canMove, + canRename, + canUploadTo, +} from '@/components/nextcloud-files/entry-permissions'; import { type ErrorLike, errorText, toErrorLike } from '@/components/nextcloud-files/error-text'; import { basename, freeName, join, parent, ROOT } from '@/components/nextcloud-files/paths'; import { EMPTY_SELECTION, selectionReducer } from '@/components/nextcloud-files/selection'; @@ -32,6 +39,7 @@ import { FileList } from './FileList'; import { DownloadIcon, ExternalIcon, + FolderIcon, MoveIcon, OpenIcon, PencilIcon, @@ -356,6 +364,14 @@ export function FileBrowser({ }); }, [listing, sort, locale]); + // Was der geoeffnete Ordner selbst erlaubt (Buchstaben des Ordners): ein nur zum Ansehen + // geteilter Ordner bietet kein Anlegen und kein Hochladen an. Unbekannt heisst erlaubt. + const folderLetters = listing?.permissions ?? null; + const canUploadHere = canUploadTo(folderLetters); + const canNewFolderHere = canCreateFolderIn(folderLetters); + const dropRulesRef = useRef({ canUploadHere: true, entries: [] as NcEntry[] }); + dropRulesRef.current = { canUploadHere, entries }; + const order = useMemo(() => entries.map((e) => e.path), [entries]); const selectedSet = useMemo(() => new Set(selection.selected), [selection.selected]); const selectedEntries = useMemo( @@ -450,10 +466,19 @@ export function FileBrowser({ const el = e.target instanceof Element ? e.target.closest('[data-drop-folder]') : null; return el?.getAttribute('data-drop-folder') ?? pathRef.current; }; + // Darf in das Ziel (diesen Ordner oder eine Ordnerzeile) abgelegt werden? Unbekannte Ziele + // (Pfadleiste) bleiben erlaubt: dort entscheidet die Nextcloud. + const allowed = (e: DragEvent): boolean => { + const target = targetOf(e); + const rules = dropRulesRef.current; + if (target === pathRef.current) return rules.canUploadHere; + const row = rules.entries.find((x) => x.type === 'folder' && x.path === target); + return row ? canUploadTo(row.permissions) : true; + }; const onEnter = (e: DragEvent) => { if (!hasFiles(e.dataTransfer)) return; depth += 1; - setDropTarget(blocked(e) ? null : targetOf(e)); + setDropTarget(blocked(e) || !allowed(e) ? null : targetOf(e)); }; const onOver = (e: DragEvent) => { if (!hasFiles(e.dataTransfer)) return; @@ -463,6 +488,11 @@ export function FileBrowser({ setDropTarget(null); return; } + if (!allowed(e)) { + if (e.dataTransfer) e.dataTransfer.dropEffect = 'none'; + setDropTarget(null); + return; + } if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; const target = targetOf(e); setDropTarget((prev) => (prev === target ? prev : target)); @@ -480,6 +510,10 @@ export function FileBrowser({ depth = 0; setDropTarget(null); if (handled || blocked(e)) return; + if (!allowed(e)) { + say(t('drop.readOnly')); + return; + } const target = targetOf(e); const snapshot = snapshotDrop(e.dataTransfer); void collectDrop(snapshot).then((result) => { @@ -675,20 +709,23 @@ export function FileBrowser({ onSelect: () => void startDownload({ kind: 'file', path: entry.path }), }); } - actions.push( - { + // Nur, was die Buchstaben des Eintrags erlauben (N umbenennen, V verschieben, D loeschen). + if (canRename(entry.permissions)) { + actions.push({ id: 'rename', label: t('menu.rename'), icon: , onSelect: () => setDialog({ kind: 'rename', entry }), - }, - { + }); + } + if (canMove(entry.permissions)) { + actions.push({ id: 'move', label: t('menu.move'), icon: , onSelect: () => setDialog({ kind: 'move', entries: [entry] }), - }, - ); + }); + } // Nur Eintraege, die Nextcloud als teilbar kennzeichnet (Buchstabe R). if (sharingEnabled && entry.permissions.includes('R')) { actions.push({ @@ -707,14 +744,16 @@ export function FileBrowser({ separated: true, }); } - actions.push({ - id: 'delete', - label: t('menu.delete'), - icon: , - destructive: true, - separated: !(serverUrl && entry.fileId), - onSelect: () => setDialog({ kind: 'delete', entries: [entry] }), - }); + if (canDelete(entry.permissions)) { + actions.push({ + id: 'delete', + label: t('menu.delete'), + icon: , + destructive: true, + separated: !(serverUrl && entry.fileId), + onSelect: () => setDialog({ kind: 'delete', entries: [entry] }), + }); + } return actions; }; @@ -827,7 +866,7 @@ export function FileBrowser({ return; case 'Delete': { const list = selectedEntries.length > 0 ? selectedEntries : entry ? [entry] : []; - if (list.length > 0) { + if (list.length > 0 && list.every((x) => canDelete(x.permissions))) { e.preventDefault(); setDialog({ kind: 'delete', entries: list }); } @@ -835,7 +874,7 @@ export function FileBrowser({ } case 'F2': { const target = selectedEntries.length === 1 ? selectedEntries[0] : entry; - if (target) { + if (target && canRename(target.permissions)) { e.preventDefault(); setDialog({ kind: 'rename', entry: target }); } @@ -938,17 +977,21 @@ export function FileBrowser({ aria-hidden="true" className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-muted text-muted-foreground" > - + {canUploadHere ? : }

{t('empty.title')}

-

{t('empty.hint')}

- +

+ {canUploadHere ? t('empty.hint') : t('empty.readOnlyHint')} +

+ {canUploadHere && ( + + )} ); @@ -987,6 +1030,8 @@ export function FileBrowser({ onMove={() => setDialog({ kind: 'move', entries: selectedEntries })} onDelete={() => setDialog({ kind: 'delete', entries: selectedEntries })} onClear={() => dispatch({ type: 'clear' })} + canMove={selectedEntries.every((e) => canMove(e.permissions))} + canDelete={selectedEntries.every((e) => canDelete(e.permissions))} /> ) : ( )} diff --git a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileList.tsx b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileList.tsx index 8dff4ea..ecae8bb 100644 --- a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileList.tsx +++ b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileList.tsx @@ -9,8 +9,8 @@ import { } from '@/components/nextcloud-files/file-format'; import { downloadUrl, type NcEntry } from '@/lib/nextcloud-files-api'; import { ChevronDownIcon, ChevronUpIcon, MoreIcon } from './icons'; -import type { SortDir, SortKey } from './Toolbar'; import { ShareIndicator } from './ShareIndicator'; +import type { SortDir, SortKey } from './Toolbar'; import { TypeTile } from './TypeTile'; /** Kopfzellen: klebt unter der App-Leiste, damit die Spalten beim Scrollen lesbar bleiben. */ diff --git a/apps/web/src/app/(portal)/modules/nextcloud-files/components/LinkShareForm.tsx b/apps/web/src/app/(portal)/modules/nextcloud-files/components/LinkShareForm.tsx index ab82bae..97b8895 100644 --- a/apps/web/src/app/(portal)/modules/nextcloud-files/components/LinkShareForm.tsx +++ b/apps/web/src/app/(portal)/modules/nextcloud-files/components/LinkShareForm.tsx @@ -9,9 +9,9 @@ import { generatePassword, type LinkFormValues, linkPasswordMode, + policyToday, type ShareAccessChoice, type ShareTarget, - todayLocal, } from '@/components/nextcloud-files/share-policy'; import type { NcShare, NcSharePolicy } from '@/lib/nextcloud-files-api'; @@ -56,11 +56,15 @@ export function LinkShareForm({ const editing = share !== undefined; const options = accessOptions(target, 'link', policy); - const initialAccess: ShareAccessChoice = - share && options.includes(share.access as ShareAccessChoice) - ? (share.access as ShareAccessChoice) - : 'view'; - const [access, setAccess] = useState(initialAccess); + // Gehoert die vorhandene Berechtigung nicht zu den angebotenen (z. B. Bearbeiten, obwohl die + // Nextcloud das oeffentliche Hochladen inzwischen verbietet, oder eine eigene Berechtigung), + // bleibt die Auswahl leer (`null`) und die Berechtigung unberuehrt: gesendet wird sie nur, + // wenn der Benutzer ausdruecklich eine andere waehlt (WR-01). + const currentNotOffered = + share !== undefined && !options.includes(share.access as ShareAccessChoice); + const [access, setAccess] = useState( + share === undefined ? 'view' : currentNotOffered ? null : (share.access as ShareAccessChoice), + ); // --- Passwort --- const passwordMode = linkPasswordMode(policy); @@ -79,7 +83,8 @@ export function LinkShareForm({ const passwordFieldVisible = editing ? editPassword === 'change' : createPasswordOn; // --- Ablauf --- - const today = todayLocal(); + // Grenzen nach dem Datum des Servers, nicht der Uhr des Browsers (IN-01). + const today = policyToday(policy); const rule = expiryRule(policy, today); const [expiryOn, setExpiryOn] = useState( editing @@ -146,6 +151,22 @@ export function LinkShareForm({ >
{t('accessLegend')} + {currentNotOffered && share && ( + + )} {options.map((o) => (
- - + {!canNewFolder && !canUpload && ( + {t('toolbar.readOnly')} + )} + {canNewFolder && ( + + )} + {canUpload && ( + + )} ); diff --git a/apps/web/src/app/(portal)/modules/nextcloud-files/nextcloud-files-page.test.tsx b/apps/web/src/app/(portal)/modules/nextcloud-files/nextcloud-files-page.test.tsx index 20495b7..b113b1a 100644 --- a/apps/web/src/app/(portal)/modules/nextcloud-files/nextcloud-files-page.test.tsx +++ b/apps/web/src/app/(portal)/modules/nextcloud-files/nextcloud-files-page.test.tsx @@ -712,6 +712,27 @@ describe('Reiter fuer Freigaben (quick-261009-dkv)', () => { expect(mockListFolder).not.toHaveBeenCalledWith('/Ben-Ordner'); }); + it('ein Neuzeichnen der Seite laedt die offene Freigabe-Ansicht nicht neu (WR-04)', async () => { + let resolveInfo: (info: NextcloudServerInfo) => void = () => undefined; + mockGetServerInfo.mockReturnValue( + new Promise((resolve) => { + resolveInfo = resolve; + }), + ); + mockGetStatus.mockResolvedValue(connectedStatus()); + render(); + fireEvent.click(await screen.findByRole('button', { name: 'Von mir geteilt' })); + expect(await screen.findByText(/Sie haben noch nichts geteilt/)).toBeTruthy(); + expect(mockListMine).toHaveBeenCalledTimes(1); + // Die Kennung der Nextcloud trifft spaet ein: die Seite zeichnet sich neu. + await act(async () => { + resolveInfo(serverInfo()); + }); + await act(async () => {}); + expect(mockListMine).toHaveBeenCalledTimes(1); + expect(screen.queryByText('Freigaben werden geladen …')).toBeNull(); + }); + it('eine Datei oeffnet den Ordner darueber', async () => { mockGetStatus.mockResolvedValue(connectedStatus()); mockListReceived.mockResolvedValue({ diff --git a/apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx b/apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx index 2704d5d..4530e1b 100644 --- a/apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx +++ b/apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx @@ -135,7 +135,8 @@ export default function NextcloudFilesPage() { setBrowserStart({ path, focus }); setTab('files'); }; - const refreshAfterExpiry = () => void reloadStatus(); + // Stabil, damit die Freigabe-Ansichten nicht bei jedem Rendern der Seite neu laden (WR-04). + const refreshAfterExpiry = useCallback(() => void reloadStatus(), [reloadStatus]); return (
diff --git a/apps/web/src/components/nextcloud-files/entry-permissions.test.ts b/apps/web/src/components/nextcloud-files/entry-permissions.test.ts new file mode 100644 index 0000000..41569db --- /dev/null +++ b/apps/web/src/components/nextcloud-files/entry-permissions.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { canCreateFolderIn, canDelete, canMove, canRename, canUploadTo } from './entry-permissions'; + +describe('Berechtigungsbuchstaben (gemessen an Nextcloud 34)', () => { + it('eigener Ordner RGDNVCK: alles erlaubt', () => { + expect(canUploadTo('RGDNVCK')).toBe(true); + expect(canCreateFolderIn('RGDNVCK')).toBe(true); + expect(canRename('RGDNVCK')).toBe(true); + expect(canMove('RGDNVCK')).toBe(true); + expect(canDelete('RGDNVCK')).toBe(true); + }); + + it('zum Ansehen geteilter Ordner selbst (SGDN): nichts anlegen', () => { + expect(canUploadTo('SGDN')).toBe(false); + expect(canCreateFolderIn('SGDN')).toBe(false); + }); + + it('Inhalt eines zum Ansehen geteilten Ordners (SG): weder anlegen, umbenennen, verschieben noch loeschen', () => { + for (const fn of [canUploadTo, canCreateFolderIn, canRename, canMove, canDelete]) { + expect(fn('SG')).toBe(false); + } + }); + + it('zum Bearbeiten geteilter Ordner (SRGDNVCK): alles erlaubt', () => { + expect(canUploadTo('SRGDNVCK')).toBe(true); + expect(canCreateFolderIn('SRGDNVCK')).toBe(true); + expect(canDelete('SRGDNVCK')).toBe(true); + }); + + it('Einzelbuchstaben zaehlen einzeln', () => { + expect(canUploadTo('RGC')).toBe(true); + expect(canCreateFolderIn('RGC')).toBe(false); + expect(canCreateFolderIn('RGK')).toBe(true); + expect(canUploadTo('RGK')).toBe(false); + expect(canRename('RGN')).toBe(true); + expect(canMove('RGN')).toBe(false); + }); + + it('unbekannt (null, undefined, leer) ist nie verboten', () => { + for (const letters of [null, undefined, '']) { + for (const fn of [canUploadTo, canCreateFolderIn, canRename, canMove, canDelete]) { + expect(fn(letters)).toBe(true); + } + } + }); +}); diff --git a/apps/web/src/components/nextcloud-files/entry-permissions.ts b/apps/web/src/components/nextcloud-files/entry-permissions.ts new file mode 100644 index 0000000..cb80942 --- /dev/null +++ b/apps/web/src/components/nextcloud-files/entry-permissions.ts @@ -0,0 +1,40 @@ +/** + * Was die Berechtigungsbuchstaben der Nextcloud (`oc:permissions`) erlauben. Der Browser blendet + * damit Aktionen aus, die die Nextcloud ohnehin ablehnen wuerde (z. B. "Neuer Ordner" in einem + * Ordner, der nur zum Ansehen geteilt wurde); verbindlich entscheidet weiter die Nextcloud. + * + * Gemessen (Nextcloud 34): eigener Ordner `RGDNVCK`, eigene Datei `RGDNVW`, ein zum Ansehen + * geteilter Ordner selbst geoeffnet `SGDN`, sein Inhalt `SG`, ein zum Bearbeiten geteilter Ordner + * `SRGDNVCK`. Buchstaben: C Dateien anlegen, K Ordner anlegen, D loeschen, N umbenennen, + * V verschieben, W Datei aendern. + * + * Fehlen die Buchstaben ganz (`null`, `undefined`, leer), ist das "unbekannt" und nie "verboten": + * es wird nichts ausgeblendet, die Nextcloud entscheidet. + */ + +export type Letters = string | null | undefined; + +const known = (letters: Letters): letters is string => + typeof letters === 'string' && letters !== ''; + +/** In diesem Ordner duerfen Dateien angelegt werden (Hochladen, Ablegen). */ +export function canUploadTo(letters: Letters): boolean { + return !known(letters) || letters.includes('C'); +} + +/** In diesem Ordner duerfen Ordner angelegt werden. */ +export function canCreateFolderIn(letters: Letters): boolean { + return !known(letters) || letters.includes('K'); +} + +export function canRename(letters: Letters): boolean { + return !known(letters) || letters.includes('N'); +} + +export function canMove(letters: Letters): boolean { + return !known(letters) || letters.includes('V'); +} + +export function canDelete(letters: Letters): boolean { + return !known(letters) || letters.includes('D'); +} diff --git a/apps/web/src/components/nextcloud-files/share-policy.test.ts b/apps/web/src/components/nextcloud-files/share-policy.test.ts index e32110e..dd386e2 100644 --- a/apps/web/src/components/nextcloud-files/share-policy.test.ts +++ b/apps/web/src/components/nextcloud-files/share-policy.test.ts @@ -7,6 +7,7 @@ import { generatePassword, linkPasswordMode, linkUpdateDiff, + policyToday, type RandomFill, type ShareTarget, targetFromEntry, @@ -88,11 +89,14 @@ function counter(start = 0): RandomFill { } describe('generatePassword', () => { - it('mindestens 20 Zeichen, von der Mindestlaenge der Nextcloud gehoben, bei 64 gedeckelt', () => { + it('mindestens 20 Zeichen, von der Mindestlaenge der Nextcloud gehoben, nie darunter (bis 256)', () => { expect(generatePassword(null, counter(1))).toHaveLength(20); expect(generatePassword(10, counter(2))).toHaveLength(20); expect(generatePassword(32, counter(3))).toHaveLength(32); - expect(generatePassword(500, counter(4))).toHaveLength(64); + // eine hohe Mindestlaenge der Nextcloud wird erreicht, sonst wuerde sie das Passwort ablehnen (IN-04) + expect(generatePassword(100, counter(5))).toHaveLength(100); + expect(generatePassword(256, counter(6))).toHaveLength(256); + expect(generatePassword(500, counter(4))).toHaveLength(256); }); it('enthaelt Gross-, Kleinbuchstaben, Ziffer und Sonderzeichen, nie aehnliche Zeichen', () => { @@ -134,6 +138,15 @@ describe('Datumshilfen', () => { expect(addDays('2026-10-09', 30)).toBe('2026-11-08'); }); + it('policyToday nimmt das Serverdatum der Regeln, sonst die Uhr des Browsers (IN-01)', () => { + const browser = new Date(2026, 0, 5, 12, 0); + expect(policyToday({ ...POLICY, today: '2026-10-09' }, browser)).toBe('2026-10-09'); + expect(policyToday(POLICY, browser)).toBe('2026-01-05'); + expect(policyToday(null, browser)).toBe('2026-01-05'); + // Unsinn vom Server wird nicht uebernommen + expect(policyToday({ ...POLICY, today: '9.10.2026' }, browser)).toBe('2026-01-05'); + }); + it('todayLocal nimmt den lokalen Kalendertag, nicht UTC', () => { expect(todayLocal(new Date(2026, 0, 5, 23, 59))).toBe('2026-01-05'); expect(todayLocal(new Date(2026, 11, 31, 0, 1))).toBe('2026-12-31'); @@ -215,6 +228,35 @@ describe('linkUpdateDiff', () => { ).toEqual({ password: '' }); }); + it('Berechtigung nicht angefasst (null): sie wird nie gesendet, auch nicht bei eigener Berechtigung (WR-01)', () => { + const custom = { ...share, access: 'custom' } as NcShare; + expect( + linkUpdateDiff(custom, { + access: null, + password: null, + expireDate: '2026-12-01', + label: 'Neu', + }), + ).toEqual({ label: 'Neu' }); + expect( + linkUpdateDiff(custom, { + access: null, + password: null, + expireDate: '2026-12-01', + label: 'Kunde', + }), + ).toEqual({}); + // ausdruecklich gewaehlt wird sie gesendet + expect( + linkUpdateDiff(custom, { + access: 'view', + password: null, + expireDate: '2026-12-01', + label: 'Kunde', + }), + ).toEqual({ access: 'view' }); + }); + it('ein Link ohne Ablauf vergleicht mit der leeren Zeichenkette', () => { const open = { ...share, expiration: null } as NcShare; expect( diff --git a/apps/web/src/components/nextcloud-files/share-policy.ts b/apps/web/src/components/nextcloud-files/share-policy.ts index 68d8f0a..acd9a16 100644 --- a/apps/web/src/components/nextcloud-files/share-policy.ts +++ b/apps/web/src/components/nextcloud-files/share-policy.ts @@ -66,11 +66,21 @@ export function linkPasswordMode(policy: NcSharePolicy | null): LinkPasswordMode const pad = (n: number, width = 2) => String(n).padStart(width, '0'); -/** Heutiges Datum nach der Uhr des Benutzers als `YYYY-MM-DD` (nicht UTC). */ +/** Heutiges Datum nach der Uhr des Browsers als `YYYY-MM-DD` (nicht UTC). Nur Rueckfall, siehe `policyToday`. */ export function todayLocal(now: Date = new Date()): string { return `${pad(now.getFullYear(), 4)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; } +/** + * Das Datum, nach dem Ablaufgrenzen gerechnet werden: das Serverdatum aus den Regeln (die + * Nextcloud rechnet nach der Uhr des Servers, nicht der des Browsers). Fehlt es, der Rueckfall + * auf die Uhr des Browsers. + */ +export function policyToday(policy: NcSharePolicy | null, now: Date = new Date()): string { + const today = policy?.today; + return typeof today === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(today) ? today : todayLocal(now); +} + /** Reine Kalenderrechnung (UTC, keine Sommerzeit-Fallen): `date` plus `n` Tage. */ export function addDays(date: string, n: number): string { const [y, m, d] = date.split('-').map(Number); @@ -101,9 +111,13 @@ export function expiryRule(policy: NcSharePolicy | null, today: string): ExpiryR }; } -/** Eingaben des Link-Formulars beim Aendern. `password`: null = behalten, '' = entfernen. */ +/** + * Eingaben des Link-Formulars beim Aendern. `password`: null = behalten, '' = entfernen. + * `access`: null = nicht angefasst (die vorhandene Berechtigung bleibt, auch wenn sie nicht zu + * den angebotenen gehoert); beim Anlegen steht immer eine Auswahl da. + */ export interface LinkFormValues { - access: ShareAccessChoice; + access: ShareAccessChoice | null; password: string | null; expireDate: string; label: string; @@ -112,7 +126,7 @@ export interface LinkFormValues { /** Nur die Felder, die sich gegenueber der vorhandenen Freigabe geaendert haben. */ export function linkUpdateDiff(share: NcShare, form: LinkFormValues): UpdateShareInput { const diff: UpdateShareInput = {}; - if (form.access !== share.access) diff.access = form.access; + if (form.access !== null && form.access !== share.access) diff.access = form.access; if (form.password !== null) diff.password = form.password; if (form.expireDate !== (share.expiration ?? '')) diff.expireDate = form.expireDate; if (form.label !== share.label) diff.label = form.label; @@ -127,7 +141,7 @@ const LOWER = 'abcdefghijkmnpqrstuvwxyz'; const DIGITS = '23456789'; const SPECIAL = '!#$%&*+-=?@_'; const MIN_GENERATED = 20; -const MAX_GENERATED = 64; +const MAX_GENERATED = 256; export type RandomFill = (array: Uint32Array) => Uint32Array; @@ -149,7 +163,8 @@ function pick(chars: string, fill: RandomFill): string { /** * Zufallspasswort aus der Betriebssystem-Zufallsquelle: mindestens 20 Zeichen (oder die - * Mindestlaenge der Nextcloud, hoechstens 64), je mindestens ein Gross- und Kleinbuchstabe, + * Mindestlaenge der Nextcloud, die bis 256 reicht und nie unterschritten wird; sonst waere + * das Passwort fuer die Nextcloud zu kurz), je mindestens ein Gross- und Kleinbuchstabe, * eine Ziffer und ein Sonderzeichen. Nextcloud prueft das Passwort trotzdem selbst. */ export function generatePassword(minLength: number | null, fill: RandomFill = cryptoFill): string { diff --git a/apps/web/src/lib/nextcloud-files-api.ts b/apps/web/src/lib/nextcloud-files-api.ts index a4f58e5..0176c83 100644 --- a/apps/web/src/lib/nextcloud-files-api.ts +++ b/apps/web/src/lib/nextcloud-files-api.ts @@ -225,6 +225,11 @@ export interface NcListing { quota: NcQuota; /** true, wenn der Ordner mehr als 5000 Eintraege hat und abgeschnitten wurde. */ truncated: boolean; + /** + * Berechtigungsbuchstaben des Ordners selbst (z. B. `RGDNVCK`; C Dateien anlegen, K Ordner + * anlegen). `null`/fehlend = unbekannt: dann wird nichts ausgeblendet, die Nextcloud entscheidet. + */ + permissions?: string | null; } /** Ordnerinhalt samt Speicherangaben. Der Pfad geht nur in der Query, nie im URL-Pfad. */ @@ -338,6 +343,8 @@ export interface NcShare { shareWithName: string | null; ownerId: string | null; ownerName: string | null; + /** Eigentuemer der Datei, nur bei Weitergaben (der Benutzer gibt etwas weiter, das ihm geteilt wurde). */ + fileOwnerName?: string | null; canEdit: boolean; canDelete: boolean; /** `YYYY-MM-DD` oder null. */ @@ -360,6 +367,8 @@ export interface NcSharee { /** Freigaberegeln der Nextcloud fuer den angemeldeten Benutzer (frisch gelesen). */ export interface NcSharePolicy { + /** Heutiges Datum (`YYYY-MM-DD`) nach der Uhr des Servers: Grundlage fuer Ablaufgrenzen. */ + today?: string; enabled: boolean; groupsEnabled: boolean; links: { diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index a549187..f1de93f 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -2444,7 +2444,8 @@ "sortBy": "Sortieren nach", "sortAscending": "Aufsteigend", "sortDescending": "Absteigend", - "reload": "Neu laden" + "reload": "Neu laden", + "readOnly": "Nur ansehen" }, "sort": { "name": "Name", @@ -2479,7 +2480,8 @@ }, "empty": { "title": "Dieser Ordner ist leer.", - "hint": "Dateien hierher ziehen oder hochladen." + "hint": "Dateien hierher ziehen oder hochladen.", + "readOnlyHint": "Hier lässt sich nichts hinzufügen." }, "loading": "Ordner wird geladen …", "folderSize": "Ordner", @@ -2500,7 +2502,8 @@ "hint": "Ordner werden mit ihrem Inhalt übernommen.", "tooMany": "Zu viele Dateien auf einmal. Legen Sie höchstens 2000 Dateien in einem Schritt ab.", "tooDeep": "Die Ordner sind zu tief verschachtelt. Mehr als 20 Ebenen lassen sich nicht ablegen.", - "unreadable": "Die abgelegten Dateien ließen sich nicht lesen. Bitte versuchen Sie es erneut." + "unreadable": "Die abgelegten Dateien ließen sich nicht lesen. Bitte versuchen Sie es erneut.", + "readOnly": "In diesen Ordner dürfen Sie nichts ablegen." }, "status": { "deleted": "{count, plural, one {„{name}“ ist im Papierkorb.} other {# Elemente in den Papierkorb verschoben.}}", @@ -2663,7 +2666,8 @@ "labelHint": "Hilft Ihnen, mehrere Links auseinanderzuhalten.", "cancel": "Abbrechen", "submitCreate": "Link erstellen", - "submitSave": "Speichern" + "submitSave": "Speichern", + "currentAccess": "Aktuell: {level}" } }, "codes": { @@ -2728,7 +2732,8 @@ "decline": "Ablehnen", "declineAria": "„{name}“ ablehnen", "truncated": "Es werden nicht alle Freigaben angezeigt. Den Rest sehen Sie in Nextcloud.", - "reload": "Neu laden" + "reload": "Neu laden", + "reshared": "Von Ihnen weitergegeben, Eigentümer: {owner}" } } } diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 0e740ce..eed32a6 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -2444,7 +2444,8 @@ "sortBy": "Sort by", "sortAscending": "Ascending", "sortDescending": "Descending", - "reload": "Reload" + "reload": "Reload", + "readOnly": "View only" }, "sort": { "name": "Name", @@ -2479,7 +2480,8 @@ }, "empty": { "title": "This folder is empty.", - "hint": "Drag files here or upload them." + "hint": "Drag files here or upload them.", + "readOnlyHint": "Nothing can be added here." }, "loading": "Loading folder …", "folderSize": "Folder", @@ -2500,7 +2502,8 @@ "hint": "Folders are uploaded with their contents.", "tooMany": "Too many files at once. Drop at most 2000 files in one go.", "tooDeep": "The folders are nested too deeply. More than 20 levels cannot be dropped.", - "unreadable": "The dropped files could not be read. Please try again." + "unreadable": "The dropped files could not be read. Please try again.", + "readOnly": "You may not drop files into this folder." }, "status": { "deleted": "{count, plural, one {“{name}” is in the trash.} other {# items moved to the trash.}}", @@ -2663,7 +2666,8 @@ "labelHint": "Helps you tell several links apart.", "cancel": "Cancel", "submitCreate": "Create link", - "submitSave": "Save" + "submitSave": "Save", + "currentAccess": "Current: {level}" } }, "codes": { @@ -2728,7 +2732,8 @@ "decline": "Decline", "declineAria": "Decline “{name}”", "truncated": "Not all shares are shown. You see the rest in Nextcloud.", - "reload": "Reload" + "reload": "Reload", + "reshared": "Passed on by you, owner: {owner}" } } } diff --git a/docs/anleitung-administration.md b/docs/anleitung-administration.md index eb806da..02dc556 100644 --- a/docs/anleitung-administration.md +++ b/docs/anleitung-administration.md @@ -398,7 +398,7 @@ Das Modul „Dateien“ (Gruppe Infrastruktur) aktivieren Sie wie jedes Modul im **Welche Adresse ist die richtige?** Gemeint ist die Adresse, von der die Anfragen des Tessera-Servers bei der Nextcloud ankommen. Meist ist das einfach die Adresse des Tessera-Servers. Läuft die Nextcloud hinter einem Proxy (zum Beispiel Nginx Proxy Manager), sieht sie unter Umständen nur die Adresse des Proxys. Im Zweifel schauen Sie im Zugriffsprotokoll des Webservers der Nextcloud nach, welche Adresse bei Tessera-Anfragen auftaucht. - **Adresse ändern:** Ändern Sie die Adresse, während Benutzer verbunden sind, fragt Tessera ausdrücklich nach. Nach dem Wechsel gelten alle bestehenden Verbindungen als abgelaufen, jeder meldet sich neu an (die alten App-Passwörter gelten nur für die alte Nextcloud und werden nie an die neue geschickt). Offene Browser-Anmeldungen werden verworfen. - **Benutzer löschen:** Löschen Sie einen Tessera-Benutzer, bleibt dessen App-Passwort in der Geräteliste seines Nextcloud-Kontos stehen. Entfernen Sie es dort (Einstellungen → Sicherheit), oder lassen Sie es vorher über „Abmelden“ in Tessera widerrufen. Ohne Tessera-Konto kann niemand den Zugang nutzen, aber er bleibt in Nextcloud eingetragen. -- **Teilen und Regeln für Links:** Die Benutzer können im Modul Dateien und Ordner mit Kollegen und Gruppen teilen und öffentliche Links erstellen. **Die Regeln dafür legen Sie in der Nextcloud fest, nicht in Tessera:** Dort unter „Verwaltungseinstellungen“ im Bereich „Teilen“ schalten Sie ein oder aus, ob Links erlaubt sind, ob ein Passwort für Links Pflicht ist (auch mit Ausnahmegruppen), ob Links ein Ablaufdatum bekommen, ob dieses Pflicht ist und nach wie vielen Tagen es spätestens gilt, ob Besucher in freigegebene Ordner hochladen dürfen (öffentliches Hochladen), ob mit Gruppen geteilt werden darf und ob neue Freigaben automatisch angenommen werden. Tessera liest diese Regeln **für jeden Benutzer bei jeder Aktion neu** aus der Nextcloud; eine Änderung wirkt deshalb nach wenigen Sekunden ohne Neustart und ohne Zwischenspeicher in Tessera. Verlangt die Nextcloud ein Passwort, fragt das Formular danach und bietet „Passwort erzeugen“ an; ein Pflicht-Ablaufdatum trägt es vor. Tessera erlaubt nie mehr, als die Nextcloud dem Benutzer erlaubt, und meldet Ablehnungen der Nextcloud auf Deutsch. **Die Adresse eines Links stammt aus der Nextcloud**, nicht aus Tessera. Tragen Sie deshalb in Tessera die Adresse ein, unter der die Nextcloud von außen erreichbar ist (die, die auch Kollegen und Kunden im Browser benutzen). Ist dort eine interne Adresse eingetragen, tragen die Links ebenfalls die interne Adresse und funktionieren für Außenstehende nicht. Alternativ legen Sie in der `config.php` der Nextcloud `overwritehost`, `overwriteprotocol` und `overwrite.cli.url` auf die externe Adresse fest, dann baut die Nextcloud ihre Links immer damit. Zum Schutz der Nextcloud legt Tessera je Benutzer höchstens 15 neue Freigaben in 10 Minuten an (Näheres im Betriebshandbuch). Freigaben per E-Mail und an andere Server verwalten die Benutzer nur in der Nextcloud selbst; Tessera zeigt sie nicht an. +- **Teilen und Regeln für Links:** Die Benutzer können im Modul Dateien und Ordner mit Kollegen und Gruppen teilen und öffentliche Links erstellen. **Die Regeln dafür legen Sie in der Nextcloud fest, nicht in Tessera:** Dort unter „Verwaltungseinstellungen“ im Bereich „Teilen“ schalten Sie ein oder aus, ob Links erlaubt sind, ob ein Passwort für Links Pflicht ist (auch mit Ausnahmegruppen), ob Links ein Ablaufdatum bekommen, ob dieses Pflicht ist und nach wie vielen Tagen es spätestens gilt, ob Besucher in freigegebene Ordner hochladen dürfen (öffentliches Hochladen), ob mit Gruppen geteilt werden darf und ob neue Freigaben automatisch angenommen werden. Tessera liest diese Regeln **für jeden Benutzer bei jeder Aktion neu** aus der Nextcloud; eine Änderung wirkt deshalb nach wenigen Sekunden ohne Neustart und ohne Zwischenspeicher in Tessera. Verlangt die Nextcloud ein Passwort, fragt das Formular danach und bietet „Passwort erzeugen“ an; ein Pflicht-Ablaufdatum trägt es vor. Tessera erlaubt nie mehr, als die Nextcloud dem Benutzer erlaubt, und meldet Ablehnungen der Nextcloud auf Deutsch. **Die Adresse eines Links stammt aus der Nextcloud**, nicht aus Tessera. Tragen Sie deshalb in Tessera die Adresse ein, unter der die Nextcloud von außen erreichbar ist (die, die auch Kollegen und Kunden im Browser benutzen). Ist dort eine interne Adresse eingetragen, tragen die Links ebenfalls die interne Adresse und funktionieren für Außenstehende nicht. Alternativ legen Sie in der `config.php` der Nextcloud `overwritehost`, `overwriteprotocol` und `overwrite.cli.url` auf die externe Adresse fest, dann baut die Nextcloud ihre Links immer damit. Zum Schutz der Nextcloud legt Tessera je Benutzer höchstens 10 neue Freigaben in 10 Minuten an (Näheres im Betriebshandbuch). Freigaben per E-Mail und an andere Server verwalten die Benutzer nur in der Nextcloud selbst; Tessera zeigt sie nicht an. - **Kein Dashboard-Widget:** Das Modul bringt keine Dashboard-Kachel mit. - **Betrieb:** Netzwerkfreigaben, eigene Zertifizierungsstellen, die Größenbegrenzung des Proxys für das Hochladen und Fehlerbilder stehen im Betriebshandbuch, Abschnitt „Dateien (Nextcloud)“. diff --git a/docs/anleitung-anwender.md b/docs/anleitung-anwender.md index f17c99c..df040b7 100644 --- a/docs/anleitung-anwender.md +++ b/docs/anleitung-anwender.md @@ -255,7 +255,7 @@ Das Modul zeigt Ihre Dateien aus der Firmen-Nextcloud direkt in Tessera: ansehen **Dateien ansehen:** Oben zeigt eine Pfadleiste, wo Sie sind; ein Klick auf einen Teil des Pfads springt dorthin. Über „Liste“ und „Raster“ wählen Sie die Ansicht (Tessera merkt sich Ihre Wahl), über „Sortieren nach“ die Reihenfolge nach Name, Größe oder Änderungsdatum. Jede Datei hat eine Kachel in der Farbe ihres Typs (zum Beispiel PDF, XLSX, ZIP); bei Bildern erscheint das Vorschaubild. Ein Doppelklick auf einen Ordner öffnet ihn. Ganz unten steht der belegte Speicherplatz. Ist Ihr Speicher in Nextcloud unbegrenzt, steht dort nur, wie viel belegt ist. -**Arbeiten mit Dateien:** Über „Neuer Ordner“ legen Sie einen Ordner an. Über das Menü an jeder Zeile (oder mit der rechten Maustaste) finden Sie Öffnen, Herunterladen, Teilen, Umbenennen, Verschieben, „In Nextcloud öffnen“ und Löschen. Beim Verschieben wählen Sie den Zielordner in einer Ordnerauswahl. **Löschen** verschiebt in den Papierkorb Ihrer Nextcloud, nach einer Rückfrage; dort holen Sie die Dateien bei Bedarf wieder heraus. Alles gilt auch für mehrere Einträge: Setzen Sie Häkchen oder halten Sie Strg (Umschalt für einen Bereich) gedrückt; eine Leiste oben nennt dann die Anzahl und bietet Herunterladen, Verschieben und Löschen für die ganze Auswahl. +**Arbeiten mit Dateien:** Über „Neuer Ordner“ legen Sie einen Ordner an. Über das Menü an jeder Zeile (oder mit der rechten Maustaste) finden Sie Öffnen, Herunterladen, Teilen, Umbenennen, Verschieben, „In Nextcloud öffnen“ und Löschen. **Was Ihnen nicht erlaubt ist, fehlt:** In einem Ordner, den andere nur zum Ansehen mit Ihnen geteilt haben, gibt es kein „Neuer Ordner“ und kein „Hochladen“ (stattdessen steht dort „Nur ansehen“), Dateien lassen sich dort nicht ablegen, und im Menü der Einträge fehlen Umbenennen, Verschieben und Löschen. Herunterladen geht immer. Beim Verschieben wählen Sie den Zielordner in einer Ordnerauswahl. **Löschen** verschiebt in den Papierkorb Ihrer Nextcloud, nach einer Rückfrage; dort holen Sie die Dateien bei Bedarf wieder heraus. Alles gilt auch für mehrere Einträge: Setzen Sie Häkchen oder halten Sie Strg (Umschalt für einen Bereich) gedrückt; eine Leiste oben nennt dann die Anzahl und bietet Herunterladen, Verschieben und Löschen für die ganze Auswahl. **Hochladen:** Ziehen Sie Dateien oder ganze Ordner aus dem Dateimanager auf die Seite. Eine gestreifte Fläche nennt das Ziel („In „…“ ablegen“); legen Sie die Dateien auf eine Ordnerzeile, landen sie in diesem Ordner. Alternativ klicken Sie auf „Hochladen“ und wählen Dateien aus. Große Dateien gehen automatisch in Stücken hoch. Am unteren Rand erscheint die **Übertragungsleiste**: Jede Datei hat einen Fortschrittsbalken, „Abbrechen“ stoppt sie, „Erneut versuchen“ wiederholt eine fehlgeschlagene, „Erledigte entfernen“ räumt auf. Die Leiste lässt sich ein- und ausklappen. @@ -263,19 +263,19 @@ Das Modul zeigt Ihre Dateien aus der Firmen-Nextcloud direkt in Tessera: ansehen **Herunterladen:** Eine einzelne Datei lädt Ihr Browser direkt herunter. Ordner und Mehrfachauswahl kommen als ZIP-Datei; haben Sie alles in einem Ordner ausgewählt, kommt der ganze Ordner als ZIP. Für eine ZIP-Datei aus einer Auswahl nimmt die Nextcloud nur eine begrenzte Zahl von Einträgen an (je nach Länge der Namen etwa 100 bis 200); bei mehr meldet Tessera das sofort, wählen Sie dann weniger aus oder laden Sie den ganzen Ordner herunter. Klappt ein Herunterladen nicht (zum Beispiel, weil die Datei inzwischen gelöscht wurde), steht der Grund in der Zeile unter der Dateiliste; ist die Verbindung abgelaufen, sehen Sie wieder den Anmeldebildschirm. -**Teilen:** Öffnen Sie das Menü einer Datei oder eines Ordners (oder klicken Sie mit der rechten Maustaste darauf) und wählen Sie „Teilen“. Es öffnet sich ein Fenster. Im Abschnitt „Personen und Gruppen“ tippen Sie mindestens zwei Buchstaben eines Namens in das Feld „Person oder Gruppe suchen“; darunter erscheinen die Treffer aus Ihrer Nextcloud, Gruppen sind als „Gruppe“ gekennzeichnet. Wählen Sie vorher unter „Berechtigung für die nächste Freigabe“, ob die Person die Datei nur „Ansehen“ oder auch „Bearbeiten“ darf, und klicken Sie dann auf den Treffer. Die Freigabe ist sofort angelegt, und die Nextcloud benachrichtigt die Kollegin oder den Kollegen selbst. Der Empfänger steht danach in der Liste darunter. Dort ändern Sie die Berechtigung jederzeit im Auswahlfeld hinter dem Namen, und mit „Freigabe für … entfernen“ nehmen Sie die Freigabe wieder zurück; die Datei selbst bleibt dabei unverändert. Wer schon eine Freigabe hat, erscheint in der Suche mit dem Vermerk „bereits geteilt“. In der Dateiliste trägt jeder geteilte Eintrag ein kleines Freigabe-Symbol; ein Klick darauf öffnet das Teilen-Fenster. Eingehende Freigaben, also Einträge, die andere mit Ihnen geteilt haben, tragen ein anderes Symbol („Mit Ihnen geteilt“). Einträge, die andere mit Ihnen geteilt haben, können Sie nicht weiterteilen; dort fehlt „Teilen“ im Menü. +**Teilen:** Öffnen Sie das Menü einer Datei oder eines Ordners (oder klicken Sie mit der rechten Maustaste darauf) und wählen Sie „Teilen“. Es öffnet sich ein Fenster. Im Abschnitt „Personen und Gruppen“ tippen Sie den Anfang eines Namens in das Feld „Person oder Gruppe suchen“ (je nach Einstellung Ihrer Nextcloud ab einem oder zwei Zeichen; ist es noch zu wenig, sagt das Fenster es); darunter erscheinen die Treffer aus Ihrer Nextcloud, Gruppen sind als „Gruppe“ gekennzeichnet. Wählen Sie vorher unter „Berechtigung für die nächste Freigabe“, ob die Person die Datei nur „Ansehen“ oder auch „Bearbeiten“ darf, und klicken Sie dann auf den Treffer. Die Freigabe ist sofort angelegt, und die Nextcloud benachrichtigt die Kollegin oder den Kollegen selbst. Der Empfänger steht danach in der Liste darunter. Dort ändern Sie die Berechtigung jederzeit im Auswahlfeld hinter dem Namen, und mit „Freigabe für … entfernen“ nehmen Sie die Freigabe wieder zurück; die Datei selbst bleibt dabei unverändert. Wer schon eine Freigabe hat, erscheint in der Suche mit dem Vermerk „bereits geteilt“. In der Dateiliste trägt jeder geteilte Eintrag ein kleines Freigabe-Symbol; ein Klick darauf öffnet das Teilen-Fenster. Eingehende Freigaben, also Einträge, die andere mit Ihnen geteilt haben, tragen ein anderes Symbol („Mit Ihnen geteilt“). Einträge, die andere mit Ihnen geteilt haben, können Sie nur weiterteilen, wenn die Freigabe das erlaubt; sonst fehlt „Teilen“ im Menü. Was Sie weitergeben, steht danach unter „Von mir geteilt“ mit dem Hinweis „Von Ihnen weitergegeben“ und dem Eigentümer der Datei. **Link erstellen:** Mit einem öffentlichen Link kann jeder, der die Adresse kennt, den Inhalt öffnen, auch ohne Nextcloud-Konto. Im Teilen-Fenster klicken Sie im Abschnitt „Link“ auf „Link erstellen“ und wählen, was andere tun dürfen: „Ansehen“ (ansehen und herunterladen), „Bearbeiten“ (Dateien ändern, bei Ordnern auch hinzufügen und löschen) oder, nur bei Ordnern, „Nur hochladen“. „Nur hochladen“ wirkt wie ein Briefkasten: Andere legen Dateien in den Ordner, sehen aber nicht, was darin liegt. Mit „Link kopieren“ legen Sie die Adresse in die Zwischenablage; ein kurzes „Kopiert“ bestätigt das. Das Passwort und das Ablaufdatum bestimmt die Nextcloud Ihrer Firma, nicht Tessera: - Verlangt die Nextcloud ein Passwort, heißt das Feld „Passwort (Ihre Nextcloud verlangt eines)“, und Sie kommen ohne Passwort nicht weiter. Mit „Passwort erzeugen“ lässt Tessera ein sicheres Passwort einsetzen; „Anzeigen“ macht es lesbar. Ist ein Passwort nicht Pflicht, können Sie den Link trotzdem mit „Mit Passwort schützen“ absichern. - Verlangt die Nextcloud ein Ablaufdatum, steht das im Formular, und das späteste erlaubte Datum ist genannt („Spätestens am …“). Sonst ist das Ablaufdatum freiwillig, und „Ohne Ablaufdatum“ ist in Ordnung. - Geben Sie das Passwort getrennt vom Link weiter, zum Beispiel in einer zweiten Nachricht. **Tessera speichert das Passwort nie und kann es später nicht mehr anzeigen:** Es steht nur, solange das Fenster nach dem Erstellen offen ist, in einem Hinweisfeld mit „Passwort kopieren“. Mit „Verstanden“ schließen Sie das Feld. Haben Sie es vergessen, vergeben Sie über „Ändern“ und „Passwort ändern“ einfach ein neues. -- Mit „Ändern“ passen Sie Berechtigung, Ablaufdatum, Passwort und die freiwillige Bezeichnung an (die Bezeichnung hilft, mehrere Links zu unterscheiden), mit „Löschen“ und der Rückfrage „Ja, löschen“ ist der Link sofort ungültig. Wer die Adresse hat, kommt danach nicht mehr an die Datei. +- Mit „Ändern“ passen Sie Berechtigung, Ablaufdatum, Passwort und die freiwillige Bezeichnung an (die Bezeichnung hilft, mehrere Links zu unterscheiden). Lässt die Nextcloud die bisherige Berechtigung eines Links nicht mehr zur Auswahl zu (zum Beispiel „Bearbeiten“, nachdem das öffentliche Hochladen ausgeschaltet wurde), steht sie als „Aktuell: …“ da und bleibt beim Speichern unverändert, bis Sie ausdrücklich eine andere wählen. Mit „Löschen“ und der Rückfrage „Ja, löschen“ ist der Link sofort ungültig. Wer die Adresse hat, kommt danach nicht mehr an die Datei. - Ob Tessera Links anbietet, entscheidet die Nextcloud. Hat Ihr Administrator öffentliche Links dort ausgeschaltet, sagt das Fenster es Ihnen. **Von mir geteilt und Mit mir geteilt:** Über den Reitern oben neben „Dateien“ finden Sie diese zwei Übersichten. „Von mir geteilt“ listet alles, was Sie geteilt haben, geordnet nach Datei oder Ordner, mit den Empfängern („Geteilt mit …“, „Link“, „Gruppe …“). „Freigaben bearbeiten“ öffnet das Teilen-Fenster, „Im Ordner zeigen“ springt in den Ordner und markiert den Eintrag. „Mit mir geteilt“ listet, was andere mit Ihnen geteilt haben, mit dem Namen des Absenders („von …“); „Öffnen“ bringt Sie in den passenden Ordner. Eine Freigabe, die Sie nicht mehr brauchen, entfernen Sie mit „Freigabe verlassen“ (nach einer Rückfrage); sie verschwindet dann aus Ihrer Nextcloud, und die Absenderin müsste sie bei Bedarf neu teilen. Hat die Nextcloud so eingestellt, dass Sie neue Freigaben zuerst bestätigen müssen, stehen sie oben unter „Noch nicht angenommen“: „Annehmen“ übernimmt sie in Ihre Dateien, „Ablehnen“ weist sie ab. Zeigt eine Übersicht nur einen Teil, steht ein Hinweis darüber, und den Rest sehen Sie in Nextcloud. -**Grenzen beim Teilen:** Zum Schutz der Nextcloud legt Tessera höchstens 15 neue Freigaben innerhalb von 10 Minuten an; danach bittet eine Meldung, einige Minuten zu warten. Freigaben, die per E-Mail oder an einen anderen Server gehen, können Sie in Tessera weder anlegen noch bearbeiten; die sehen und verwalten Sie nur in Nextcloud (das Teilen-Fenster weist mit einer Zeile darauf hin, wenn es solche Freigaben gibt). +**Grenzen beim Teilen:** Zum Schutz der Nextcloud legt Tessera höchstens 10 neue Freigaben innerhalb von 10 Minuten an; danach bittet eine Meldung, einige Minuten zu warten. Dieselbe Meldung kommt, wenn Sie in kurzer Zeit sehr oft versuchen, eine Freigabe anzulegen, auch wenn die Nextcloud sie ablehnt (mehr als 40 Versuche in 10 Minuten). Freigaben, die per E-Mail oder an einen anderen Server gehen, können Sie in Tessera weder anlegen noch bearbeiten; die sehen und verwalten Sie nur in Nextcloud (das Teilen-Fenster weist mit einer Zeile darauf hin, wenn es solche Freigaben gibt). **Tastatur:** diff --git a/docs/anleitung-betrieb.md b/docs/anleitung-betrieb.md index 0db1926..593b48c 100644 --- a/docs/anleitung-betrieb.md +++ b/docs/anleitung-betrieb.md @@ -192,7 +192,7 @@ Das Modul „Dateien“ spricht aus dem `api`-Container mit der Nextcloud der In - **Nginx Proxy Manager (Tessera-Adresse):** Beim Hochladen schickt der Browser große Dateien in Stücken von 8 MiB durch `/api-proxy`. Der Proxy vor Tessera muss das durchlassen: `client_max_body_size` mindestens `10m` (besser `64m`) und Lese- und Sendezeitlimits (`proxy_read_timeout`, `proxy_send_timeout`) von mindestens 120 Sekunden. Bei der Nginx-Voreinstellung von 1 MiB bricht jeder größere Upload schon beim ersten Stück ab. - **Zusammenbau großer Dateien:** Nextcloud setzt die Stücke am Ende zu einer Datei zusammen. Dauert das länger als etwa 20 Sekunden, antwortet Tessera sofort und die Oberfläche fragt den Fortschritt ab; die Verbindung bleibt also nie lange offen. Unvollständige Uploads (Stücke ohne Abschluss, etwa nach einem Seitenwechsel) räumt Nextcloud nach 24 Stunden selbst auf. - **Zustand im Arbeitsspeicher:** Offene Browser-Anmeldungen (Zwei-Faktor), laufende Zusammenbauten, die Aufrufsperre gegen die Brute-Force-Sperre der Nextcloud, die zwischengespeicherte Kennung der Nextcloud (10 Minuten) und noch ausstehende Widerrufe von App-Passwörtern liegen im Prozess des `api`-Containers. Ein Widerruf steht aus, wenn Nextcloud ihn gerade nicht annehmen konnte (Sperre nach „zu viele Anfragen“, Netzfehler, Wartung); Tessera versucht ihn nach dem Ende der Sperre bzw. nach einer Minute erneut, höchstens sechsmal. Ebenso beobachtet Tessera eine abgebrochene Browser-Anmeldung bis zu ihrem Ablauf und widerruft ein dort doch noch ausgestelltes App-Passwort sofort. Ein Neustart verwirft all das: Wer gerade eine Browser-Anmeldung offen hatte, startet sie neu; angemeldete Benutzer bleiben angemeldet (ihre Zugänge liegen verschlüsselt in der Datenbank). Ein dabei verlorener Widerruf bleibt als Gerät im Nextcloud-Konto des Benutzers stehen und lässt sich dort entfernen. -- **Teilen:** Tessera legt je Benutzer höchstens 15 neue Freigaben innerhalb von 10 Minuten an. Das liegt bewusst unter dem eigenen Limit der Nextcloud von 20 in 10 Minuten: Läge Tessera darüber, meldete die Nextcloud „zu viele Anfragen“, und Tessera hielte dann alle Anfragen an die Nextcloud für 15 Minuten an, für alle Benutzer. Der Zähler zählt auch Versuche, die die Nextcloud ablehnt, liegt im Arbeitsspeicher des `api`-Containers und beginnt nach einem Neustart bei null. Die Regeln für Links (Passwort, Ablaufdatum, öffentliches Hochladen, Gruppen) liest Tessera bei jeder Aktion frisch aus der Nextcloud und speichert nichts davon; eine Änderung in der Nextcloud wirkt nach wenigen Sekunden ohne Neustart (die Nextcloud selbst hält ihre Angaben nach einer Änderung per `occ` kurz in einem Zwischenspeicher). Passwörter von Links speichert und protokolliert Tessera nie, und auch die Adressen der Links stehen in keinem Protokoll. Die Adresse eines Links baut die Nextcloud aus dem Namen, unter dem Tessera sie aufruft (Administrationshandbuch, Abschnitt „Dateien: Nextcloud anbinden“). +- **Teilen:** Tessera legt je Benutzer höchstens 10 neue Freigaben innerhalb von 10 Minuten an. Das liegt bewusst deutlich unter dem eigenen Limit der Nextcloud von 20 in 10 Minuten: Läge Tessera darüber, meldete die Nextcloud „zu viele Anfragen“, und Tessera hielte dann alle Anfragen an die Nextcloud für 15 Minuten an, für alle Benutzer. Der Zähler liegt im Arbeitsspeicher des `api`-Containers und beginnt nach einem Neustart bei null; die Zusicherung gilt also nur, solange der Container nicht neu startet. Freigaben, die jemand direkt in der Nextcloud anlegt, zählen gegen deren Limit von 20, ohne dass Tessera davon weiß; die Luft zwischen 10 und 20 ist dafür gedacht. Zusätzlich zählt Tessera jeden Versuch, eine Freigabe anzulegen, auch einen abgelehnten (zum Beispiel „gibt es schon“): mehr als 40 Versuche in 10 Minuten je Benutzer weist Tessera ab, ohne die Nextcloud zu fragen, damit wiederholte Fehlversuche sie nicht belasten. Die Regeln für Links (Passwort, Ablaufdatum, öffentliches Hochladen, Gruppen) liest Tessera bei jeder Aktion frisch aus der Nextcloud und speichert nichts davon; eine Änderung in der Nextcloud wirkt nach wenigen Sekunden ohne Neustart (die Nextcloud selbst hält ihre Angaben nach einer Änderung per `occ` kurz in einem Zwischenspeicher). Passwörter von Links speichert und protokolliert Tessera nie, und auch die Adressen der Links stehen in keinem Protokoll. Die Adresse eines Links baut die Nextcloud aus dem Namen, unter dem Tessera sie aufruft (Administrationshandbuch, Abschnitt „Dateien: Nextcloud anbinden“). - **Brute-Force-Ausnahme in der Nextcloud:** Tragen Sie die Adresse des Tessera-Servers dort in die Ausnahmeliste ein (Administrationshandbuch, Abschnitt „Dateien: Nextcloud anbinden“). Sonst kann eine Reihe falscher Anmeldungen die Nextcloud für alle Benutzer gleichzeitig sperren. - **Verschlüsselung:** Die gespeicherten App-Passwörter sind mit `TESSERA_ENCRYPTION_KEY` verschlüsselt (Kapitel 2). Ein anderer Schlüssel macht sie unlesbar; die Benutzer müssen sich dann neu verbinden. @@ -766,4 +766,4 @@ Für die Desktop-Auslieferung ist keine neue Pflichtvariable nötig. | Eine Fehlermeldung aus der Desktop-App nennt als Herkunft „Desktop-App (unbekannt)“ ohne Version, Betreff-Kürzel `[Desktop]` | Der Client ist älter als diese Fassung: er meldet dem Server beim Start nur `desktop=1`, nicht Version, Stand und Betriebssystem (Parameter `dv`, `dc`, `dos`, aus denen `web` das Cookie `tessera_desktop_client` bildet) | Kein Fehler, die Meldung ist trotzdem als Desktop-App erkennbar. Client über „Auf Version … aktualisieren“ im Infobereich oder den Browser-Installer aktualisieren; danach stehen Betriebssystem, Version und Stand in der Meldung. | | Beim Hochladen in „Dateien“ bricht jede größere Datei beim ersten 8-MB-Stück ab (Fehler „Die Verbindung wurde unterbrochen“, im Proxy-Protokoll `413`) | Der Proxy vor Tessera (Nginx Proxy Manager) lässt keine Anfragen über seiner Größengrenze durch (`client_max_body_size`), oder sein Zeitlimit ist zu kurz | Für die Tessera-Adresse `client_max_body_size` auf mindestens `10m` und die Lese-/Sendezeitlimits auf mindestens 120 Sekunden stellen (Kapitel 3, Abschnitt „Dateien (Nextcloud)“). | | In „Dateien“ tragen öffentliche Links eine interne Adresse (zum Beispiel ein interner Rechnername oder eine IP-Adresse) und lassen sich von außen nicht öffnen | Die Nextcloud baut die Adresse eines Links aus dem Namen, unter dem Tessera sie aufruft; in Tessera ist die interne Adresse der Nextcloud eingetragen | In den Einstellungen des Moduls die von außen erreichbare Adresse der Nextcloud eintragen, oder in der `config.php` der Nextcloud `overwritehost`, `overwriteprotocol` und `overwrite.cli.url` auf die externe Adresse setzen; bereits erstellte Links ändern sich nicht rückwirkend, sie müssen neu erstellt werden. | -| In „Dateien“ erscheint „Sie haben in kurzer Zeit viele Freigaben angelegt. Bitte warten Sie einige Minuten.“ | Tessera hat für den Benutzer 15 neue Freigaben innerhalb von 10 Minuten angelegt (oder Versuche gezählt, die die Nextcloud abgelehnt hat) | Einige Minuten abwarten (der Zähler läuft nach 10 Minuten ab); ein Neustart des `api`-Containers setzt den Zähler zurück, ist aber nur im Ausnahmefall nötig. | +| In „Dateien“ erscheint „Sie haben in kurzer Zeit viele Freigaben angelegt. Bitte warten Sie einige Minuten.“ | Tessera hat für den Benutzer 10 neue Freigaben innerhalb von 10 Minuten angelegt oder mehr als 40 Versuche in 10 Minuten gezählt (auch abgelehnte) | Einige Minuten abwarten (der Zähler läuft nach 10 Minuten ab); ein Neustart des `api`-Containers setzt den Zähler zurück, ist aber nur im Ausnahmefall nötig. | diff --git a/docs/anleitung-entwicklung.md b/docs/anleitung-entwicklung.md index c40c160..2fc6e3b 100644 --- a/docs/anleitung-entwicklung.md +++ b/docs/anleitung-entwicklung.md @@ -836,12 +836,31 @@ Browser an. 401 und 403 verlassen die API nie. Die Datums-Eingabe prüft der DTO Zwei Fallen beim Anlegen: Ein `POST` für einen Empfänger, der die Freigabe schon hat, liefert die alte Freigabe zurück und **verschickt die Benachrichtigung erneut**. Der Dienst prüft deshalb vorab über `shares/by-path`, ob es sie schon gibt (`shareAlreadyExists`, 409), und ändert Berechtigungen -immer per `PUT`. Außerdem begrenzt Tessera selbst auf 15 neue Freigaben je Benutzer in 10 Minuten -(`checkShareCreate`, im Arbeitsspeicher, zählt auch Versuche, die die Nextcloud ablehnt; -unmittelbar vor dem `POST`, nach allen Vorprüfungen). Das liegt absichtlich unter dem Limit der +immer per `PUT`. Außerdem begrenzt Tessera selbst auf 10 neue Freigaben je Benutzer in 10 Minuten +(`checkShareCreate`, im Arbeitsspeicher eines Prozesses, zählt auch Versuche, die die Nextcloud +ablehnt; unmittelbar vor dem `POST`, nach allen Vorprüfungen). Davor zählt `checkShareAttempt` +jeden Versuch, auch abgelehnte (40 in 10 Minuten), und zwar vor den Abfragen an die Nextcloud: +so können wiederholte, abgelehnte Anfragen („gibt es schon“) sie nicht beliebig oft belasten. +Beide Zähler räumen leere Einträge regelmäßig weg. Die Zusicherung gilt nur je Prozess ohne Neustart, +und Freigaben, die direkt in der Nextcloud entstehen, zählen gegen deren Limit, ohne dass Tessera +es weiß; deshalb 10 statt 20. Das liegt absichtlich unter dem Limit der Nextcloud (20 in 600 s): Deren 429 würde die Aufrufsperre des Ursprungs auslösen, und die hielte die Anfragen aller Benutzer 15 Minuten an. Die Oberfläche legt immer nur eine Freigabe zugleich an. +Gemessen nach der Prüfung (Nextcloud 34.0.4): `uid_owner` einer Freigabe ist der **Freigebende**, +`uid_file_owner` der Eigentümer der Datei. Gibt Ben einen Ordner weiter, den Anna ihm geteilt hat, +steht bei ihm `uid_owner = ben`, `uid_file_owner = anna`, `path` im Baum von Ben und `file_target` im +Baum des Empfängers. Tessera behandelt das als eigene Freigabe von Ben (Pfad = `path`, Hinweis +„Von Ihnen weitergegeben“); als eingehend gilt nur, was ein anderer freigegeben hat. Pfade, Ziele +und Empfängerkennungen gehen unverändert zurück an die Nextcloud und werden nie bereinigt +(`verbatimId`; ein doppeltes oder nachgestelltes Leerzeichen gehört zum Namen); nur Anzeigetexte +laufen durch `cleanText`. Eine offene Freigabe lehnt man mit `DELETE shares/{id}` ab (200; `DELETE +shares/pending/{id}` ergibt 405). Die Berechtigungsbuchstaben eines Ordners kommen mit seiner +eigenen Zeile in der Listenantwort (`permissions` der Liste): eigener Ordner `RGDNVCK`, zum Ansehen +geteilter Ordner selbst `SGDN`, sein Inhalt `SG`; die Dateiansicht blendet damit „Neuer Ordner“ +(`K`), „Hochladen“ und Ablegen (`C`) und je Eintrag Umbenennen (`N`), Verschieben (`V`) und +Löschen (`D`) aus. Fehlen die Buchstaben, bleibt alles sichtbar (unbekannt ist nie verboten). + Live-Test: `.planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh [people|links|received|version|all]` gegen die Test-Nextcloud (`nc-test-setup.sh` der Etappe 1 vorher). Er legt Benutzer `ben` und Gruppe `tessera-team` an, schaltet die Ratenbegrenzung der @@ -849,5 +868,5 @@ Nextcloud (`ratelimit.protection.enabled`) nur für den Lauf aus und stellt die `occ config:app:set core …` um (`shareapi_enforce_links_password`, `shareapi_default_expire_date`, `shareapi_enforce_expire_date`, `shareapi_expire_after_n_days`; für die offenen Freigaben `user:setting anna files_sharing default_accept`). Alles setzt der `trap` zurück. Der Zähler von -Tessera liegt im Prozess: `all` verbraucht 7 der 15 Plätze, nach höchstens zwei `all`-Läufen +Tessera liegt im Prozess: `all` verbraucht 7 der 10 Plätze, nach jedem `all`-Lauf `docker compose restart api`.