docs(quick-261009-dkv): Dateien Etappe 2a Teilen
This commit is contained in:
+4
-4
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
|||||||
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
|
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
|
||||||
Plan: 6 of 6
|
Plan: 6 of 6
|
||||||
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
|
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
|
||||||
Last activity: 2026-10-09 - Quick 261008-who Eigene Module vorladen
|
Last activity: 2026-10-09 - Completed quick task 261009-dkv: Dateien Etappe 2a Teilen (lokal, nicht gepusht)
|
||||||
|
|
||||||
Progress: [██████████] 99%
|
Progress: [██████████] 99%
|
||||||
|
|
||||||
@@ -544,8 +544,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-10-02T09:10:00Z
|
Last session: 2026-10-09T09:40:00Z
|
||||||
Resumed: 2026-10-02 ueber /gsd-resume-work (HANDOFF nach Freigabe 1.9.2 eingelesen und entfernt).
|
Resumed: 2026-10-09 ueber /gsd-resume-work (HANDOFF eingelesen und entfernt; CI 41a5754 gruen).
|
||||||
Stopped at: Session resumed — wartet auf Rueckmeldung live/alpha auf 1.9.2 und den Auftrag fuer das neue Modul.
|
Stopped at: Teilen fertig (261009-dkv, lokal). Naechstes laut User 09.10.: 2) Cert-Manager-Umbau, 3) Anleitungen pruefen/nacharbeiten; danach Sicherheitsprotokoll/CI-Scanner, Dateien-Suche.
|
||||||
Resume file: None
|
Resume file: None
|
||||||
Last activity: 2026-09-29 - Quick 260929-if2 Erinnerungen-Widget (lokal, nicht gepusht); v1.7.0 auf alpha+live
|
Last activity: 2026-09-29 - Quick 260929-if2 Erinnerungen-Widget (lokal, nicht gepusht); v1.7.0 auf alpha+live
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
# Quick Task 261009-dkv: Modul Dateien Etappe 2a: Teilen von Dateien und Ordnern ueber Nextcloud - Context
|
||||||
|
|
||||||
|
**Gathered:** 2026-10-09
|
||||||
|
**Status:** Ready for planning
|
||||||
|
|
||||||
|
<domain>
|
||||||
|
## Task Boundary
|
||||||
|
|
||||||
|
Module "Dateien" (slug `nextcloud-files`), Etappe 2a: sharing. Users share files and folders of their
|
||||||
|
own Nextcloud account from inside Tessera (Nextcloud OCS Files Sharing API, always under the caller's
|
||||||
|
own app password), see existing shares, change them and remove them. Builds on quick 261008-mzu
|
||||||
|
(Etappe 1: browse/upload/download/rename/move/delete). Search is NOT part of this task (later quick).
|
||||||
|
Module version bump + module changelog + user/admin docs are part of the task.
|
||||||
|
|
||||||
|
</domain>
|
||||||
|
|
||||||
|
<decisions>
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
### Share targets
|
||||||
|
- Both: share with colleagues (Nextcloud users AND groups, found via Nextcloud's sharee search) and
|
||||||
|
public links (to hand to customers etc.).
|
||||||
|
|
||||||
|
### Link protection
|
||||||
|
- Follow the Nextcloud server policy — do not invent Tessera-side rules. User believes the company
|
||||||
|
Nextcloud enforces a password for links but NOT an expiry date. Tessera must read the policy from
|
||||||
|
the Nextcloud capabilities (password enforced, expiry enforced/default days, etc.) and reflect it
|
||||||
|
in the form (required fields, defaults, maximums); Nextcloud's error messages on policy violations
|
||||||
|
must be shown understandably in German. Optional fields (expiry when not enforced) remain freely
|
||||||
|
settable.
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
- Simple choice: "Ansehen" (read only) or "Bearbeiten" (edit). For folders additionally
|
||||||
|
"Nur hochladen" (file drop / Briefkasten, mainly for public links). No per-bit checkboxes.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
- Both views: "Von mir geteilt" and "Mit mir geteilt" as separate views in the module, plus a share
|
||||||
|
indicator on every shared entry in the file list. Shares can be opened from both places to change
|
||||||
|
or remove them.
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Copy-link button, optional link label/note, notification behaviour (Nextcloud's own behaviour for
|
||||||
|
user/group shares is fine), how "Mit mir geteilt" items are opened/navigated, accepting/declining
|
||||||
|
pending incoming shares if the server requires it, UI layout of the share dialog (follow existing
|
||||||
|
dialog patterns of the module), error mapping, rate/size limits, test strategy.
|
||||||
|
|
||||||
|
</decisions>
|
||||||
|
|
||||||
|
<specifics>
|
||||||
|
## Specific Ideas
|
||||||
|
|
||||||
|
- Etappe 1 was prepared for this: DAV layer keeps a generic `davRequest`, auth client an `ocsRequest`,
|
||||||
|
entries keep Nextcloud permission letters (R = shareable), the row menu takes an action list.
|
||||||
|
- Respect all Etappe-1 safety rules (fixed path prefixes, no redirects, no cookies, call gate on 429,
|
||||||
|
401 handling / account marked expired, German error contract, RLS/tenant rules).
|
||||||
|
- Local test Nextcloud container `tessera-nc-test` (http://172.17.0.1:18080) exists for e2e tests;
|
||||||
|
its sharing policy can be set via occ to test "password enforced" behaviour.
|
||||||
|
|
||||||
|
</specifics>
|
||||||
|
|
||||||
|
<canonical_refs>
|
||||||
|
## Canonical References
|
||||||
|
|
||||||
|
- .planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/261008-mzu-PLAN.md (Etappe 1 design decisions D-A..D-O)
|
||||||
|
- .planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/261008-mzu-SUMMARY.md
|
||||||
|
- .planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/261008-mzu-REVIEW.md
|
||||||
|
- Nextcloud OCS Share API docs (developer manual: client APIs / OCS share API, sharee API)
|
||||||
|
|
||||||
|
</canonical_refs>
|
||||||
+396
@@ -0,0 +1,396 @@
|
|||||||
|
---
|
||||||
|
phase: quick-261009-dkv
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
quick_id: 261009-dkv
|
||||||
|
description: "Modul Dateien (nextcloud-files) Etappe 2a: Teilen von Dateien und Ordnern ueber Nextcloud (Personen, Gruppen, oeffentliche Links nach der Nextcloud-Richtlinie), Ansichten Von mir geteilt / Mit mir geteilt, Freigabe-Kennzeichen in der Dateiliste, Modulversion bleibt 1.0.0 (unveroeffentlichter Eintrag ergaenzt), Modul-Changelog, CHANGELOG und alle vier Anleitungen"
|
||||||
|
date: 2026-10-09
|
||||||
|
files_modified:
|
||||||
|
# Task 1 — tracer: share with people and groups end to end, share indicator
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-shares.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-shares.spec.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-files-shares.service.spec.ts
|
||||||
|
- apps/api/src/nextcloud-files/dto/nextcloud-files-shares.dto.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-files.types.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-login-guard.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-login-guard.spec.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-propfind.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-propfind.spec.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-files.controller.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-files.controller.spec.ts
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-files.module.ts
|
||||||
|
- apps/api/src/module-registry/module-manage-handlers.spec.ts
|
||||||
|
- apps/web/src/lib/nextcloud-files-api.ts
|
||||||
|
- apps/web/src/lib/nextcloud-files-api.test.ts
|
||||||
|
- apps/web/src/components/nextcloud-files/share-policy.ts
|
||||||
|
- apps/web/src/components/nextcloud-files/share-policy.test.ts
|
||||||
|
- apps/web/src/components/nextcloud-files/error-text.ts
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareDialog.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareDialog.test.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareIndicator.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/FileList.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/FileGrid.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/icons.tsx
|
||||||
|
- apps/web/src/messages/de.json
|
||||||
|
- apps/web/src/messages/en.json
|
||||||
|
- apps/web/src/messages/umlaut-dictionary.ts
|
||||||
|
- .planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh
|
||||||
|
# Task 2 — links under the Nextcloud policy, the two share views, incoming/pending shares
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/LinkShareForm.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/SharesView.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/components/SharesView.test.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx
|
||||||
|
- apps/web/src/app/(portal)/modules/nextcloud-files/nextcloud-files-page.test.tsx
|
||||||
|
# Task 3 — module version, changelogs, guides, full gates, browser proof
|
||||||
|
- apps/api/src/nextcloud-files/nextcloud-files.changelog.ts
|
||||||
|
- apps/api/src/module-registry/module-changelog.spec.ts
|
||||||
|
- CHANGELOG.md
|
||||||
|
- docs/anleitung-anwender.md
|
||||||
|
- docs/anleitung-administration.md
|
||||||
|
- docs/anleitung-betrieb.md
|
||||||
|
- docs/anleitung-entwicklung.md
|
||||||
|
autonomous: true
|
||||||
|
requirements: [QUICK-261009-dkv]
|
||||||
|
|
||||||
|
estimate:
|
||||||
|
tokens: 170000
|
||||||
|
raw_tokens: 170000
|
||||||
|
tasks: 3
|
||||||
|
confidence: low
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "A connected user opens „Teilen“ from the row menu (or the share indicator) of a file or folder that Nextcloud marks as shareable (permission letter R), finds colleagues AND groups through Nextcloud's own sharee search, adds them with „Ansehen“ or „Bearbeiten“, changes the permission and removes the share again; the recipient sees exactly these shares in Nextcloud (live e2e with the test users ben and the group tessera-team)"
|
||||||
|
- "A user creates public links for files and folders („Ansehen“, „Bearbeiten“, for folders also „Nur hochladen“), copies the link URL that Nextcloud returned, changes and deletes links; when Nextcloud enforces a link password the field is required and „Passwort erzeugen“ fills it, otherwise password protection is an optional switch; when Nextcloud enforces an expiry date the date is required, prefilled with today plus the server's days and limited to that maximum, otherwise it stays optional and an emptied field creates the link without expiry (expireDate empty string) — all derived from the user's own /ocs/v2.php/cloud/capabilities, read fresh on every policy read and every write"
|
||||||
|
- "The API re-checks password and expiry rules from the capabilities before it calls Nextcloud (a missing enforced password or expiry never reaches Nextcloud), maps every Nextcloud refusal to a German error code with Nextcloud's own message as a second line, never answers 401 or 403, marks the connection expired on a Nextcloud 401 and keeps the Etappe-1 call gate on 429; a link password never appears in any API response, error body or api log line"
|
||||||
|
- "The views „Von mir geteilt“ and „Mit mir geteilt“ (tabs for every connected user) list the user's own user, group and link shares and the shares other people made with them, including pending shares with „Annehmen“ and „Ablehnen“; every item can be opened in the file view, own shares are changed or removed from the view, incoming shares can be left; email, federated and other share types appear only as a count with a pointer to Nextcloud"
|
||||||
|
- "Shared entries carry a share indicator in list and grid view (outgoing from oc:share-types, incoming from the permission letters); the outgoing indicator opens the share dialog"
|
||||||
|
- "Tessera lets one user create at most 15 shares within 10 minutes (the 16th gets 429 tooManyShares without any Nextcloud call), so Nextcloud's own limit of 20 per 10 minutes, whose 429 would pause the whole Nextcloud for all users, is never reached through Tessera; a share for a recipient who already has one is refused with shareAlreadyExists instead of re-sending Nextcloud's notification"
|
||||||
|
- "The module still shows version 1.0.0; the unreleased 1.0.0 module-changelog entry gained the three sharing items; CHANGELOG.md and the Anwender-, Administrations-, Betriebs- and Entwicklungsanleitung describe sharing; screenshots in dark mode (and some in light mode) prove dialog, link form under an enforced password, indicator and both views against the real test Nextcloud"
|
||||||
|
artifacts:
|
||||||
|
- path: "apps/api/src/nextcloud-files/nextcloud-shares.ts"
|
||||||
|
provides: "share-specific OCS transport on top of ncRequest (JSON body, query, reads the error body), parsers for shares, sharees and the sharing policy, permission mapping"
|
||||||
|
exports: ["ocsShareRequest", "parseShare", "parseShareList", "parseSharees", "parseSharePolicy", "permissionsFor", "accessOf"]
|
||||||
|
- path: "apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts"
|
||||||
|
provides: "policy, shares of a path, sharee search, create/update/remove/accept, mine/received, pre-validation from capabilities, duplicate check, create limiter, error matrix"
|
||||||
|
exports: ["NextcloudFilesSharesService"]
|
||||||
|
- path: "apps/api/src/nextcloud-files/dto/nextcloud-files-shares.dto.ts"
|
||||||
|
provides: "strict DTOs: kind and access enums (no raw bitmasks), strict date, length caps"
|
||||||
|
- path: "apps/web/src/components/nextcloud-files/share-policy.ts"
|
||||||
|
provides: "pure helpers: access options, password mode, expiry rule, addDays, password generator, update diff"
|
||||||
|
- path: "apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareDialog.tsx"
|
||||||
|
provides: "share dialog on the module Dialog: people and groups section, link section, errors with Nextcloud message"
|
||||||
|
- path: "apps/web/src/app/(portal)/modules/nextcloud-files/components/SharesView.tsx"
|
||||||
|
provides: "views Von mir geteilt / Mit mir geteilt incl. pending shares"
|
||||||
|
- path: "apps/api/src/nextcloud-files/nextcloud-files.changelog.ts"
|
||||||
|
provides: "module changelog: the single unreleased 1.0.0 release extended by three sharing items"
|
||||||
|
contains: "Öffentliche Links"
|
||||||
|
- path: ".planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh"
|
||||||
|
provides: "live e2e against tessera-nc-test: sections people, links, received, version"
|
||||||
|
key_links:
|
||||||
|
- from: "apps/api/src/nextcloud-files/nextcloud-shares.ts ocsShareRequest"
|
||||||
|
to: "ncRequest (Etappe-1 transport with call gate)"
|
||||||
|
via: "fixed prefix /ocs/v2.php/, segment-encoded ids, session authorization and credentialKey"
|
||||||
|
pattern: "prefix: '/ocs/v2\\.php/'"
|
||||||
|
- from: "apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts create"
|
||||||
|
to: "NextcloudLoginGuard.checkShareCreate"
|
||||||
|
via: "counted right before the POST, after all pre-validation"
|
||||||
|
pattern: "checkShareCreate\\("
|
||||||
|
- from: "apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts"
|
||||||
|
to: "mapNcFailure with onExpired -> account.markExpired"
|
||||||
|
via: "transport failures, 401, 429 and 5xx keep the Etappe-1 error contract"
|
||||||
|
pattern: "mapNcFailure\\("
|
||||||
|
- from: "apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts"
|
||||||
|
to: "GET /ocs/v2.php/cloud/capabilities with the caller's own app password"
|
||||||
|
via: "parseSharePolicy on every policy read and every write, no cache across calls"
|
||||||
|
pattern: "'cloud', 'capabilities'"
|
||||||
|
- from: "apps/api/src/nextcloud-files/nextcloud-propfind.ts buildEntry"
|
||||||
|
to: "oc:share-types (already requested by PROPFIND_BODY)"
|
||||||
|
via: "shareTypes number array on every entry"
|
||||||
|
pattern: "shareTypes"
|
||||||
|
- from: "apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx menuActions"
|
||||||
|
to: "ShareDialog"
|
||||||
|
via: "DialogState kind share, only when the entry permissions contain R"
|
||||||
|
pattern: "kind: 'share'"
|
||||||
|
- from: "apps/api/src/nextcloud-files/nextcloud-files.seed.ts"
|
||||||
|
to: "NEXTCLOUD_FILES_CHANGELOG"
|
||||||
|
via: "latestVersion still yields 1.0.0 (no version string in the seed)"
|
||||||
|
pattern: "latestVersion\\(NEXTCLOUD_FILES_CHANGELOG\\)"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Module „Dateien“ (slug `nextcloud-files`), Etappe 2a: users share files and folders of their OWN Nextcloud account from inside Tessera — with colleagues and groups (Nextcloud sharee search) and as public links — always under the caller's own app password, see existing shares in two views and in the file list, change and remove them. Nextcloud's sharing policy (from the capabilities of the calling user) decides what is required. Search is NOT part of this task.
|
||||||
|
|
||||||
|
Purpose: Etappe 1 (quick 261008-mzu) made the files usable inside Tessera; without sharing users still switch to Nextcloud for the most common collaboration step.
|
||||||
|
|
||||||
|
Three tasks, executed strictly in order. Task 1 is the tracer (one complete path: share a folder with a colleague, through every layer, proven live). Task 2 expands to links under the policy plus both views and incoming shares. Task 3 bumps the module version, writes changelogs and all four guides, runs every gate on the rebuilt stack and proves the UI in the browser.
|
||||||
|
|
||||||
|
Locked decisions — from CONTEXT.md (user, NON-NEGOTIABLE):
|
||||||
|
- D-01 Share targets: BOTH colleagues — Nextcloud users AND groups, found via Nextcloud's sharee search — and public links (to hand to customers).
|
||||||
|
- D-02 Link protection follows the Nextcloud server policy, no Tessera-invented rules. Tessera reads the policy from the capabilities (password enforced, expiry enabled/enforced/days, etc.), reflects it in the form (required fields, defaults, maximums) and shows Nextcloud's policy errors understandably in German. Optional fields (expiry when not enforced) stay freely settable. The user expects the company Nextcloud to enforce a link password but no expiry — both variants must work.
|
||||||
|
- D-03 Permissions: simple choice „Ansehen“ (read) or „Bearbeiten“ (edit); for folders additionally „Nur hochladen“ (file drop / Briefkasten, mainly for links). No per-bit checkboxes.
|
||||||
|
- D-04 Overview: separate views „Von mir geteilt“ and „Mit mir geteilt“ plus a share indicator on every shared entry of the file list; shares can be opened from both places to change or remove them.
|
||||||
|
|
||||||
|
Locked decisions — orchestrator answers to the research's open questions (NON-NEGOTIABLE):
|
||||||
|
- D-05 Module version (CORRECTED by orchestrator after planning): follow the guide rule „Höchstens ein Sprung je Modul zwischen zwei Tessera-Freigaben“ (docs/anleitung-entwicklung.md, around line 319). The module has never been in a Tessera release (last release v1.10.1 on 2026-10-06; the 1.0.0 entry is dated 2026-10-08, so it is unreleased), therefore NO new version: the version stays 1.0.0 and the three sharing items are appended to the existing 1.0.0 release (version and date unchanged; a brand-new module stays 1.0.0 even with added Neu-items). Root `CHANGELOG.md` gets the bullets under „## Unveröffentlicht“, without a „Modulversion …“ suffix. No deviation from the guide.
|
||||||
|
- D-06 Only user (0), group (1) and public link (3) shares are shown and creatable; email (4), federated and all other types are filtered out (count only).
|
||||||
|
- D-07 Link password UX: policy enforces a password → required field with a „Passwort erzeugen“ button; otherwise an optional toggle.
|
||||||
|
- D-08 Expiry UX: enforced → required, prefilled with the server default and limited to the server maximum; otherwise optional; to create a link without expiry send `expireDate: ""`.
|
||||||
|
- D-09 Tessera-side limiter for share creation: 15 per 10 minutes per Tessera user, so Nextcloud's 429 never pauses the whole origin.
|
||||||
|
- D-10 Documentation is mandatory: Anwender (Teilen), Administration (Nextcloud sharing policy); per the user's standing rule also Betrieb, Entwicklung, CHANGELOG and the module changelog. App texts German with „Sie“, real umlauts.
|
||||||
|
|
||||||
|
Binding from Etappe 1 (261008-mzu, unchanged): D-C transport rules (fixed prefixes, segment encoding, no redirects, no cookies, never call a URL from a Nextcloud answer), D-D error contract (never 401/403 to the browser, `{ code, message }` with German text), D-H path rules (`parseUserPath`), D-N route rights and order (class `@UseModule`, statics before `:param`, controller spec checks the order), D-O call gate (429 pauses the origin, first 401 kills the credential), tenant and user only from the token, L-09 design rules (Mosaik tokens, no ALL-CAPS labels, no middle-dot meta strings, no arrow buttons, calm UI), L-11 project rules (de/en key parity, umlaut guard, no `.env` reads, rebuild with `--build`).
|
||||||
|
|
||||||
|
Claude's discretion (decided here, apply as written):
|
||||||
|
- D-11 API surface (all Benutzen level, `@Controller('modules/nextcloud-files')`): statics `GET shares/policy`, `GET shares/by-path?path=`, `GET sharees?term=&itemType=`, `POST shares`, `GET shares/mine`, `GET shares/received`; at the END after the existing parameter routes `PUT shares/:id`, `DELETE shares/:id`, `POST shares/:id/accept` (200). The browser sends `kind: 'user' | 'group' | 'link'` and `access: 'view' | 'edit' | 'upload'`, never a share type number or permission bitmask; the API maps: view → 1; edit → folder 15, file 3; upload → 4 (folder links only); bit 16 (reshare) is never sent; kind → shareType 0 / 1 / 3. The item type and its writability come from the API's own PROPFIND Depth 0 (`dav.stat`) on create and from `GET shares/{id}` on update — never from the browser.
|
||||||
|
- D-12 Share transport: new `ocsShareRequest` in `nextcloud-shares.ts` on top of `ncRequest` (the Etappe-1 `ocsRequest` stays untouched: it maps 403 to `app-password-given` and discards error bodies, which the login code relies on). JSON bodies for POST/PUT (passwords never in a URL), body read on every status (2xx cap 8 MiB, non-2xx cap 64 KiB, capabilities cap 1 MiB), `ocs.meta.message` sanitised (control characters removed, whitespace collapsed, max 300 characters), share lists capped at 2000 entries with `truncated`.
|
||||||
|
- D-13 Policy is read from `GET /ocs/v2.php/cloud/capabilities` with the caller's own credential on every `GET shares/policy` and before every create and every link update — no cache (it is per user, an admin change is visible at once, and the e2e toggles it between calls).
|
||||||
|
- D-14 Error codes (added to `NcErrorCode` + `NC_ERROR_DEFAULTS`, German defaults): `sharingDisabled` 409 „Teilen ist in Ihrer Nextcloud ausgeschaltet.“ (also used with the message „Teilen mit Gruppen ist in Ihrer Nextcloud ausgeschaltet.“), `linkSharingDisabled` 409 „Öffentliche Links sind in Ihrer Nextcloud ausgeschaltet.“, `shareAccessInvalid` 400 „Diese Berechtigung ist für diesen Eintrag nicht möglich.“, `shareRecipientInvalid` 422 „Diese Person oder Gruppe kennt Ihre Nextcloud nicht.“, `shareAlreadyExists` 409 „Der Eintrag ist schon so geteilt. Ändern Sie die vorhandene Freigabe.“, `sharePasswordRequired` 400 „Ihre Nextcloud verlangt für Links ein Passwort.“, `sharePasswordRejected` 400 „Nextcloud lehnt dieses Passwort ab. Bitte wählen Sie ein längeres oder weniger gebräuchliches Passwort.“, `shareExpiryRequired` 400 „Ihre Nextcloud verlangt für Links ein Ablaufdatum.“, `shareExpiryInvalid` 400 „Dieses Ablaufdatum lässt Ihre Nextcloud nicht zu. Es darf nicht in der Vergangenheit und nicht nach dem erlaubten Höchstdatum liegen.“, `shareRejected` 422 „Nextcloud hat diese Freigabe abgelehnt.“, `shareNotFound` 404 „Diese Freigabe gibt es nicht mehr.“, `tooManyShares` 429 with `retryAfterSeconds` „Sie haben in kurzer Zeit viele Freigaben angelegt. Bitte warten Sie einige Minuten.“. Codes that come from a Nextcloud answer carry `ncMessage` (sanitised) as extra field.
|
||||||
|
- D-15 Error matrix (Nextcloud 34 source, verified at planning: password-policy failures are HTTP 400; „Passwords are enforced“ on create 403; expiry in the past or beyond the maximum is a GenericShareException with code 404 and arrives as HTTP 404 on create AND update; missing enforced expiry on create 403; any other update failure 400 „Failed to update share.“; update of an incoming share 403; unknown id 404; delete without right 403). Applied by one function `mapShareFailure(operation, result, sent)`: transport failures, credential-dead, 429 and every status ≥ 500 go to `mapNcFailure` with `onExpired`. create: 400 + password sent → `sharePasswordRejected`; 404 + non-empty expireDate sent → `shareExpiryInvalid`; 404 for user/group → `shareRecipientInvalid`; 404 for link → `notFound`; 400/403/other 4xx → `shareRejected`. update: 400 + non-empty password sent → `sharePasswordRejected`; 400 or 404 + expireDate field sent → `shareExpiryInvalid`; 404 otherwise → `shareNotFound`; 400/403/other 4xx → `shareRejected`. remove/accept/read by id: 404 → `shareNotFound`; other 4xx → `shareRejected`. by-path read: 404 → `notFound`. Never switch on message text (it is localised).
|
||||||
|
- D-16 Pre-validation before any write call (from the fresh policy and the stat/GET result): API disabled → `sharingDisabled`; group while group sharing is off → `sharingDisabled` with the group message; link while links are off → `linkSharingDisabled`; access not offered for this item (upload on a file or for user/group; edit on an item without the letters W, C or K / without update or create bit; link edit or upload while public upload is off) → `shareAccessInvalid`; recipient already has a share of the same kind on this path (by-path list) → `shareAlreadyExists`; a second link while `multiple_links` is false → `shareAlreadyExists`; link without password (create) or password `""` (update) while enforced → `sharePasswordRequired`; link expireDate absent or `""` on create, or `""` on update, while enforced → `shareExpiryRequired`; expireDate not `^\d{4}-\d{2}-\d{2}$` or not a real calendar date → `shareExpiryInvalid` (date RANGE is left to Nextcloud — its timezone decides near midnight). Password, expireDate and label are dropped for user/group shares (never forwarded); on create a link expireDate that is absent is not sent (server default applies) — the web always sends it for links (a date or `""`).
|
||||||
|
- D-17 Received and pending: `GET shares/received` = `GET shares?shared_with_me=true` + `GET shares/pending` (a 404/405 on the pending call means an older server without the route → empty pending list, not an error); accept = `POST shares/pending/{id}`; decline and leave = `DELETE shares/{id}` by the recipient. „Öffnen“ navigates the file view to `file_target` (folder) or to its parent with the file focused.
|
||||||
|
- D-18 Password generator in the browser (CSPRNG via `crypto.getRandomValues`, length max(20, policy minLength) capped at 64, at least one upper case letter, lower case letter, digit and special character, no look-alike characters). Deviation from the research's suggestion to call `password_policy/api/v1/generate`: that app is optional on the company server, a 20-character CSPRNG value meets every usual rule, and Nextcloud still validates (its 400 message is shown). Link label: yes (optional, max 255); note field: no. Notifications: Nextcloud's own behaviour (no `sendMail`). The link URL is the `url` Nextcloud returned, shown only to the share owner, only when it is http/https, in a read-only input with „Link kopieren“ (navigator.clipboard, fallback: select the text); Tessera never requests it. Expiry of user/group shares is not sent (Nextcloud applies its own default/enforcement) and is shown read-only.
|
||||||
|
- D-19 UI: `ShareDialog` built on the module `Dialog` (wide; this also keeps the file-view shortcuts out of the search field), title „„{name}“ teilen“, section „Personen und Gruppen“ (combobox search with debounce 300 ms, starts at max(1, minSearchLength) characters, results as listbox, already-shared recipients disabled, access select next to the field, default „Ansehen“; rows with name, „Gruppe“ marker, access select, read-only expiry, remove button), section „Link“ (Task 2), footer „Fertig“; errors as `role="alert"` with the code text and a second line „Meldung der Nextcloud: …“. User/group remove acts at once; link delete asks inline. Share indicator: outgoing = icon button (link icon if a link exists, else people icon) with aria-label, opens the dialog; incoming = static icon with title and screen-reader text „Mit Ihnen geteilt“. Tabs Dateien / Von mir geteilt / Mit mir geteilt for every connected user (Einstellungen stays for managers); the share tabs and the Teilen action are hidden only when the policy says sharing is off.
|
||||||
|
- D-20 Test strategy: specs assert literal outgoing calls (method, exact URL, exact JSON body, headers) — never values rebuilt with the production helper (STATE pitfall „Tautologischer Test“). One live e2e script `e2e-shares.sh [people|links|received|version|all]` against `tessera-nc-test`; it adds the Nextcloud user `ben` (no two-factor) and the group `tessera-team` (with ben), toggles policies with occ and resets everything in a trap, and switches Nextcloud's own rate limit off for its run only (`ratelimit.protection.enabled`, reset in the trap) so repeated runs never trigger an origin pause; Tessera's limiter is proven by unit specs. Budget: one `all` run creates at most 6 shares through Tessera.
|
||||||
|
|
||||||
|
Output: share layer, service, DTOs, routes, propfind share types, web client, policy helpers, share dialog with link form, share indicator, two views, tab and navigation changes, e2e script, module changelog 1.0.0 extended, changelogs, four guides, screenshots. Three commits on main, NOT pushed.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@~/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/STATE.md
|
||||||
|
@./CLAUDE.md
|
||||||
|
@.planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/261009-dkv-CONTEXT.md
|
||||||
|
@.planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/261009-dkv-RESEARCH.md
|
||||||
|
@.planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/261008-mzu-SUMMARY.md
|
||||||
|
|
||||||
|
Discovered facts the executor can rely on (verified during planning on 2026-10-09):
|
||||||
|
- Transport (`apps/api/src/nextcloud-files/nextcloud-http.ts`): `ncRequest(transport, gate, opts)` with `NcRequestOptions { baseUrl, prefix, segments?, query?: Record<string,string>, method, headers?, body?: string|Buffer|Readable|null, authorization?, credentialKey?, ocs?: boolean, headersTimeoutMs?, bodyTimeoutMs?, signal? }`; returns `{ ok: true, status, headers, body }` for every HTTP status (also 4xx) or `NcFailure { ok: false, kind, status?, retryAfterSeconds? }`; it already turns a 429 into an origin pause plus `{ ok: false, kind: 'http', status: 429 }` and a 401 with credentialKey into `kind: 'credential-dead'`. `/ocs/v2.php/` is in `ALLOWED_PREFIXES` — nothing to add. `buildNcUrl` encodes every query key and value with `encodeURIComponent` in insertion order (so `shareType[0]` becomes `shareType%5B0%5D`). `ocs: true` adds `OCS-APIRequest: true` and `Accept: application/json`; custom headers cookie/host/authorization are forbidden. `readCappedText(body, maxBytes)` returns `{ ok, text }` or `too-large`/transport kinds. `parseUserPath(raw)` → segments (400 invalidPath). `basicAuth`.
|
||||||
|
- Etappe-1 OCS helper `ocsRequest` in `nextcloud-auth-client.ts` maps 403 to `app-password-given` and drains error bodies with no message — do not use or change it for shares.
|
||||||
|
- Error contract (`nextcloud-files.types.ts`): `NcErrorCode` union + `NC_ERROR_DEFAULTS: Record<NcErrorCode, { status, message }>` (every new code needs a default), `ncErrorDefault(code, extra?, message?)`, `ncError(code, status, message, extra?)`, `NcSession { baseUrl, ncUserId, authorization, credentialKey }`. `mapNcFailure(result, { onExpired })` in `nextcloud-upstream.ts` is the Etappe-1 mapper (credential-dead/401 → connectionExpired + onExpired, 429/paused → nextcloudLocked, 503 → nextcloudMaintenance, other ≥500 → nextcloudError, timeouts → nextcloudUnavailable, redirect → nextcloudRedirect).
|
||||||
|
- Service pattern: `NextcloudFilesService` (`nextcloud-files.service.ts`) — constructor `(account: NextcloudFilesAccountService, gate: NextcloudCallGate, @Inject(NEXTCLOUD_TRANSPORT) transport)`, private `session(tenantId, userId)` = `account.getSession`, private `fail()` = `throw await mapNcFailure(result, { onExpired: () => this.account.markExpired(tenantId, userId) })`. `dav.stat(transport, gate, session, segments)` (`nextcloud-dav.ts`) → `{ ok: true, status, entry: NcEntry | null }` (404 → entry null) or NcFailure. The new service touches no Prisma model → RLS inventory and `docs/mandantentrennung-zugriffsklassifikation.md` stay unchanged.
|
||||||
|
- Spec harness: `nextcloud-files.service.spec.ts` `setup()` with a fake `NextcloudTransport` returning `{ statusCode, headers, body: Readable.from([Buffer]) }`, `SESSION = { baseUrl: 'https://cloud.example/nc', ncUserId: 'anna', authorization: 'Basic YW5uYTphcHAtcHctMTIz', credentialKey: 'k1' }`, real `NextcloudCallGate`, `account = { getSession, markExpired }` mocks, `codeOf(e)` reads `e.response.code`. The share service spec needs a queue of replies (one per call) and records `calls[i].method/url/headers/body`.
|
||||||
|
- Login guard (`nextcloud-login-guard.ts`): injectable clock `now`, `pruneTimes(list, now, windowMs)`, `checkFlowStart(userId)` (10 per 10 min, throws `tooMany(ms)` which builds `tooManyAttempts`) — `checkShareCreate` is the sibling with its own constants and the `tooManyShares` code.
|
||||||
|
- PROPFIND (`nextcloud-propfind.ts`): `PROPFIND_BODY` already requests `<oc:share-types/>`, the parser has `isArray` for `share-type`, `buildEntry` does not read it; `NcEntry` fields name, path, type, size, mime, mtime, etag, fileId, permissions (letters, R = shareable), hasPreview, favorite. Own root entries show `RGDNVCK` (folders) / `RGDNVW` (files); received items carry `S` [research A1, verify live in Task 2].
|
||||||
|
- Controller (`nextcloud-files.controller.ts`): class `@UseModule('nextcloud-files')`, constructor `(settings, account, files, transfer, serverInfo)`, `requireTenantId(req)` / `requireUserId(req)`; `saveSettings` carries `@Roles(ADMIN, SUPER_ADMIN)` since CR-02 (the Etappe-1 role-grep gate no longer applies; the specs check rights). Static routes end with `@Put('uploads/file')`, then the parameter block starts with `@Get('connect/flow/:flowId')` and ends with `@Delete('uploads/:uploadId')`. `nextcloud-files.controller.spec.ts` has „Pfade und Methoden“ (RequestMethod GET 0, POST 1, PUT 2, DELETE 3), „alle anderen Handler stehen auf Benutzen-Ebene“ and the declaration-order check. `apps/api/src/module-registry/module-manage-handlers.spec.ts` lists Benutzen handlers of `NextcloudFilesController` in an `it.each` (comment „Spätere Aufgaben … ergänzen diese Liste“). Module providers in `nextcloud-files.module.ts`.
|
||||||
|
- Web: `apps/web/src/lib/nextcloud-files-api.ts` — private `request<T>(path, { method, json })` (credentials include, GET no-store, throws `NextcloudFilesRequestError(status, code, message, extra)`), web `NcEntry` mirrors the API. `components/nextcloud-files/error-text.ts` — `KNOWN` set, `errorText(t, error, locale)` (special cases nextcloudLocked minutes, quotaExceeded), `toErrorLike`. `FileBrowser.tsx` — props `{ serverUrl, onExpired }`, `DialogState` union (newFolder/rename/move/delete), `menuActions(entry): EntryAction[]` (open/downloadZip/download, rename, move, openInNextcloud, delete), `focusAfterLoad` ref, `load(path, { quiet })`, reads `?path=` on mount and mirrors it with `replaceState`; `isTypingTarget` ignores keys inside `[role="dialog"]`. `Dialog.tsx` props `{ title, onClose, children, footer?, wide?, initialFocus? }` (focus trap, Escape). `EntryAction { id, label, icon, href?, onSelect?, destructive?, separated? }`. `TypeTile` takes `entry: { name, type, mime }`. Icons in `components/icons.tsx` are lucide-style `export const XIcon = (p: P) => (…)`. `page.tsx` — `TabId = 'files' | 'settings'`, `TabBar` from `@/components/accounting/tab-bar` rendered only for `canManage`, FileBrowser only rendered in the files tab (switching tabs remounts it), `SettingsSection`, `PageHeader` with AccountBar in the files tab. Tests: `FileBrowser.test.tsx` mocks `@/lib/nextcloud-files-api` via `importOriginal`; page test renders with `NextIntlClientProvider locale="de" messages={de}`.
|
||||||
|
- Messages: namespace `nextcloudFiles` in `apps/web/src/messages/de.json`/`en.json` (sections tabs, notConfigured, connect, account, errors, settings, browser {menu, …}, dialogs, transfers, codes). `umlaut-guard.spec.ts` fails on any new token with ae/oe/ue/ss that is not on `UMLAUT_ALLOWLIST` in `umlaut-dictionary.ts` (add correct German words there); message variables must not contain such letter pairs (use `{name}`, `{count}`, `{date}`, `{days}`, `{term}`, `{detail}`, `{minutes}` — never `{query}`).
|
||||||
|
- Module changelog: `apps/api/src/nextcloud-files/nextcloud-files.changelog.ts` has exactly one release 1.0.0 dated 2026-10-08 with four `new` items; the seed uses `latestVersion(NEXTCLOUD_FILES_CHANGELOG)`; `apps/api/src/module-registry/module-changelog.spec.ts` checks format (strictly descending versions, real dates newest first, de+en, no replacement spellings, no tenant/licence words) and pins in the test „domains und nextcloud-files haben genau eine Version 1.0.0 vom 2026-10-08“ (around line 202). The Marktplatz reads `GET /modules/changelog/:slug`. CHANGELOG.md has „## Unveröffentlicht“ → „### Neu“ with the Etappe-1 bullets „Neues Modul „Dateien“ …“, „Dateien, Anmeldung: …“, „Dateien, Arbeiten mit Dateien: …“, „Dateien, Hochladen und Herunterladen: …“; module version bumps are mentioned like „Modulversion 1.1.0.“ (see the DKV bullet).
|
||||||
|
- Guides: `docs/anleitung-anwender.md` „### Dateien (Nextcloud)“ (line ~245; the „**Arbeiten mit Dateien:**“ paragraph lists the row-menu actions „Öffnen, Herunterladen, Umbenennen, Verschieben, „In Nextcloud öffnen“ und Löschen“); `docs/anleitung-administration.md` „### Dateien: Nextcloud anbinden“ (line ~359); `docs/anleitung-betrieb.md` „### Dateien (Nextcloud)“ in chapter 3 (line ~186, bullets with bold lead-ins) and the „### Fehlerbilder“ table; `docs/anleitung-entwicklung.md` „## Konventionen und Fallstricke“ (line ~693, paragraphs „**Titel (quick-id):** …“).
|
||||||
|
- Test Nextcloud `tessera-nc-test` (Nextcloud 34.0.4, running): host `http://localhost:18080`, from the api container `http://172.17.0.1:18080` (`NC_BASE`); users admin/Admin-Pass-12345, anna/User1-Pass-12345 („Anna Müller“), zoe (two-factor, „Zwei Faktor“); groups admin, twofa. Verified config keys: link password enforced = app config `core shareapi_enforce_links_password` (lexicon BOOL: `occ config:app:set core shareapi_enforce_links_password --value=true --type=boolean`), link default expiry `core shareapi_default_expire_date` (BOOL), link expiry enforced `core shareapi_enforce_expire_date` (BOOL), days `core shareapi_expire_after_n_days` (string, default 7) — reset each with `occ config:app:delete core <key>`; pending shares for anna: `occ user:setting anna files_sharing default_accept no`, reset `occ user:setting --delete anna files_sharing default_accept`; Nextcloud rate limits off: `occ config:system:set ratelimit.protection.enabled --value=false --type=boolean`, reset `occ config:system:delete ratelimit.protection.enabled`; `createShare` carries `UserRateLimit(limit: 20, period: 600)`. Capabilities: `files_sharing.public.expire_date.days` is a STRING when present. The password policy runs in the SHARING context (`minLength` 10 in the test server).
|
||||||
|
- e2e harness (`.planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/e2e-lib.sh`, sourced): `API`, `WEB`, `NC_BASE`, `E2E_TMP`, `e2e_fail`, `e2e_login <jar> [user] [pw]` (default admin/admin123), `e2e_status <jar> <method> <url> [json] [outfile]` (prints the status, body to `$E2E_TMP/body.out`), `e2e_expect <want> <got> <what>`, `e2e_contains <file> <text> <what>`, `e2e_activate`, `e2e_set_address`, `e2e_connect_anna <jar>` (prints status), `e2e_wait_health`, `NC_OCC …` (occ as www-data in the test container). `e2e-files.sh` shows the style (python3 -I for JSON, curl -u for direct Nextcloud calls, `trap … EXIT`).
|
||||||
|
- Stack: db, api :3001, web :3000 (production build via `/api-proxy`) and mailhog run; rebuild with `docker compose up -d --build api` (plus `web` when web code changed) — plain `up` does not rebuild. Playwright MCP is configured in `.mcp.json` (chromium); Etappe 1 used a throwaway playwright-core script in the scratchpad when MCP tools were not available. Screenshots go to `.playwright-mcp/nextcloud-files/` (gitignored). Dark mode is switched with the theme button (user preference: check in dark first). Never measure by calling fetch from inside the page (it misleads in both directions) — use the UI.
|
||||||
|
- Git: the index already contains unrelated staged deletions (`.planning/.continue-here.md`, `.planning/HANDOFF.json`) and a modified `.planning/STATE.md` — they belong to the orchestrator. Commit ONLY the task's files: `git add <new files>` then `git commit -m "…" -- <every file of the task>`. German subject, prefix `feat(nextcloud-files):`, body ends with `Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>`. Never push (the user bundles pushes). No deploy to the test server. Never read `.env` files.
|
||||||
|
- Literals for specs: `anna:app-pw-123` → `Basic YW5uYTphcHAtcHctMTIz`; `encodeURIComponent('/Projekte/Ärger 100%')` = `%2FProjekte%2F%C3%84rger%20100%25`; `encodeURIComponent('shareType[0]')` = `shareType%5B0%5D`.
|
||||||
|
|
||||||
|
@apps/api/src/nextcloud-files/nextcloud-http.ts
|
||||||
|
@apps/api/src/nextcloud-files/nextcloud-files.service.ts
|
||||||
|
@apps/api/src/nextcloud-files/nextcloud-upstream.ts
|
||||||
|
@apps/api/src/nextcloud-files/nextcloud-files.types.ts
|
||||||
|
@apps/api/src/nextcloud-files/nextcloud-login-guard.ts
|
||||||
|
@apps/api/src/nextcloud-files/nextcloud-files.controller.ts
|
||||||
|
@apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx
|
||||||
|
@apps/web/src/app/(portal)/modules/nextcloud-files/components/Dialog.tsx
|
||||||
|
@apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="tracer" tdd="true">
|
||||||
|
<name>Task 1: Tracer — share a file or folder with a colleague or a group, end to end (share layer, service, routes, web client, share dialog people section, row menu, share indicator), proven live against the test Nextcloud</name>
|
||||||
|
<files>apps/api/src/nextcloud-files/nextcloud-shares.ts, apps/api/src/nextcloud-files/nextcloud-shares.spec.ts, apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts, apps/api/src/nextcloud-files/nextcloud-files-shares.service.spec.ts, apps/api/src/nextcloud-files/dto/nextcloud-files-shares.dto.ts, apps/api/src/nextcloud-files/nextcloud-files.types.ts, apps/api/src/nextcloud-files/nextcloud-login-guard.ts, apps/api/src/nextcloud-files/nextcloud-login-guard.spec.ts, apps/api/src/nextcloud-files/nextcloud-propfind.ts, apps/api/src/nextcloud-files/nextcloud-propfind.spec.ts, apps/api/src/nextcloud-files/nextcloud-files.controller.ts, apps/api/src/nextcloud-files/nextcloud-files.controller.spec.ts, apps/api/src/nextcloud-files/nextcloud-files.module.ts, apps/api/src/module-registry/module-manage-handlers.spec.ts, apps/web/src/lib/nextcloud-files-api.ts, apps/web/src/lib/nextcloud-files-api.test.ts, apps/web/src/components/nextcloud-files/share-policy.ts, apps/web/src/components/nextcloud-files/share-policy.test.ts, apps/web/src/components/nextcloud-files/error-text.ts, apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareDialog.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareDialog.test.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareIndicator.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/FileList.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/FileGrid.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/icons.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json, apps/web/src/messages/umlaut-dictionary.ts, .planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh</files>
|
||||||
|
<precondition>The local stack (db, api, web) runs, `curl -s http://localhost:18080/status.php` contains `"installed":true` (container tessera-nc-test) and `bash .planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/nc-test-setup.sh` prints `nc test ready`.</precondition>
|
||||||
|
<behavior>
|
||||||
|
- Share transport (fake transport, real gate): GET shares of `/Projekte/Ärger 100%` requests exactly `https://cloud.example/nc/ocs/v2.php/apps/files_sharing/api/v1/shares?path=%2FProjekte%2F%C3%84rger%20100%25&reshares=true` with method GET and the headers `authorization: Basic YW5uYTphcHAtcHctMTIz`, `ocs-apirequest: true`, `accept: application/json` and no cookie header; sharee search for term `ben` on a folder requests exactly `https://cloud.example/nc/ocs/v2.php/apps/files_sharing/api/v1/sharees?search=ben&itemType=folder&perPage=20&shareType%5B0%5D=0&shareType%5B1%5D=1`; creating a user share sends POST `https://cloud.example/nc/ocs/v2.php/apps/files_sharing/api/v1/shares` with `content-type: application/json` and the body literal `{"path":"/Projekte","shareType":0,"shareWith":"ben","permissions":15}`; update sends PUT `https://cloud.example/nc/ocs/v2.php/apps/files_sharing/api/v1/shares/17` with body `{"permissions":1}`; delete sends DELETE to the same URL without body; capabilities = GET `https://cloud.example/nc/ocs/v2.php/cloud/capabilities`. A 404 answer `{"ocs":{"meta":{"status":"failure","statuscode":404,"message":"Wrong share ID, share does not exist"},"data":[]}}` yields `{ ok: true, status: 404, message: 'Wrong share ID, share does not exist' }`; a message with control characters and 500 characters comes back cleaned and cut to 300; a non-JSON 2xx yields kind invalid-response; a 2xx body over 8 MiB yields too-large; a 429 pauses the origin (the next call is not sent); a 401 yields credential-dead.
|
||||||
|
- Parsers: the research's live user-share JSON (copied verbatim into the spec) → `{ id: '1', kind: 'user', path: '/Projekte', name: 'Projekte', itemType: 'folder', mime: null, itemWritable: true, permissions: 31, access: 'edit', shareWith: 'zoe', shareWithName: 'Zwei Faktor', ownerId: 'anna', ownerName: 'Anna Müller', canEdit: true, canDelete: true, expiration: '2026-12-31', label: '', url: null, hasPassword: false, target: '/Projekte', sharedAt: '2026-10-09T07:52:58.000Z' }` and JSON.stringify of the result contains none of storage_id, attributes, mail_send, item_source, token; a link fixture with `password: 'redacted'`, a token and `url: 'http://cloud.example/nc/index.php/s/AbC123'` owned by anna and parsed for self anna → kind link, hasPassword true, url kept, the string redacted absent; the same fixture parsed for self zoe → url null; url `javascript:alert(1)` → null; share_type 4 → null (caller counts it as hidden); `accessOf`: 1 → view, 17 → view, 4 → upload, 3 → edit, 15 → edit, 31 → edit, 0 and 16 → custom; `permissionsFor('edit','folder')` 15, `('edit','file')` 3, `('view', any)` 1, `('upload','folder')` 4; `parseShareList` accepts an array (GET) and an object (POST/PUT answer), keeps at most 2000 and flags truncated. `parseSharees` on the research fixture → `[{ kind: 'user', id: 'zoe', label: 'Zwei Faktor', detail: 'zoe' }, { kind: 'group', id: 'twofa', label: 'twofa', detail: null }]` (exact and normal lists merged, deduped by kind and id, remotes/emails/lookup dropped, max 25). `parseSharePolicy` on the research's live capabilities → `{ enabled: true, groupsEnabled: true, links: { enabled: true, passwordRequired: false, passwordSuggested: false, expiryDefaultDays: null, expiryEnforced: false, uploadAllowed: true, multipleLinks: true }, internalExpiry: { defaultDays: null, enforced: false }, minSearchLength: 0, passwordMinLength: 10 }`; `public: { enabled: false }` → links.enabled false and every link flag false; `expire_date: { enabled: true, days: '7', enforced: true }` → expiryDefaultDays 7, expiryEnforced true; days 'abc' or '0' → null; missing files_sharing or `api_enabled: false` → enabled false.
|
||||||
|
- Service (queue of fake replies, mocked account, real gate, guard with fake clock): create `{ path: '/Projekte', kind: 'user', shareWith: 'ben', access: 'edit' }` sends in this order PROPFIND Depth 0 on `https://cloud.example/nc/remote.php/dav/files/anna/Projekte`, GET capabilities, GET shares?path=%2FProjekte&reshares=true, POST with the literal body above, and returns the parsed share; a file `/Bericht.txt` with access edit sends permissions 3, view sends 1; access upload for a user share, or edit on an entry with letters `RG`, → 400 shareAccessInvalid and no POST; ben already in the by-path list as user → 409 shareAlreadyExists and no POST; kind group while groupsEnabled false → 409 sharingDisabled with the group message and no POST; api disabled → 409 sharingDisabled; path '' or '/' → 400 invalidPath without any call. Error matrix (it.each) on the create POST: 404 → 422 shareRecipientInvalid; 403 with message 'You cannot share a folder that contains other shares' → 422 shareRejected with ncMessage equal to that text; 400 → 422 shareRejected; 500 → nextcloudError; 503 → nextcloudMaintenance; 401 → 409 connectionExpired and markExpired called once; 429 → 503 nextcloudLocked; no case ends as HTTP 401 or 403. Update `17 { access: 'view' }` sends GET shares/17 then PUT `{"permissions":1}`; a share with can_edit false → 422 shareRejected without PUT; PUT 404 → 404 shareNotFound; ids 'abc' and a 21-digit id → 404 shareNotFound without any call; an update without fields returns the current share and sends no PUT. Remove 17 → DELETE; 404 → shareNotFound; 403 → 422 shareRejected. Sharees with term '' → `{ sharees: [] }` without a call. Policy: two calls → two capability requests (no cache). Limiter: 15 creates in 10 minutes pass, the 16th → 429 tooManyShares with retryAfterSeconds 600 and no POST; 10 minutes later creates pass again; a create refused by pre-validation does not count.
|
||||||
|
- Guard: `checkShareCreate(userId)` allows 15 per 10 minutes per user, user B unaffected by user A, retryAfterSeconds counts to the end of the window of the oldest create.
|
||||||
|
- PROPFIND: `<oc:share-types><oc:share-type>0</oc:share-type><oc:share-type>3</oc:share-type><oc:share-type>3</oc:share-type></oc:share-types>` → `shareTypes: [0, 3]`; an empty `<oc:share-types/>` → `[]`; a non-number value is ignored.
|
||||||
|
- Controller: routes `getSharePolicy` [0,'shares/policy'], `listSharesForPath` [0,'shares/by-path'], `searchSharees` [0,'sharees'], `createShare` [1,'shares'], `updateShare` [2,'shares/:id'], `deleteShare` [3,'shares/:id']; none carries MODULE_MANAGE_KEY or ROLES_KEY; the declaration-order check passes (the two `:id` handlers are declared after every static handler); tenant and user reach the service from the token only; without a user in the token → ForbiddenException and no service call.
|
||||||
|
- Web: `listSharesForPath('/Ärger 100%')` fetches `…/modules/nextcloud-files/shares/by-path?path=%2F%C3%84rger%20100%25`; `searchSharees('ben', 'folder')` fetches `…/sharees?term=ben&itemType=folder`; `createShare` POSTs the JSON input; `accessOptions` → folder writable user share [view, edit], entry with letters `RG` [view]; `generatePassword` (injected random) has length max(20, minLength), at least one of each class, no look-alike characters. ShareDialog (mocked api): shows existing rows; typing `be` calls `searchSharees('be', 'folder')` once after the debounce; choosing ben calls `createShare({ path, kind: 'user', shareWith: 'ben', access: 'view' })` once and shows the new row; an already shared recipient is disabled in the list; changing a row's access calls `updateShare(id, { access: 'edit' })`; remove calls `deleteShare(id)`; an error `shareRejected` with `extra.ncMessage` shows the German text and „Meldung der Nextcloud: …“; `connectionExpired` calls onExpired. FileBrowser: the row menu shows „Teilen“ only for entries whose permissions contain R and opens the dialog titled „„Projekte“ teilen“; an entry with shareTypes [0] shows the indicator button (aria-label with the name) that opens the dialog; an entry with S in its permissions shows the incoming marker.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
**Share layer (per D-03, D-06, D-11, D-12, D-14).** New `apps/api/src/nextcloud-files/nextcloud-shares.ts` with a header comment naming quick 261009-dkv and the Etappe-1 rules it keeps. `ocsShareRequest(transport, gate, session, { method, segments, query?, json?, maxBytes? })` calls `ncRequest` with `prefix: '/ocs/v2.php/'`, the segments (shares base `['apps','files_sharing','api','v1','shares']`, ids appended as their own segment), `ocs: true`, `authorization` and `credentialKey` from the session, `headers: { 'content-type': 'application/json' }` plus `body: JSON.stringify(json)` only when json is given, 15 s timeouts. It reads the body on every status (2xx cap = maxBytes, default 8 MiB; non-2xx cap 64 KiB), parses `ocs.meta.message` (sanitised by a `cleanText(value, max)` helper: remove U+0000–U+001F and U+007F, collapse whitespace, cut to 300) and `ocs.data`, and returns `{ ok: true, status, message, data }` or the NcFailure untouched (also `too-large`, `invalid-response` for non-JSON 2xx; non-JSON non-2xx just has message null). Types: `NcShareKind = 'user' | 'group' | 'link'`, `NcShareAccess = 'view' | 'edit' | 'upload' | 'custom'`, `NcShareView` with exactly the fields of the behavior block (plus `pending?: boolean` used in Task 2), `NcSharee { kind: 'user' | 'group', id, label, detail: string | null }`, `NcSharePolicy` with the shape of the behavior block. `permissionsFor(access, itemType)` and `accessOf(permissions)` per D-11 (mask 15 before deriving; upload = create without read). `parseShare(raw, selfId)` returns null for share types other than 0/1/3 or ids not matching `^\d{1,20}$`, builds name from the last segment of `file_target` for received shares and of `path` otherwise, `itemWritable` from `item_permissions & 6`, `mime` from `mimetype` for files (null for folders), `expiration` from the first 10 characters when they form a date, `sharedAt` from `stime` seconds, `hasPassword` only for links with a non-empty password field, `url` only for links whose `uid_owner` equals selfId and whose URL parses with protocol http: or https:, every display string through `cleanText` (max 255) — never copies unknown fields. `parseShareList(data, selfId)` → `{ shares, hidden, truncated }` (array or single object, cap 2000). `parseSharees(data)` and `parseSharePolicy(capabilities)` per behavior (numbers accepted as number or numeric string, `days` clamped 1..3650, `minSearchLength` 0..32, `password_policy.minLength` 1..256 or null; `multiple_links` missing while links are on → true). Never call `api.generate` or any URL from capabilities. Write `nextcloud-shares.spec.ts` first (literal URLs and bodies).
|
||||||
|
|
||||||
|
**Error contract and limiter (per D-09, D-14).** In `nextcloud-files.types.ts` add all twelve codes of D-14 with their German defaults (Task 2 uses the link codes; defining them once keeps the type closed). In `nextcloud-login-guard.ts` add `SHARE_CREATE_LIMIT = 15`, `SHARE_CREATE_WINDOW_MS = 10 * 60 * 1000` and `checkShareCreate(userId)` (same pruneTimes pattern as `checkFlowStart`, throws `tooManyShares` with `retryAfterSeconds`); extend its spec.
|
||||||
|
|
||||||
|
**Service (per D-01, D-03, D-11, D-13, D-15, D-16).** New `nextcloud-files-shares.service.ts`, `@Injectable() NextcloudFilesSharesService(account, gate, @Inject(NEXTCLOUD_TRANSPORT) transport, guard: NextcloudLoginGuard)`, header comment with the rights rule (caller's own session only, tenant and user from the token, no database access). A private `loadPolicy(session)` calls `ocsShareRequest` with segments `['cloud', 'capabilities']` (cap 1 MiB) and `parseSharePolicy`, on every use (D-13). Methods: `policy(tenantId, userId)`; `sharesForPath(tenantId, userId, rawPath)` → `{ path, shares, hidden, truncated }` (root → invalidPath); `sharees(tenantId, userId, term, itemType)` → `{ sharees }` (trimmed term shorter than 1 → no call); `create(tenantId, userId, input)` for kinds user and group in this task (link arrives in Task 2): parseUserPath, root → invalidPath, session, `dav.stat` (404 → notFound), policy, D-16 checks, by-path duplicate check, `guard.checkShareCreate(userId)`, POST, parse the returned object; `update(tenantId, userId, id, input)`: id regex → shareNotFound without call, GET by id, `can_edit` false → shareRejected, access validated against kind and item, PUT with only the changed permissions (no field → return current share); `remove(tenantId, userId, id)` → `{ deleted: true }`. One private `mapShareFailure(operation, result, sent)` implements D-15 and throws; transport failures go to `mapNcFailure(result, { onExpired: () => this.account.markExpired(tenantId, userId) })`. Never log bodies, passwords, tokens or URLs. Write `nextcloud-files-shares.service.spec.ts` first per behavior (reply queue; it.each error matrix asserting codes and that no HTTP status is 401 or 403).
|
||||||
|
|
||||||
|
**DTOs, routes, module (per D-11).** `dto/nextcloud-files-shares.dto.ts`: `ShareByPathQueryDto { path: string (IsString, MaxLength 4096) }`, `ShareeQueryDto { term (IsString, MaxLength 100, no control characters), itemType (IsIn file, folder) }`, `CreateShareDto { path, kind (IsIn user, group in this task), shareWith (IsString, MaxLength 255, Matches no control characters), access (IsIn view, edit, upload) }`, `UpdateShareDto { access? }`. Controller: inject the new service as the sixth constructor argument; static handlers `getSharePolicy`, `listSharesForPath`, `searchSharees`, `createShare` placed right after `putSingle` (before the parameter block); `updateShare` (`@Put('shares/:id')`) and `deleteShare` (`@Delete('shares/:id')`) at the very END; ids reach the service as plain strings (the service validates). Update the header comment's route list. Register the service in `nextcloud-files.module.ts`. Extend `nextcloud-files.controller.spec.ts` (constructor arguments, „Pfade und Methoden“, Benutzen level, order, token pass-through) and the Benutzen `it.each` list in `module-manage-handlers.spec.ts` with the six handler names.
|
||||||
|
|
||||||
|
**Share types on entries.** In `nextcloud-propfind.ts` read `share-types` → `share-type` values into `shareTypes: number[]` (integers 0..99, deduped, ascending) on `NcEntry`; update `nextcloud-propfind.spec.ts` and every other spec fixture that builds full NcEntry objects so tsc stays green.
|
||||||
|
|
||||||
|
**Web client and helpers.** In `apps/web/src/lib/nextcloud-files-api.ts` add `shareTypes: number[]` to `NcEntry`, the types `NcShare`, `NcShareKind`, `NcShareAccess`, `NcSharee`, `NcSharePolicy` (mirroring the API) and `getSharePolicy()`, `listSharesForPath(path)`, `searchSharees(term, itemType)`, `createShare(input)`, `updateShare(id, input)`, `deleteShare(id)` (paths and terms only in the query or JSON, encoded with encodeURIComponent); extend its test. New `components/nextcloud-files/share-policy.ts` (+ test): `type ShareTarget = { path, name, type: 'file' | 'folder', mime: string | null, writable: boolean }`, `targetFromEntry(entry)` (writable when letters contain W, C or K), `accessOptions(target, kind, policy)` per D-16, `generatePassword(minLength, random = crypto.getRandomValues bound)` per D-18. In `error-text.ts` add the new codes to `KNOWN`, a `tooManyShares` case with minutes like `nextcloudLocked`, and `ncMessageOf(error)` returning the trimmed `extra.ncMessage` string or null.
|
||||||
|
|
||||||
|
**Dialog, menu, indicator (per D-19, D-04).** New `components/ShareDialog.tsx` on `Dialog` (wide) with props `{ target: ShareTarget, onClose, onChanged, onExpired }`: loads policy and `listSharesForPath` on open (loading and error states), section „Personen und Gruppen“ per D-19 (combobox with `aria-expanded`, `aria-controls`, `aria-activedescendant`, arrow keys and Enter; results grouped users first; groups hidden when policy.groupsEnabled is false; one create at a time, controls disabled while busy), rows per D-19, a muted note when `hidden > 0` („{count} weitere Freigaben, zum Beispiel per E-Mail, sehen Sie nur in Nextcloud.“), policy enabled false → only the text of `sharingDisabled`. After every successful change call `onChanged`. New `components/ShareIndicator.tsx` per D-19 (outgoing button / incoming marker, focus ring, Mosaik tokens). Add lucide-style icons to `icons.tsx`: ShareIcon (lucide share-2), LinkIcon (link), UserIcon (user), UsersIcon (users), CopyIcon (copy). `FileBrowser.tsx`: `DialogState` gains `{ kind: 'share'; target: ShareTarget }`; `menuActions` inserts `{ id: 'share', label: t('menu.share'), icon: ShareIcon, onSelect }` after move, only when `entry.permissions.includes('R')` and the new prop `sharingEnabled` (default true) is true; no share action in the multi-selection bar; render ShareDialog for that state; `onChanged` reloads the current folder quietly. `FileList.tsx` / `FileGrid.tsx` render ShareIndicator next to the name (outgoing when `shareTypes.length > 0`, incoming when permissions contain S) without breaking the dense row layout or truncation; clicking it opens the dialog through a callback from FileBrowser (it must not select or open the row). Extend `FileBrowser.test.tsx` (mock the new api functions) and write `ShareDialog.test.tsx` per behavior. Messages: add `nextcloudFiles.share.*` (dialog texts), `browser.menu.share`, indicator texts and every new `codes.*` text plus `codes.ncDetail` „Meldung der Nextcloud: {detail}“ in de AND en (formal Sie, real umlauts, no tenant or licence words); add correct German tokens with ae/oe/ue/ss to `UMLAUT_ALLOWLIST` when the guard asks.
|
||||||
|
|
||||||
|
**Live e2e (per D-20).** Write `.planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh` with the Write tool (bash, `set -euo pipefail`, sources `../../261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/e2e-lib.sh` relative to its own directory, header comment: test values only, reads no .env). Argument: section `people`, `links`, `received`, `version` or `all` (default all; this task implements setup, cleanup and `people`; Task 2 adds `links` and `received`, Task 3 `version`). Setup: wait for health, admin login, activate, set address, `e2e_connect_anna` (expect 200), probe `GET $API/modules/nextcloud-files/shares/policy` — anything but 200 fails with the hint „API-Container neu bauen: docker compose up -d --build api“; ensure Nextcloud user `ben` (password `User3-Pass-12345`, display name „Ben Beispiel“, created with `docker exec -e OC_PASS=… -u www-data tessera-nc-test php occ user:add --password-from-env --display-name=… ben` only when `occ user:info ben` fails) and group `tessera-team` containing ben (idempotent); switch Nextcloud rate limits off for the run; create the fixture folder `/Tessera-Teilen-<epoch>` with `Bericht.txt` and subfolder `Briefkasten` in anna's account via DAV (`curl -u anna:…`). The `trap … EXIT` deletes the fixture folder (removes its shares), deletes ben's fixtures, resets every occ key the script touched (rate limit, link password and expiry keys, anna's default_accept) and removes `$E2E_TMP`. Section people: policy JSON has enabled true and links.passwordRequired false; sharees for `ben` (itemType folder) contain user ben, for `tessera` contain group tessera-team; create user share on the fixture folder for ben with access edit → 201, kind user, access edit, permissions 15; ben's own `GET …/shares?shared_with_me=true` (curl -u ben, OCS headers, JSON) lists the folder; the same create again → 409 shareAlreadyExists and ben still sees exactly one share; update to view → 200 permissions 1 and ben sees permissions 1; group share of `Bericht.txt` for tessera-team with view → 201; `GET shares/by-path` of the folder lists exactly the user share; `GET files?path=/` shows the fixture folder with shareTypes containing 0 and `GET files?path=<fixture>` shows Bericht.txt with shareTypes containing 1; DELETE the user share → 200, ben no longer sees it, by-path empty; `DELETE shares/abc` → 404 shareNotFound; every error status seen is neither 401 nor 403. Print `e2e shares people ok`.
|
||||||
|
|
||||||
|
**Rebuild, run, commit.** `docker compose up -d --build api web`, wait for /health, run the verify chain. Commit `feat(nextcloud-files): Teilen mit Personen und Gruppen – Durchstich` with exactly the files of this task (see context, Git). Do not push.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>pnpm --filter @tessera/api exec vitest run src/nextcloud-files module-manage-handlers && pnpm --filter @tessera/web exec vitest run modules/nextcloud-files components/nextcloud-files src/lib/nextcloud-files src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/nextcloud-files apps/web/src/components/nextcloud-files "apps/web/src/app/(portal)/modules/nextcloud-files" apps/web/src/lib/nextcloud-files-api.ts && node -e 'const de=require("./apps/web/src/messages/de.json"),en=require("./apps/web/src/messages/en.json");const w=(o,p,r)=>{for(const[k,v]of Object.entries(o||{})){const q=p+"."+k;if(v&&typeof v==="object")w(v,q,r);else r[q]=v}return r};const a=w(de.nextcloudFiles,"n",{}),b=w(en.nextcloudFiles,"n",{});if(Object.keys(a).length===0||Object.keys(a).sort().join()!==Object.keys(b).sort().join()){console.error("key mismatch");process.exit(1)}for(const v of [...Object.values(a),...Object.values(b)])if(/mandant|tenant|lizenz|licens|→|·/i.test(String(v))){console.error("bad text",v);process.exit(1)}' && bash .planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/nc-test-setup.sh && bash .planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh people && echo "task1 ok"</automated>
|
||||||
|
</verify>
|
||||||
|
<done>A connected user shares a file or folder with a Nextcloud user or group from the row menu, changes the permission and removes the share; the share indicator appears in list and grid; the API validates before calling Nextcloud, never answers 401/403 and limits creates to 15 per 10 minutes; specs, tsc, biome and the live `people` section are green on the rebuilt stack; one commit on main, not pushed.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Public links under the Nextcloud policy (password, expiry, upload-only, copy), views „Von mir geteilt“ and „Mit mir geteilt“ with pending shares, opening shared items in the file view</name>
|
||||||
|
<files>apps/api/src/nextcloud-files/nextcloud-shares.ts, apps/api/src/nextcloud-files/nextcloud-shares.spec.ts, apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts, apps/api/src/nextcloud-files/nextcloud-files-shares.service.spec.ts, apps/api/src/nextcloud-files/dto/nextcloud-files-shares.dto.ts, apps/api/src/nextcloud-files/nextcloud-files.controller.ts, apps/api/src/nextcloud-files/nextcloud-files.controller.spec.ts, apps/api/src/module-registry/module-manage-handlers.spec.ts, apps/web/src/lib/nextcloud-files-api.ts, apps/web/src/lib/nextcloud-files-api.test.ts, apps/web/src/components/nextcloud-files/share-policy.ts, apps/web/src/components/nextcloud-files/share-policy.test.ts, apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareDialog.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareDialog.test.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/LinkShareForm.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/SharesView.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/SharesView.test.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx, apps/web/src/app/(portal)/modules/nextcloud-files/nextcloud-files-page.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json, apps/web/src/messages/umlaut-dictionary.ts, .planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh</files>
|
||||||
|
<precondition>Task 1 is committed: `git log --oneline -1 -- apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts` shows the tracer commit and `e2e-shares.sh people` passes.</precondition>
|
||||||
|
<behavior>
|
||||||
|
- Link create (service): policy passwordRequired and no password → 400 sharePasswordRequired after only the PROPFIND and capabilities calls; with password, `expireDate: ''` and label `Kunde` on folder `/Projekte` access view → POST body literal `{"path":"/Projekte","shareType":3,"permissions":1,"password":"Geheim-Pass-2026!","expireDate":"","label":"Kunde"}`; access upload on folder → `"permissions":4`; upload on a file, upload or edit while uploadAllowed false → 400 shareAccessInvalid without POST; links disabled → 409 linkSharingDisabled; multipleLinks false and a link exists on the path → 409 shareAlreadyExists; expiry enforced and expireDate absent or '' → 400 shareExpiryRequired without POST; expireDate '2026-02-30' → 400 shareExpiryInvalid without POST; POST 400 with password sent → sharePasswordRejected with ncMessage; POST 404 with expireDate '2026-12-01' → shareExpiryInvalid with ncMessage; POST 403 → shareRejected. JSON.stringify of every result and of every thrown error body never contains the password.
|
||||||
|
- Link update: password '' while passwordRequired → sharePasswordRequired without PUT; expireDate '' while expiryEnforced → shareExpiryRequired without PUT; bodies contain only the changed fields (`{"password":"Neu-Pass-2026!x"}`, `{"expireDate":""}`, `{"permissions":4}`, `{"label":"Angebot"}`); PUT 400 with non-empty password → sharePasswordRejected; PUT 400 or 404 with expireDate sent → shareExpiryInvalid; PUT 404 otherwise → shareNotFound.
|
||||||
|
- Lists: mine = GET `https://cloud.example/nc/ocs/v2.php/apps/files_sharing/api/v1/shares` → `{ shares, hidden, truncated }` with types 0/1/3; received = GET `…/shares?shared_with_me=true` plus GET `…/shares/pending` → `{ shares, pending, hidden, truncated }` with types 0/1 only and `pending: true` on pending items; pending call answering 404 or 405 → `pending: []`; accept 17 → POST `https://cloud.example/nc/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/17` → `{ accepted: true }`; accept 404 → shareNotFound.
|
||||||
|
- Controller: `listMyShares` [0,'shares/mine'] and `listReceivedShares` [0,'shares/received'] declared before the parameter block; `acceptShare` [1,'shares/:id/accept'] at the end with HttpCode 200; all Benutzen level.
|
||||||
|
- Web helpers: `linkPasswordMode` → required / suggested / optional; `expiryRule(policy, '2026-10-09')` enforced 7 days → `{ required: true, defaultDate: '2026-10-16', maxDate: '2026-10-16', minDate: '2026-10-09' }`, default 7 not enforced → `{ required: false, defaultDate: '2026-10-16', maxDate: null, minDate: '2026-10-09' }`, none → defaultDate and maxDate null; `addDays('2026-12-28', 7)` = '2027-01-04'; `linkUpdateDiff(share, form)` returns only changed fields; `accessOptions` for a folder link with uploadAllowed → [view, edit, upload], file link → [view, edit], uploadAllowed false → [view].
|
||||||
|
- Link UI (mocked api): enforced password → password field required, submit disabled until filled, „Passwort erzeugen“ fills a value of at least 20 characters shown in plain text with a copy button; not enforced → switch „Mit Passwort schützen“ off (on when passwordSuggested); enforced expiry → date prefilled with defaultDate, `max` set, no way to clear it; optional expiry cleared → createShare receives `expireDate: ''`; „Nur hochladen“ offered only for folders; the created link row shows the URL in a read-only input and „Link kopieren“ calls `navigator.clipboard.writeText(url)` and shows „Kopiert“; „Löschen“ asks inline and only the confirmation calls deleteShare; an error sharePasswordRejected shows the Nextcloud message as second line.
|
||||||
|
- SharesView: mode byMe groups shares by path (item name, parent folder, recipients incl. „Link“, access, expiry) with „Freigaben bearbeiten“ opening ShareDialog and „Im Ordner zeigen“; mode withMe lists owner, access and expiry with „Öffnen“ (calls onOpen with folder path, or parent plus file name) and „Freigabe verlassen“ (confirmation, then deleteShare); pending items appear in „Noch nicht angenommen“ with „Annehmen“ (acceptShare) and „Ablehnen“ (deleteShare); empty states with instructions; `hidden > 0` note; connectionExpired → onExpired.
|
||||||
|
- Page: a connected Benutzen user sees the tabs Dateien, Von mir geteilt, Mit mir geteilt and no Einstellungen; a manager also Einstellungen; not connected Benutzen user → no TabBar (as before); policy enabled false → no share tabs and FileBrowser gets sharingEnabled false; „Öffnen“ in Mit mir geteilt switches to Dateien and FileBrowser starts in the target folder with the file focused; choosing a tab in the TabBar clears that start.
|
||||||
|
- FileBrowser: props `initialPath`/`initialFocus` win over `?path=` on mount and focus the named entry after the first load.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
**API: links (per D-01, D-02, D-03, D-07, D-08, D-11, D-15, D-16).** Extend `CreateShareDto` with kind `link` and optional `password` (IsString, MaxLength 256), `expireDate` (IsString, Matches `^(\d{4}-\d{2}-\d{2})?$`), `label` (IsString, MaxLength 255); `UpdateShareDto` gains the same optional fields (empty string = remove password / remove expiry / clear label). In the service: link create builds the JSON body in the order path, shareType 3, permissions, password (when given), expireDate (when the field is present, also `""`), label (when given); run every D-16 link check against the fresh policy and the by-path list before `checkShareCreate`; link update fetches the policy, applies the update checks and sends only changed fields; a real-date check (`new Date(value + 'T00:00:00Z')` round-trip) backs the DTO regex. The D-15 matrix branches for password and expiry are driven by the `sent` record (which fields were sent and whether non-empty). Add `mine(tenantId, userId)`, `received(tenantId, userId)` (two calls, pending tolerant of 404/405) and `accept(tenantId, userId, id)`; extend both specs first per behavior.
|
||||||
|
|
||||||
|
**API: routes.** Controller `listMyShares` (`@Get('shares/mine')`) and `listReceivedShares` (`@Get('shares/received')`) next to the Task-1 statics, `acceptShare` (`@Post('shares/:id/accept')`, `@HttpCode(200)`) at the very end; update the header route list, the controller spec and the Benutzen list in `module-manage-handlers.spec.ts`.
|
||||||
|
|
||||||
|
**Web: helpers, client, link form (per D-07, D-08, D-18).** `nextcloud-files-api.ts`: link fields in the create/update input types, `listMyShares()`, `listReceivedShares()`, `acceptShare(id)` (+ test). `share-policy.ts`: `linkPasswordMode(policy)`, `todayLocal()` (local calendar date `YYYY-MM-DD`), `addDays(date, n)` (pure calendar arithmetic in UTC), `expiryRule(policy, today)`, `linkUpdateDiff(share, form)`, link branch of `accessOptions` (+ tests). New `components/LinkShareForm.tsx` used inline by ShareDialog for create and edit: access radio group (Ansehen / Bearbeiten / Nur hochladen with one short explanation each, „Nur hochladen“ = „Andere legen Dateien in diesen Ordner, sehen aber nichts darin.“), password per D-07 (label states when Nextcloud requires it, „Passwort erzeugen“, show/hide, hint „Mindestens {count} Zeichen.“ when passwordMinLength is known; in edit mode „Passwort ändern“ and, only when not required, „Passwort entfernen“), expiry per D-08 (native date input with `min`/`max`, label states when Nextcloud requires it and the maximum in days; optional with default → prefilled plus „Ohne Ablaufdatum“; optional without default → switch „Ablaufdatum festlegen“), label field („Hilft Ihnen, mehrere Links auseinanderzuhalten.“), submit „Link erstellen“ / „Speichern“ and „Abbrechen“; after creating a link with a password show „Geben Sie das Passwort getrennt vom Link weiter. Tessera kann es später nicht mehr anzeigen.“ with a copy button while the panel is open. The browser re-checks nothing beyond the form rules; the API is the gate.
|
||||||
|
|
||||||
|
**Web: dialog link section (per D-19, D-04).** ShareDialog gains section „Link“: links disabled → the `linkSharingDisabled` text; otherwise link rows (label or „Link“, access, „mit Passwort“, „gültig bis {date}“, URL in a read-only input, „Link kopieren“ with clipboard fallback to selecting the text, „Ändern“, „Löschen“ with inline confirmation „Link löschen? Wer ihn hat, kommt danach nicht mehr an „{name}“.“) and „Link erstellen“ (or „Weiteren Link erstellen“ when multipleLinks allows it). Errors per section with the Nextcloud message line. Extend `ShareDialog.test.tsx` per behavior.
|
||||||
|
|
||||||
|
**Web: views and navigation (per D-04, D-06, D-17).** New `components/SharesView.tsx` with prop `mode: 'byMe' | 'withMe'`, `onOpen(path, focusName?)`, `onExpired`: loads `listMyShares` or `listReceivedShares`, dense list in the Mosaik style of FileList (TypeTile via `{ name, type: itemType, mime }`, name, muted parent folder, recipients or owner, access label, expiry), actions per behavior, ShareDialog for „Freigaben bearbeiten“ (target from the share: path, name, itemType, mime, itemWritable), reload after every change, empty states („Sie haben noch nichts geteilt. Öffnen Sie im Reiter „Dateien“ das Menü einer Datei oder eines Ordners und wählen Sie „Teilen“.“ / „Mit Ihnen hat noch niemand etwas geteilt.“), truncated and hidden notes; `SharesView.test.tsx` per behavior. `FileBrowser.tsx`: props `initialPath?: string`, `initialFocus?: string` used on mount instead of `?path=` (focus via the existing `focusAfterLoad`). `page.tsx`: `TabId` gains `sharedByMe` and `sharedWithMe`; tabs per D-19; TabBar rendered when there is more than one tab; when connected load `getSharePolicy()` once (connectionExpired → reloadStatus; other errors → policy null, tabs and Teilen stay visible); pass `sharingEnabled={policy?.enabled !== false}`; state `browserStart` set by `onOpen` (folder → path; file → parent plus name) together with the switch to the files tab, cleared whenever the user picks a tab; AccountBar also on the share tabs. Extend the page test and `FileBrowser.test.tsx`. Messages de + en for everything new (tabs, link form, views), allowlist as needed.
|
||||||
|
|
||||||
|
**Live e2e (per D-20).** Extend `e2e-shares.sh`. Section links: link on the fixture folder with access view and `expireDate: ""` → 201, kind link, `url` starting with `$NC_BASE/`, hasPassword false, expiration null; link with access upload on `Briefkasten` → permissions 4; upload on `Bericht.txt` → 400 shareAccessInvalid and anna's direct Nextcloud list for that path (curl -u anna) has no new link; set the link password policy → `GET shares/policy` shows links.passwordRequired true; link without password → 400 sharePasswordRequired with no new link in Nextcloud; link with a random strong password (`Tessera-E2E-` plus 16 random characters from /dev/urandom via python3 -I secrets) → 201, hasPassword true, the response body does not contain the password; PUT password `abc` → 400 sharePasswordRejected with a non-empty ncMessage; PUT password '' → 400 sharePasswordRequired; reset the password key; set default expiry, enforced, 7 days → policy shows expiryDefaultDays 7 and expiryEnforced true; create with `expireDate: ""` → 400 shareExpiryRequired; create with today+3 (`date -u -d '+3 days' +%F`) → 201 with that expiration; PUT expireDate today+30 → 400 shareExpiryInvalid (record the Nextcloud status seen for the SUMMARY by printing it); PUT expireDate '' → 400 shareExpiryRequired; create with expireDate `31.12.2026x` → 400 and no new link in Nextcloud; reset the expiry keys → policy back to expiryDefaultDays null; `GET shares/mine` lists the created links; delete every link → 200; `docker compose logs api --since <script start>` contains neither the password nor any link URL; every error status seen is neither 401 nor 403. At most four creates in this section. Section received: ben creates `/Ben-Ordner-<epoch>` and shares it with anna through Nextcloud directly (curl -u ben POST, form or JSON, OCS headers, permissions 1) → Tessera `GET shares/received` lists it with ownerName „Ben Beispiel“, canEdit false and target `/Ben-Ordner-<epoch>`; `GET files?path=/` lists the entry and its permissions contain S (print the letters; if S is missing fail with a hint to switch the incoming marker to `nc:mount-type`); `GET shares/mine` does not list it; Tessera DELETE as anna (leave) → 200 and received no longer lists it (print what ben's own list shows afterwards for the SUMMARY); set anna's default_accept to no, ben shares `/Ben-Briefkasten-<epoch>` → received.pending contains it and received.shares does not; `POST shares/<id>/accept` → 200 and received.shares contains it; leave again; reset the setting. Print `e2e shares links ok` / `e2e shares received ok`.
|
||||||
|
|
||||||
|
**Rebuild, run, commit.** `docker compose up -d --build api web`, wait for /health, run the verify chain (if a run hits 429 tooManyShares while debugging, `docker compose restart api` clears Tessera's in-memory counter). Commit `feat(nextcloud-files): Links nach den Regeln der Nextcloud, Von mir geteilt und Mit mir geteilt` with exactly the files of this task. Do not push.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>pnpm --filter @tessera/api exec vitest run src/nextcloud-files module-manage-handlers && pnpm --filter @tessera/web exec vitest run modules/nextcloud-files components/nextcloud-files src/lib/nextcloud-files src/messages && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/nextcloud-files apps/web/src/components/nextcloud-files "apps/web/src/app/(portal)/modules/nextcloud-files" apps/web/src/lib/nextcloud-files-api.ts && node -e 'const de=require("./apps/web/src/messages/de.json"),en=require("./apps/web/src/messages/en.json");const w=(o,p,r)=>{for(const[k,v]of Object.entries(o||{})){const q=p+"."+k;if(v&&typeof v==="object")w(v,q,r);else r[q]=v}return r};const a=w(de.nextcloudFiles,"n",{}),b=w(en.nextcloudFiles,"n",{});if(Object.keys(a).length===0||Object.keys(a).sort().join()!==Object.keys(b).sort().join()){console.error("key mismatch");process.exit(1)}for(const v of [...Object.values(a),...Object.values(b)])if(/mandant|tenant|lizenz|licens|→|·/i.test(String(v))){console.error("bad text",v);process.exit(1)}' && bash .planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/nc-test-setup.sh && bash .planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh all && echo "task2 ok"</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Links work under both policy variants (password enforced or not, expiry enforced or not) with server-side pre-validation and German errors; the views Von mir geteilt and Mit mir geteilt list, open, change, remove, leave, accept and decline shares; the live sections people, links and received are green on the rebuilt stack; one commit on main, not pushed.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Module changelog (1.0.0 extended), CHANGELOG and all four guides, full gates on the rebuilt stack, all e2e scripts, browser proof in dark and light mode</name>
|
||||||
|
<files>apps/api/src/nextcloud-files/nextcloud-files.changelog.ts, apps/api/src/module-registry/module-changelog.spec.ts, CHANGELOG.md, docs/anleitung-anwender.md, docs/anleitung-administration.md, docs/anleitung-betrieb.md, docs/anleitung-entwicklung.md, .planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh</files>
|
||||||
|
<precondition>Tasks 1 and 2 are committed and `e2e-shares.sh all` passes on the current stack.</precondition>
|
||||||
|
<behavior>
|
||||||
|
- module-changelog.spec: domains still has exactly one release 1.0.0 dated 2026-10-08; nextcloud-files still has exactly one release 1.0.0 dated 2026-10-08, now with seven `new` items (the four Etappe-1 items unchanged, followed by the three sharing items); the format tests (descending versions, real dates, de+en, no replacement spellings, no tenant/licence words) and „Seed-Version entspricht dem neuesten Changelog-Eintrag“ pass.
|
||||||
|
- e2e section version: `GET $API/modules/changelog/nextcloud-files` answers 200 and its first release has version 1.0.0 and contains the sharing item about public links.
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
**Version and changelogs (per D-05).** In `nextcloud-files.changelog.ts` append to the `changes` of the existing 1.0.0 release (version and date unchanged, the four existing items unchanged) three `new` items: de „Dateien und Ordner mit Kolleginnen, Kollegen und Gruppen aus Ihrer Nextcloud teilen, wahlweise zum Ansehen oder Bearbeiten; Berechtigungen ändern und Freigaben wieder entfernen.“ / en „Share files and folders with colleagues and groups from your Nextcloud, either to view or to edit; change permissions and remove shares again.“; de „Öffentliche Links erstellen und kopieren, für Ordner auch zum reinen Hochladen; Passwort und Ablaufdatum folgen den Regeln Ihrer Nextcloud.“ / en „Create and copy public links, for folders also upload-only; password and expiry date follow the rules of your Nextcloud.“; de „Die Ansichten „Von mir geteilt“ und „Mit mir geteilt“ zeigen alle Freigaben, und geteilte Einträge sind in der Dateiliste markiert.“ / en „The views “Shared by me” and “Shared with me” list all shares, and shared items are marked in the file list.“. Adjust the pinned spec test only as far as needed for the behavior block (one release 1.0.0, seven items). In `CHANGELOG.md` under „## Unveröffentlicht“ → „### Neu“, directly after the bullet „Dateien, Hochladen und Herunterladen: …“, add two bullets in plain words with „Sie“: „Dateien, Teilen: …“ (row menu „Teilen“, colleagues and groups from the Nextcloud, Ansehen or Bearbeiten, change and remove; public links with Ansehen, Bearbeiten or for folders Nur hochladen, „Link kopieren“; password and expiry date follow the rules of the company Nextcloud — when it requires a password Tessera asks for one and can create one; Tessera never stores the password; at most 15 new shares within 10 minutes; no „Modulversion …“ suffix) and „Dateien, Übersicht der Freigaben: …“ (tabs „Von mir geteilt“ and „Mit mir geteilt“, open, change, remove, leave, accept pending shares; mark in the file list; email and server shares only in Nextcloud). Add the section `version` to `e2e-shares.sh` per behavior (also part of `all`).
|
||||||
|
|
||||||
|
**Guides (per D-10; everyday language, „Sie“, detailed, no tenant or licence wording).** `docs/anleitung-anwender.md`, section „### Dateien (Nextcloud)“: add „Teilen“ to the row-menu list in „**Arbeiten mit Dateien:**“ and new paragraphs „**Teilen:**“ (where, who can be found, Ansehen vs. Bearbeiten, Nextcloud notifies the colleague, change and remove, the share symbol in the list), „**Link erstellen:**“ (what a public link is, Nur hochladen as a letter box, Link kopieren, password and expiry date — when Nextcloud requires them the form says so, „Passwort erzeugen“, give the password separately, Tessera cannot show it later, expiry optional otherwise), „**Von mir geteilt und Mit mir geteilt:**“ (both tabs, open, change, leave, accept or decline when Nextcloud asks), and one sentence each on the 15-per-10-minutes limit and on email/server shares visible only in Nextcloud. `docs/anleitung-administration.md`, section „### Dateien: Nextcloud anbinden“: paragraph „**Teilen und Regeln für Links:**“ — the rules are set in the Nextcloud administration under sharing settings (allow links, enforce link password incl. exception groups, default and enforced expiry with days, public upload, sharing with groups, automatic acceptance) and Tessera reads them for each user at every share action, no restart; the link address comes from Nextcloud: enter in Tessera the address under which the Nextcloud is reachable from outside, otherwise links carry the internal address (alternatively set `overwritehost`/`overwriteprotocol` in the Nextcloud config.php); users can only share what Nextcloud lets them share. `docs/anleitung-betrieb.md`, „### Dateien (Nextcloud)“: bullet „- **Teilen:** …“ (Tessera allows 15 new shares per user in 10 minutes, below Nextcloud's own 20 in 10 minutes whose „zu viele Anfragen“ would pause all requests to the Nextcloud for 15 minutes; the counter lives in the api process memory and a restart resets it; rules are read fresh from Nextcloud, nothing cached) plus a row in „### Fehlerbilder“ (links show an internal address → Nextcloud address in Tessera or overwritehost). `docs/anleitung-entwicklung.md`, „## Konventionen und Fallstricke“: paragraph „**Dateien (Nextcloud), Teilen (quick-261009-dkv):**“ — own OCS share layer `nextcloud-shares.ts` (reads error bodies; the login helper is not used), routes and the kind/access enums without raw bitmasks, policy fresh per write, the error matrix and why (PUT hides reasons, expiry errors arrive as 404, never switch on localised text), POST for an existing recipient returns the old share and re-sends mail (duplicate check), the 15/10 limiter vs. the origin pause, live test `e2e-shares.sh` with the occ keys and the rate-limit switch of the test Nextcloud.
|
||||||
|
|
||||||
|
**Final gates.** Full `pnpm --filter @tessera/api test` and `pnpm --filter @tessera/web test`, both tsc, biome lint on all touched paths. `docker compose up -d --build api web` (this also restarts the api process and clears Tessera's share counter), wait for /health, check the seed line in the api log, rerun `nc-test-setup.sh`, `e2e-settings.sh`, `e2e-connect.sh`, `e2e-files.sh`, `e2e-transfer.sh` (Etappe-1 regression, the file listing changed) and `e2e-shares.sh all`.
|
||||||
|
|
||||||
|
**Browser proof (per D-19, L-09).** With Playwright MCP (or the Etappe-1 fallback) at http://localhost:3000 as admin/admin123 with anna connected: prepare by DAV a folder „Projekte“ with a PDF and an XLSX and a folder „Angebote“, let ben share „Ben-Unterlagen“ with anna directly in Nextcloud and one more share while anna's default_accept is no (pending), set the link password policy for the enforced-password shots; reset every occ change at the end. Switch to dark mode with the theme button and capture under `.playwright-mcp/nextcloud-files/`: `s2a-dark-menu.png` (row menu with Teilen), `s2a-dark-people.png` (dialog with search results open and one existing user row), `s2a-dark-link-form.png` (link form under the enforced password with a generated password), `s2a-dark-links.png` (link row with URL and „Link kopieren“ after clicking it, „Kopiert“ visible), `s2a-dark-indicator.png` (list with outgoing and incoming markers), `s2a-dark-by-me.png`, `s2a-dark-with-me.png` (incl. „Noch nicht angenommen“), `s2a-dark-mobile.png` (dialog at 390×844); then light mode: `s2a-light-people.png`, `s2a-light-links.png`, `s2a-light-by-me.png`, `s2a-light-with-me.png`. Exercise in the browser: share with ben, change to Bearbeiten, remove; create a link, copy it, change its expiry, delete it; open an item from „Mit mir geteilt“ and land in its folder; accept the pending share. Review every screenshot against L-09 (calm dense list, readable markers in both modes, no ALL-CAPS labels, no middle dots, no arrow buttons, focus visible) and fix what is off (also in component files of Tasks 1–2; add them to this commit). Commit `feat(nextcloud-files): Teilen in Modul-Changelog, Anleitungen und Changelog` with exactly the files of this task. Do not push.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>pnpm --filter @tessera/api test && pnpm --filter @tessera/web test && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && pnpm exec biome lint apps/api/src/nextcloud-files apps/web/src/components/nextcloud-files "apps/web/src/app/(portal)/modules/nextcloud-files" apps/web/src/lib/nextcloud-files-api.ts && grep -q "Öffentliche Links" apps/api/src/nextcloud-files/nextcloud-files.changelog.ts && ! grep -q "version: '1.1.0'" apps/api/src/nextcloud-files/nextcloud-files.changelog.ts && grep -q "Dateien, Teilen:" CHANGELOG.md && grep -q "\*\*Teilen:\*\*" docs/anleitung-anwender.md && grep -q "Von mir geteilt und Mit mir geteilt" docs/anleitung-anwender.md && grep -q "Teilen und Regeln für Links" docs/anleitung-administration.md && grep -q "\*\*Teilen:\*\*" docs/anleitung-betrieb.md && grep -q "Dateien (Nextcloud), Teilen (quick-261009-dkv)" docs/anleitung-entwicklung.md && docker compose ps --status running --services | grep -qx api && docker compose ps --status running --services | grep -qx web && docker compose logs api 2>&1 | grep -q "Nextcloud files module seeded in registry" && E=.planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e && bash $E/nc-test-setup.sh && bash $E/e2e-settings.sh && bash $E/e2e-connect.sh && bash $E/e2e-files.sh && bash $E/e2e-transfer.sh && bash .planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh all && test "$(ls .playwright-mcp/nextcloud-files/s2a-dark-*.png 2>/dev/null | wc -l)" -ge 8 && test "$(ls .playwright-mcp/nextcloud-files/s2a-light-*.png 2>/dev/null | wc -l)" -ge 4 && echo "final gates ok"</automated>
|
||||||
|
<human-check>After the user's own pull on alpha (the user deploys, not Claude): open a file of the company Nextcloud, „Teilen“ → „Link erstellen“ and confirm that the form requires a password (the user expects the company policy to enforce it) and offers no forced expiry; copy the link and open it in a private browser window — the address must be the external Nextcloud address; share a file with a colleague and check that the colleague sees it in Nextcloud.</human-check>
|
||||||
|
</verify>
|
||||||
|
<done>Module stays 1.0.0, its unreleased module-changelog entry extended by the sharing items; CHANGELOG and the four guides describe sharing; full api and web suites, both tsc, biome, all Etappe-1 e2e scripts and every e2e-shares section green on the rebuilt stack; eight dark and four light screenshots reviewed against L-09; one commit on main, not pushed.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| browser → API (`/modules/nextcloud-files/shares*`, `sharees`) | untrusted caller; tenant and user only from the validated session; kind, access, path, recipient, password, date, label, ids untrusted |
|
||||||
|
| API → Nextcloud OCS (`/ocs/v2.php/...`) | outbound with the caller's own app password; every answer untrusted (JSON shape, messages, display names, URLs) |
|
||||||
|
| Nextcloud content → browser | display names, labels, Nextcloud messages, link URLs rendered or copied by the browser |
|
||||||
|
| public link URL → third parties | anyone with the URL reaches the item; password and expiry are Nextcloud's protection |
|
||||||
|
| shared server IP ↔ Nextcloud rate limit | one user's create burst can pause the Nextcloud for everyone (origin-wide gate) |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||||
|
| T-dkv-01 | Tampering / Elevation of Privilege | create/update permissions | high | mitigate | browser sends only the `kind` and `access` enums (class-validator IsIn, whitelist strips unknown fields such as raw permissions or shareType); the API maps them to bitmasks itself, item type and writability come from its own PROPFIND / GET by id, bit 16 is never sent, upload only for folder links; specs assert the literal bodies |
|
||||||
|
| T-dkv-02 | Elevation of Privilege | acting on other people's files or shares | high | mitigate | every call uses `getSession(tenantId, userId)` from the token; paths go through `parseUserPath` and stay in the caller's own Nextcloud account; Nextcloud enforces ownership (`can_edit`, incoming edit 403); share ids `^\d{1,20}$` before they become a segment; controller spec proves token-only identity |
|
||||||
|
| T-dkv-03 | Information Disclosure | link password | high | mitigate | password only in the request memory and the JSON body to Nextcloud (never in a URL), never stored, never logged, never echoed: responses carry `hasPassword` only, Nextcloud's `redacted` value is dropped; specs check JSON.stringify of results and errors; e2e greps the api log and the response |
|
||||||
|
| T-dkv-04 | Information Disclosure | link URL / token | medium | mitigate | `url` only for links owned by the caller and only http/https; token never forwarded as its own field; URLs never logged; the e2e greps the api log for the URL |
|
||||||
|
| T-dkv-05 | Tampering / SSRF | URLs in Nextcloud answers | high | mitigate | only fixed `/ocs/v2.php/` segments of the configured base via `ncRequest` (no redirects, no cookies); `url`, `api.generate` and any other URL from capabilities or shares are never requested |
|
||||||
|
| T-dkv-06 | Denial of Service | origin-wide pause after a Nextcloud 429 | high | mitigate | Tessera limiter 15 creates / 10 min / user below Nextcloud's 20 / 600 s, checked before the POST and after pre-validation; one create at a time in the UI; specs for the limiter; Etappe-1 call gate still pauses on any 429 |
|
||||||
|
| T-dkv-07 | Denial of Service | large share lists / responses | low | mitigate | body caps (8 MiB, 64 KiB errors, 1 MiB capabilities), 2000 shares with `truncated`, sharee search `perPage=20` and 25 results, term ≤ 100 |
|
||||||
|
| T-dkv-08 | Tampering | input values | medium | mitigate | DTOs: strict date regex plus real-date check, label ≤ 255, recipient ≤ 255 without control characters, password ≤ 256, term ≤ 100; lenient Nextcloud date parsing never sees anything but `YYYY-MM-DD` or `""` |
|
||||||
|
| T-dkv-09 | Repudiation / Spoofing | duplicate POST re-sends notifications | low | mitigate | by-path duplicate check → `shareAlreadyExists` without POST; permission changes always via PUT |
|
||||||
|
| T-dkv-10 | Information Disclosure | raw Nextcloud share objects | medium | mitigate | parsers build a small view (no storage ids, attributes, mail flags, email or federated recipients); other share types become a count only |
|
||||||
|
| T-dkv-11 | Elevation of Privilege (client) / error contract | Nextcloud 401/403 reaching the web | high | mitigate | `mapShareFailure` + `mapNcFailure`: 401 → `connectionExpired` with `markExpired`, 403 → `shareRejected` 422; it.each matrix asserts no 401/403 ever leaves the API |
|
||||||
|
| T-dkv-12 | Tampering (stored XSS) | display names, labels, messages, URLs | medium | mitigate | all rendered as React text, control characters stripped and lengths capped on the API side; link URL only in a read-only input, no `dangerouslySetInnerHTML`, clipboard writes plain text |
|
||||||
|
| T-dkv-13 | Elevation of Privilege | policy bypass by a crafted request | medium | mitigate | API re-checks password/expiry/link/upload/group rules from fresh capabilities on every write; Nextcloud stays the final authority |
|
||||||
|
| T-dkv-14 | Elevation of Privilege | route shadowing | low | mitigate | statics before `:id` routes, declaration-order assertion in the controller spec, e2e calls every route |
|
||||||
|
| T-dkv-15 | Information Disclosure | links leaking an internal host | low | accept | Tessera shows the URL Nextcloud built and never rewrites it; the administration guide explains the external address / overwritehost |
|
||||||
|
| T-dkv-SC | Tampering | npm/pip/cargo installs | low | accept | no new packages (research: Package Legitimacy Audit not applicable) |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Each task's `<automated>` chain passes; Task 1 proves the people path live, Task 2 the links (both policy variants) and incoming/pending shares live, Task 3 reruns every suite and every e2e script (Etappe 1 and 2a) on the rebuilt stack and adds the browser proof.
|
||||||
|
- Multi-source coverage audit:
|
||||||
|
|
||||||
|
| Source item | Covered by |
|
||||||
|
|-------------|------------|
|
||||||
|
| GOAL: share files and folders via Nextcloud under the caller's own app password; list Von mir geteilt / Mit mir geteilt; indicator; change and remove; follow the sharing policy; search excluded | Tasks 1–3 |
|
||||||
|
| D-01 users AND groups via sharee search + public links | Task 1 (users/groups, sharees), Task 2 (links) |
|
||||||
|
| D-02 policy from capabilities, required/default/maximum in the form, German policy errors, optional fields free | Task 1 (parseSharePolicy, error codes), Task 2 (link pre-validation, form, matrix, e2e with occ toggles) |
|
||||||
|
| D-03 Ansehen / Bearbeiten, folders Nur hochladen, no bit checkboxes | Task 1 (enums, mapping), Task 2 (upload for folder links) |
|
||||||
|
| D-04 both views + indicator, open from both places to change/remove | Task 1 (indicator, dialog from list), Task 2 (SharesView, navigation) |
|
||||||
|
| D-05 version stays 1.0.0, unreleased entry extended + CHANGELOG | Task 3 |
|
||||||
|
| D-06 only types 0/1/3, others filtered | Task 1 (parsers, hidden count), Task 2 (mine/received filters) |
|
||||||
|
| D-07 password required + „Passwort erzeugen“ / optional toggle | Task 2 (LinkShareForm, generator from Task 1) |
|
||||||
|
| D-08 expiry required with default/max / optional, `expireDate: ""` | Task 2 |
|
||||||
|
| D-09 limiter 15 / 10 min / user | Task 1 |
|
||||||
|
| D-10 docs (Anwender, Administration, Betrieb, Entwicklung) + German Sie texts | Task 3 (guides), Tasks 1–2 (messages, parity check) |
|
||||||
|
| CONTEXT discretion: copy link, label/note, notifications, received navigation, accept/decline pending, dialog layout, error mapping, limits, test strategy | D-17, D-18, D-19, D-15, D-12, D-20 in Tasks 1–3 |
|
||||||
|
| CONTEXT specifics: Etappe-1 safety rules, davRequest/ocsRequest hooks, permission letter R, action-list menu, tessera-nc-test with occ policy | Tasks 1–2 (ncRequest, menu action, R check, e2e) |
|
||||||
|
| RESEARCH: ocsRequest unsuitable → own helper reading error bodies | Task 1 (D-12) |
|
||||||
|
| RESEARCH: JSON bodies; sharees itemType required, indexed shareType keys | Task 1 (spec literals) |
|
||||||
|
| RESEARCH: capabilities per user, conditional keys, days as string | Task 1 (parseSharePolicy), D-13 |
|
||||||
|
| RESEARCH: PUT hides reasons → pre-validate; error shapes | Task 1–2 (D-15/D-16, refined by the source check: expiry errors are 404) |
|
||||||
|
| RESEARCH: lenient expireDate parsing → strict validation | Task 2 (DTO + real-date check, e2e `31.12.2026x`) |
|
||||||
|
| RESEARCH: POST for existing recipient returns old share and re-sends mail | Task 1 (shareAlreadyExists) |
|
||||||
|
| RESEARCH: default notification, no sendMail | D-18 |
|
||||||
|
| RESEARCH: oc:share-types already requested → shareTypes | Task 1 |
|
||||||
|
| RESEARCH: list caps and truncated | Task 1 |
|
||||||
|
| RESEARCH: route order statics first | Tasks 1–2 (controller spec) |
|
||||||
|
| RESEARCH: 401 handling via mapNcFailure | Task 1 |
|
||||||
|
| RESEARCH: received shares not editable; leave via DELETE; pending accept POST | Task 2 |
|
||||||
|
| RESEARCH: link URL host from Nextcloud | Task 3 (admin + operations guide, human check) |
|
||||||
|
| RESEARCH: file vs folder permissions (edit 3 vs 15, upload folder only) | Task 1–2 (permissionsFor, accessOptions) |
|
||||||
|
| RESEARCH: folders containing shares → 403 German | Task 1 (shareRejected with ncMessage) |
|
||||||
|
| RESEARCH: dialog on Dialog to keep shortcuts away | Task 1 (D-19) |
|
||||||
|
| RESEARCH: password_policy minLength as hint; generator | Task 1–2 (D-18; deviation from the generate endpoint justified) |
|
||||||
|
| RESEARCH A1 (S letter) | Task 2 e2e measures it, fallback named |
|
||||||
|
| RESEARCH A3 (occ keys) | verified at planning, used in Task 2 e2e |
|
||||||
|
| RESEARCH A5/A6 | limiter independent of Retry-After (Task 1); can_edit check plus mapped 403 (Task 1) |
|
||||||
|
| RESEARCH open question 1 (extend 1.0.0 vs 1.1.0) | decided by orchestrator: extend 1.0.0 per guide rule (D-05) |
|
||||||
|
| RESEARCH open questions 2 and 3 | D-06, D-07 |
|
||||||
|
| Deferred / out of scope: search; licensing and multi-tenancy topics | not planned |
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- From the file view and from both share views a connected user shares with users, groups and by public link, changes and removes shares, leaves incoming shares and accepts pending ones; shared entries are marked in list and grid.
|
||||||
|
- Link password and expiry behave exactly as the user's Nextcloud policy says — proven live with the policy switched on and off in the test Nextcloud; Nextcloud refusals arrive as German texts with the Nextcloud message; no 401/403 reaches the browser; the password never appears in a response or log.
|
||||||
|
- 15 creates per 10 minutes per user in Tessera; duplicates refused without re-notification.
|
||||||
|
- Module version stays 1.0.0 with its unreleased module-changelog entry extended; CHANGELOG and all four guides updated.
|
||||||
|
- Full api and web suites, both tsc runs, biome lint, all Etappe-1 e2e scripts and every e2e-shares section green on the rebuilt stack; eight dark and four light screenshots reviewed against L-09.
|
||||||
|
- Three commits on main, nothing pushed, nothing deployed.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/261009-dkv-SUMMARY.md` when done (not committed by the executor). Besides the measured gate table per task, deviations and threat status it must contain: (1) the measured Nextcloud statuses printed by the e2e (expiry beyond the maximum on update, weak password on update) and whether they matched D-15; (2) whether received items carried S in their permissions and what ben's list showed after anna left the share; (3) confirmation that the version stayed 1.0.0 per the guide rule „Höchstens ein Sprung je Modul zwischen zwei Tessera-Freigaben“; (4) the screenshot list with paths; (5) a checklist for the user's real environment: company Nextcloud version and its link rules (password enforced? expiry?) as Tessera shows them, link URL uses the external Nextcloud address, a colleague receives a share, the desktop app copies a link (clipboard) — deployment and pull stay with the user.
|
||||||
|
</output>
|
||||||
+272
@@ -0,0 +1,272 @@
|
|||||||
|
# Quick 261009-dkv: Dateien Etappe 2a (Teilen) - Research
|
||||||
|
|
||||||
|
**Researched:** 2026-10-09
|
||||||
|
**Domain:** Nextcloud OCS Files Sharing API + Sharee API, integrated into the existing `nextcloud-files` module (NestJS + Next.js)
|
||||||
|
**Confidence:** HIGH (NC 34.0.4 server source read in the test container and probed live; write-path shapes from source, not from live POST/PUT)
|
||||||
|
|
||||||
|
<user_constraints>
|
||||||
|
## User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Locked Decisions
|
||||||
|
- Share targets: both. Colleagues (Nextcloud users AND groups, found via Nextcloud's sharee search) and public links.
|
||||||
|
- Link protection: follow the Nextcloud server policy, do not invent Tessera-side rules. User believes the company Nextcloud enforces a password for links but NOT an expiry date. Tessera must read the policy from the Nextcloud capabilities (password enforced, expiry enforced/default days, etc.) and reflect it in the form (required fields, defaults, maximums); Nextcloud's error messages on policy violations must be shown understandably in German. Optional fields (expiry when not enforced) remain freely settable.
|
||||||
|
- Permissions: simple choice "Ansehen" (read only) or "Bearbeiten" (edit). For folders additionally "Nur hochladen" (file drop / Briefkasten, mainly for public links). No per-bit checkboxes.
|
||||||
|
- Overview: both views "Von mir geteilt" and "Mit mir geteilt" as separate views in the module, plus a share indicator on every shared entry in the file list. Shares can be opened from both places to change or remove them.
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
Copy-link button, optional link label/note, notification behaviour (Nextcloud's own for user/group shares is fine), how "Mit mir geteilt" items are opened/navigated, accepting/declining pending incoming shares if the server requires it, UI layout of the share dialog (follow existing dialog patterns), error mapping, rate/size limits, test strategy.
|
||||||
|
|
||||||
|
### Deferred Ideas (OUT OF SCOPE)
|
||||||
|
Search (later quick). Not mentioned in CONTEXT but excluded by project memory: licensing, multi-tenancy topics.
|
||||||
|
</user_constraints>
|
||||||
|
|
||||||
|
## Project Constraints (from CLAUDE.md / memory)
|
||||||
|
- Work only via GSD workflow; German UI texts with "Sie", real umlauts; user chat in German with "du".
|
||||||
|
- Etappe-1 safety rules stay: fixed path prefixes (`/ocs/v2.php/` already allowed), no redirects, no cookies, never call a URL from an NC response, call gate on 429, German error contract (never 401/403 to the browser), `tenantId`/`userId` only from token.
|
||||||
|
- Every module change: module changelog entry + docs; no Docker deploy to the test server by Claude; no password-leak warnings; ASVS level 1, `security_enforcement: true`.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The whole feature is a thin, typed proxy over eight OCS calls, all under the already allowed prefix `/ocs/v2.php/`. The existing `ocsRequest` (in `nextcloud-auth-client.ts`) is NOT usable as-is: it has no body/query support, it maps every 403 to `app-password-given` and throws away the body of every non-2xx answer, which is exactly where the share error messages live. Add a new share-specific OCS helper (in a new `nextcloud-shares.ts`, same layer as `nextcloud-dav.ts`) on top of `ncRequest` that returns `{status, ocsMessage, data}`.
|
||||||
|
|
||||||
|
Three server behaviours drive the design: (1) policy lives in `GET /ocs/v2.php/cloud/capabilities` and is PER USER (password-enforcement excludes groups); (2) on PUT, Nextcloud hides policy violations behind the generic message `Failed to update share.`, so Tessera must pre-validate password/expiry from the capabilities; (3) `createShare` has a Nextcloud user rate limit of 20 per 600 s, and a 429 without `Retry-After` would pause the ENTIRE origin for 15 min in Tessera's call gate, so Tessera needs its own lower per-user limit on creates.
|
||||||
|
|
||||||
|
**Primary recommendation:** New files `nextcloud-shares.ts` (OCS layer + parsers) and `nextcloud-files-shares.service.ts` (+ DTOs, controller routes), JSON bodies for POST/PUT, strict Tessera-side validation of date/permissions/recipient, capabilities fetched per request (short per-credential cache), password never stored/logged/echoed, 15-creates-per-10-min Tessera limiter, extend the unreleased module changelog entry 1.0.0.
|
||||||
|
|
||||||
|
## Architectural Responsibility Map
|
||||||
|
|
||||||
|
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||||
|
|------------|-------------|----------------|-----------|
|
||||||
|
| Share CRUD, sharee search, capabilities | API / Backend (NestJS, own app password) | Nextcloud (authority) | Browser never talks to NC; credential is decrypted only in `getSession` |
|
||||||
|
| Policy enforcement (password/expiry/permissions) | Nextcloud | API pre-validation, Browser form hints | Server is authority; API/Browser only mirror it to avoid opaque errors |
|
||||||
|
| Share indicator in file list | API (parse `oc:share-types` from existing PROPFIND) | Browser | Data already requested by `PROPFIND_BODY`, just not parsed |
|
||||||
|
| Copy-link / display of link URL | Browser | API passes NC's `url` field | URL is only displayed/copied, never fetched by Tessera |
|
||||||
|
| Create-rate limiting | API (own limiter) | Nextcloud `UserRateLimit` | Avoid origin-wide gate pause |
|
||||||
|
| Sharee name resolution, "Mit mir geteilt" navigation | Browser (existing browser navigates `file_target`) | API | Received shares are mounted in the recipient's own tree |
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
No new packages. Everything needed exists: `undici` (via `ncRequest`), `class-validator` DTOs, vitest 3.2.6 (api) / 4.1.9 (web), Biome. **Package Legitimacy Audit:** not applicable, no external packages are installed in this task. Packages removed (SLOP): none. Flagged (SUS): none.
|
||||||
|
|
||||||
|
## Nextcloud API facts (all against NC 34.0.4 in `tessera-nc-test`)
|
||||||
|
|
||||||
|
All calls: `OCS-APIRequest: true`, `Accept: application/json` (already set by `ncRequest` with `ocs: true`), Basic auth with the user's app password. v2 mirrors the OCS status into the HTTP status. Body of POST/PUT may be JSON (`Content-Type: application/json`): [VERIFIED live: `POST {"path":"/nope","shareType":3}` returned `Wrong path, file/folder does not exist`, not `Please specify a file or folder path`, so the JSON body was parsed]. Use JSON bodies: password never appears in a URL and no form-encoding quirks.
|
||||||
|
|
||||||
|
### Endpoints [VERIFIED: /var/www/html/apps/files_sharing/appinfo/routes.php:83-125, ShareAPIController.php in container]
|
||||||
|
| Purpose | Call | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| List my shares | `GET /ocs/v2.php/apps/files_sharing/api/v1/shares` | No params = shares created by me (all types incl. email, talk, federated: filter client side to types you handle) |
|
||||||
|
| Shares of one path | `GET .../shares?path=/Projekte&reshares=true` | `path` relative to the user's home, same string as `entry.path`. Unknown path: HTTP 404 `Wrong path, file/folder does not exist`. Without resharing rights only own shares are returned |
|
||||||
|
| Shared with me | `GET .../shares?shared_with_me=true` | Own shares are filtered out. Types user, group (+circle, room, deck: ignore) |
|
||||||
|
| Pending incoming | `GET .../shares/pending` | Returns `permissions: 0` per item. Only relevant when the admin turned off auto-accept |
|
||||||
|
| Accept pending | `POST .../shares/pending/{id}` | Route exists for POST only (PUT gives 405). Decline = `DELETE .../shares/{id}` by the recipient |
|
||||||
|
| Create | `POST .../shares` | Body fields below |
|
||||||
|
| Update | `PUT .../shares/{id}` | Several fields in ONE request are fine (source handles permissions, password, expireDate, note, label together). If NONE of the known fields is sent: 400 `Wrong or no update parameter given`. Send only changed fields |
|
||||||
|
| Delete | `DELETE .../shares/{id}` | 200 with empty data. Recipient of a group share only leaves (deleteFromSelf) |
|
||||||
|
| Sharees | `GET .../sharees?search=&itemType=file\|folder&perPage=20&shareType%5B0%5D=0&shareType%5B1%5D=1` | `itemType` is REQUIRED (else 400 `Missing itemType`). `ncRequest.query` is a `Record<string,string>` so duplicate `shareType[]` is impossible; the indexed form `shareType[0]=0&shareType[1]=1` works [VERIFIED live, returned users+groups only] |
|
||||||
|
| Capabilities | `GET /ocs/v2.php/cloud/capabilities` | Per user. ~100 KB JSON |
|
||||||
|
| Password generate (optional) | `GET /ocs/v2.php/apps/password_policy/api/v1/generate` | Live answer `{"data":{"password":"YJMsw7P9DE"}}` (10 chars). Capabilities contain absolute `api.generate` URLs: ignore them, use the fixed path |
|
||||||
|
|
||||||
|
### Create body (POST) [VERIFIED: ShareAPIController::createShare signature]
|
||||||
|
`path` (string), `shareType` (0 user, 1 group, 3 link; 4 = email out of scope), `shareWith` (user id / group id; not for links), `permissions` (int), `password` (link), `expireDate` (`YYYY-MM-DD`), `label` (link, max 255), `note`, optional `sendMail` ('true'/'false').
|
||||||
|
|
||||||
|
Permission masks (read=1, update=2, create=4, delete=8, share=16):
|
||||||
|
| UI choice | Folder | File | Source of truth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Ansehen | 1 | 1 | |
|
||||||
|
| Bearbeiten | 15 (1+2+4+8) | 3 (1+2) | server strips create/delete for files: `$permissions & ~(Constants::PERMISSION_DELETE \| Constants::PERMISSION_CREATE)` |
|
||||||
|
| Nur hochladen (folder link only) | 4 | not offered | link validation: `Share must at least have READ or CREATE permissions` |
|
||||||
|
Do not send 16 (resharing) in this etappe. User/group shares always get READ OR-ed in by the server; send explicit permissions always (omitted = `default_permissions`, here 31 incl. share bit). Offer "Bearbeiten" only if the entry's DAV permission letters allow it (own root shows `RGDNVCK` [VERIFIED live PROPFIND]); letters: `R` reshare, `G` read, `W` write, `D` delete, `N`/`V` rename/move, `C`/`K` create, `S` = received share [ASSUMED: letter meaning from Nextcloud DAV docs, not re-read this session].
|
||||||
|
|
||||||
|
### Response share object [VERIFIED live for a user share; link-specific keys VERIFIED from formatShare source]
|
||||||
|
User share (live, folder `/Projekte` to `zoe`): `{"id":"1","share_type":0,"uid_owner":"anna","displayname_owner":"Anna Müller","permissions":31,"can_edit":true,"can_delete":true,"stime":1791532378,"parent":null,"expiration":"2026-12-31 23:59:59","token":null,"uid_file_owner":"anna","note":"","label":"","displayname_file_owner":"Anna Müller","path":"/Projekte","item_type":"folder","item_permissions":31,"is-mount-root":false,"mount-type":"","mimetype":"httpd/unix-directory","has_preview":false,"storage_id":"home::anna","storage":3,"item_source":294,"file_source":294,"file_parent":93,"file_target":"/Projekte","item_size":6810,"item_mtime":1791489905,"share_with":"zoe","share_with_displayname":"Zwei Faktor","share_with_displayname_unique":"zoe","mail_send":1,"hide_download":0,"attributes":null}` (data is an OBJECT for POST/PUT, an ARRAY for GET lists and GET by id).
|
||||||
|
Link shares additionally carry `token`, `url`, `password` (the literal string `redacted` when set, else null), `share_with` (same redacted value), `send_password_by_talk`. `expiration` is `Y-m-d H:i:s` in the server timezone: take the first 10 chars. For incoming shares `file_target` is the path inside the recipient's own tree and `item_permissions` are the effective permissions. Parse into a small Tessera `NcShare` type, never forward the raw object (drops `storage_id`, `attributes`, etc.).
|
||||||
|
|
||||||
|
### Capabilities (policy) [VERIFIED: Capabilities.php source + live JSON]
|
||||||
|
Live (`files_sharing`): `public.password.enforced=false`, `public.password.askForOptionalPassword=false`, `public.expire_date.enabled=false`, `public.expire_date_internal.enabled=false`, `public.upload=true`, `public.upload_files_drop=true`, `public.multiple_links=true`, `resharing=true`, `group_sharing=true`, `default_permissions=31`, `sharee.minSearchStringLength=0`, `sharebymail.password.enforced=false`.
|
||||||
|
Rules from the source you must reproduce:
|
||||||
|
- If link sharing is off: `public = {"enabled": false}` and NO other `public.*` keys. If the share API is off: `api_enabled=false`. Hide the whole Teilen feature then.
|
||||||
|
- `public.expire_date.{days,enforced}` exist ONLY when `public.expire_date.enabled` is true (enabled = default expiry configured). Same for `public.expire_date_internal` (applies to USER and GROUP shares, and it sits inside `public`, so it is missing when links are disabled: treat missing as "no policy").
|
||||||
|
- `public.password.enforced` is evaluated for the calling user (excluded groups: `shareapi_enforce_links_password_excluded_groups`): fetch with the user's own credential, never cache across users.
|
||||||
|
- `password_policy` capability exists (`minLength: 10`, `enforceNonCommonPassword: true`, HIBP true in the test server). Which context applies to sharing is unclear (only an `account` context is listed): use it as a hint only.
|
||||||
|
- Expiry semantics [VERIFIED: Manager::validateExpirationDateLink]: today is allowed, earlier is `Expiration date is in the past`; when enforced the date must be present and at most today + `days` (`Cannot set expiration date more than %n days in the future`). When a default expiry is enabled and NO `expireDate` is sent on create, the server fills today + default days. To create WITHOUT expiry when it is optional, send `expireDate: ""` (empty string sets `setNoExpirationDate`). So: pre-fill the form with today+days when `enabled`, send `""` when the user clears an optional field, omit it only to accept the server default.
|
||||||
|
- PHP parsing of `expireDate` is lenient: `"31.12.2026x"` was ACCEPTED and created a share [VERIFIED live, see Test-state note]. Tessera must validate `^\d{4}-\d{2}-\d{2}$` AND a real calendar date and send exactly that.
|
||||||
|
|
||||||
|
### Error shapes (OCS meta in the body; HTTP status mirrors `statuscode`) [VERIFIED live unless noted]
|
||||||
|
| Case | HTTP | `ocs.meta.message` |
|
||||||
|
|---|---|---|
|
||||||
|
| unknown path | 404 | `Wrong path, file/folder does not exist` |
|
||||||
|
| unknown share id (GET/PUT/DELETE/accept) | 404 | `Wrong share ID, share does not exist` |
|
||||||
|
| unknown user | 404 | `Please specify a valid account to share with` |
|
||||||
|
| unknown group | 404 | `Please specify a valid group` [source] |
|
||||||
|
| unknown share type | 400 | `Unknown share type` |
|
||||||
|
| bad link permissions (2) | 400 | `Share must at least have READ or CREATE permissions` |
|
||||||
|
| label > 255 | 400 | `Maximum label length is 255` |
|
||||||
|
| public upload on a file | 400 | `Public upload is only possible for publicly shared folders` |
|
||||||
|
| `sharees` without itemType | 400 | `Missing itemType` |
|
||||||
|
| password policy violated (POST and PUT) | 400 | the policy hint text, e.g. validate endpoint says `Password is among the 1,000,000 most common ones. Please make it unique. Password needs to be at least 10 characters long. Password is present in compromised password list. Please choose a different password.` [HintException wrapped as 400, source] |
|
||||||
|
| create: password missing while enforced, expiry rules, already-exists-via-group, sharing disabled, folder contains received shares | 403 | e.g. `Passwords are enforced for link and mail shares`, `Expiration date is enforced`, `You cannot share a folder that contains other shares` [source: `GenericShareException\|\InvalidArgumentException` become `OCSForbiddenException`] |
|
||||||
|
| update: ANY non-hint failure (enforced password removed, bad expiry, ...) | 400 | only `Failed to update share.` (details are logged server-side, not returned) |
|
||||||
|
| update of a share you did not create (recipient) | 403 | `You are not allowed to edit incoming shares` |
|
||||||
|
| delete without right | 403 | `Could not delete share` |
|
||||||
|
| user rate limit on create | 429 | no `Retry-After` header seen in the AppFramework code [VERIFIED: grep found none]; limit `#[UserRateLimit(limit: 20, period: 600)]` on `createShare` |
|
||||||
|
Messages are localized with the NC user's language, so never switch on message text. Map on (operation, HTTP status, whether password/expireDate were sent) and show the NC message as a secondary line (plain text, control chars stripped, max ~300 chars). Pre-validation from capabilities makes most of these unreachable.
|
||||||
|
|
||||||
|
Other verified behaviours:
|
||||||
|
- POST for a recipient that already has the share returns HTTP 200 with the EXISTING share, unchanged, and re-triggers the notification (`catch (AlreadySharedException $e) { ... $share = $e->getExistingShare();` in Manager::createShare). So the UI must not offer already-shared recipients and must use PUT to change permissions.
|
||||||
|
- Default notification: `mail_send` is 1 for user shares by default (mail goes out only if the recipient has an address and NC mail works). Leave Nextcloud's behaviour (decision: discretion), do not send `sendMail`.
|
||||||
|
- `sharees` result: `{"exact":{...},"users":[{"label":"Zwei Faktor","subline":"","icon":"icon-user","value":{"shareType":0,"shareWith":"zoe"},"shareWithDisplayNameUnique":"zoe","status":[]}],"groups":[{"label":"twofa","value":{"shareType":1,"shareWith":"twofa"}}],"remotes":[],"emails":[],...}`. Users and groups appear in `users`/`groups` AND (on exact match) in `exact.*`: merge and dedupe by `shareType:shareWith`. The caller (anna) is not listed. Pagination via `Link` response header (ignore, `perPage=20` is enough; the user refines the search).
|
||||||
|
- PROPFIND already requests `<oc:share-types/>`; unshared entries return an empty element (`<oc:share-types/>`, live). `parsePropfind` has `isArray` for `share-type` but `buildEntry` never reads it: add `shareTypes: number[]` to `NcEntry` (and to the web `NcEntry`). Received items carry the `S` letter in `oc:permissions` and are mounted at `file_target`.
|
||||||
|
- GET list responses are capped by `OCS_MAX_BYTES = 1 MiB` in `ocsRequest`; the new helper needs its own cap (suggest 8 MiB) and a share count cap (e.g. 2000, `truncated` flag like `MAX_LIST_ENTRIES`).
|
||||||
|
|
||||||
|
## Integration points (Etappe-1 code)
|
||||||
|
|
||||||
|
| Concern | Where | What to do |
|
||||||
|
|---|---|---|
|
||||||
|
| OCS transport | `apps/api/src/nextcloud-files/nextcloud-auth-client.ts` `ocsRequest` (lines ~120-175): `ncRequest(... prefix '/ocs/v2.php/', ocs: true)` | Do not extend it for shares (login code relies on 403 = `app-password-given`). Write `ocsShareRequest` in a new file using `ncRequest` with `method`, `segments` (e.g. `['apps','files_sharing','api','v1','shares', id]`), `query` (encoded per key/value by `buildNcUrl`), `headers: {'content-type':'application/json'}`, `body: JSON.stringify(...)`, `authorization/credentialKey` from `NcSession`. Return `{status, ocsMessage, data}`; read the body for non-2xx (cap 64 KiB) |
|
||||||
|
| Path whitelist | `nextcloud-http.ts` `ALLOWED_PREFIXES` | `/ocs/v2.php/` already allowed, nothing to add. `buildNcUrl` requires prefix ending in `/` for segments |
|
||||||
|
| Segments | `validateSegment` / `encodeSegments` | Share id: validate `^\d{1,20}$` in the DTO (`@Matches`) before it becomes a segment; entry `path` goes through `parseUserPath` and is rebuilt as `/${segments.join('/')}` before it is put in query/body |
|
||||||
|
| Session + errors | `NextcloudFilesService.session()/fail()` pattern, `mapNcFailure` in `nextcloud-upstream.ts` | New service uses `account.getSession(tenantId, userId)`, `mapNcFailure` for transport errors (401/credential-dead -> `connectionExpired`, paused/429 -> `nextcloudLocked`). Share-specific statuses go through a new mapper (below) |
|
||||||
|
| Error contract | `nextcloud-files.types.ts` `NcErrorCode` + `NC_ERROR_DEFAULTS` (never 401/403) | Add codes with German texts and HTTP 4xx other than 401/403: `shareRejected` (422, carries `ncMessage`), `sharePasswordRejected` (400, carries `ncMessage`), `shareExpiryInvalid` (400), `shareRecipientInvalid` (404 -> use 422 to avoid clashing with `notFound`), `shareLimit` (429 -> `tooManyAttempts`-style with `retryAfterSeconds`, own code `tooManyShares`), `sharingDisabled` (409). Web: add them to `KNOWN` in `components/nextcloud-files/error-text.ts` and to `nextcloudFiles.codes` in `src/messages/de.json`/`en.json` |
|
||||||
|
| Controller | `nextcloud-files.controller.ts` | Routes under `modules/nextcloud-files`. Static first, params at the END (spec `nextcloud-files.controller.spec.ts:193` checks order): `GET shares/capabilities`, `GET shares/mine`, `GET shares/received`, `GET shares/by-path`, `GET sharees`, `POST shares`, then at the end `PUT shares/:id`, `DELETE shares/:id`, `POST shares/:id/accept`. Class-level `@UseModule`, no role decorator (Benutzen level, like all file routes); update the doc comment listing routes |
|
||||||
|
| Rate limit | `nextcloud-login-guard.ts` has `pruneTimes` + `checkFlowStart` (10 per 10 min per user) | Add a sibling `checkShareCreate(userId)`: 15 per 10 min per user, throws `tooManyShares`. Reason: NC allows 20/600 s; its 429 has no `Retry-After`, `gate.pause(origin, undefined)` then pauses ALL users for `DEFAULT_PAUSE_SECONDS = 15 * 60` |
|
||||||
|
| Entry model | `nextcloud-propfind.ts` `NcEntry` (`permissions`, `fileId`, `favorite`...) | Add `shareTypes`; update `nextcloud-propfind.spec.ts`; web `lib/nextcloud-files-api.ts` `NcEntry` |
|
||||||
|
| Row menu | `FileBrowser.tsx` `menuActions(entry)` (lines ~638-695), `EntryAction` in `EntryMenu.tsx` | Insert `{id:'share', label, icon, onSelect: () => setDialog({kind:'share', entry})}` after `move`, only when `entry.permissions.includes('R')` (and multi-select: no share action, single entry only). Extend `DialogState` (line ~51) |
|
||||||
|
| Dialog | `components/Dialog.tsx` (`title`, `footer`, `wide`, `initialFocus`, focus trap, Escape) | `ShareDialog.tsx` wide variant; sections: "Mit Personen oder Gruppen" (sharee search + list), "Link" (create/list). Pattern of `NameDialog.tsx`/`DeleteDialog.tsx` for submit/error state; errors via `errorText(t, toErrorLike(err), locale)` |
|
||||||
|
| List indicator | `FileList.tsx`, `FileGrid.tsx`, `TypeTile.tsx` | Small share icon when `entry.shareTypes.length > 0` or permissions contain `S`; click opens the dialog; add an `aria-label`. After any share change re-list the folder |
|
||||||
|
| Views | `app/(portal)/modules/nextcloud-files/page.tsx` (`TabId = 'files' | 'settings'`, TabBar rendered only `canManage`) | Add `sharedByMe`, `sharedWithMe` to `TabId`; show the TabBar for every connected user (today it only renders for managers). New components `SharesView.tsx` (list, actions). "Mit mir geteilt" item click: switch to tab `files` and navigate `FileBrowser` to `file_target` (folder) or its parent (file) with the file preselected; needs a `initialPath` prop on `FileBrowser` |
|
||||||
|
| Module version | `nextcloud-files.changelog.ts` (only entry `1.0.0`, date `2026-10-08`), seed uses `latestVersion(...)` | Last tag `v1.10.1` (2026-10-06), `CHANGELOG.md` lists "Neues Modul Dateien" under "Unveröffentlicht", so the 1.0.0 entry is unreleased: per `docs/anleitung-entwicklung.md` ("Höchstens ein Sprung je Modul zwischen zwei Tessera-Freigaben") extend the 1.0.0 entry with `new` items (de+en, "Sie", real umlauts). `module-changelog.spec.ts:202` pins exactly one entry `1.0.0` / `2026-10-08`: leave as is. If the planner prefers 1.1.0, that test must change too |
|
||||||
|
| Docs | `docs/anleitung-anwender.md` section "Dateien (Nextcloud)" (menu sentence lists "Öffnen, Herunterladen, Umbenennen, Verschieben, „In Nextcloud öffnen“ und Löschen"), `docs/anleitung-administration.md` "Dateien: Nextcloud anbinden", `docs/anleitung-betrieb.md` "Dateien (Nextcloud)" + troubleshooting table, `docs/anleitung-entwicklung.md` (share layer, route list), root `CHANGELOG.md` "Unveröffentlicht" | Admin doc: the sharing rules (password, expiry, link upload) are set in Nextcloud Administration > Sharing and Tessera mirrors them. Betrieb: 20/10-min Nextcloud limit, Tessera limit 15, link URL host comes from Nextcloud's own address settings (`overwritehost`/trusted domain) |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser (ShareDialog / SharesView / list icon)
|
||||||
|
| JSON, cookie auth
|
||||||
|
v
|
||||||
|
Controller (UseModule, tenant+user from token) -> DTO validation (class-validator, strict date/permission enum)
|
||||||
|
v
|
||||||
|
NextcloudFilesSharesService
|
||||||
|
|-- shareCreateLimiter (per user) -> 429 tooManyShares (no NC call)
|
||||||
|
|-- getSession(tenantId,userId) -> NcSession (credentialKey, Basic auth)
|
||||||
|
|-- pre-validate vs capabilities (password required, expiry window, link allowed, drop allowed)
|
||||||
|
v
|
||||||
|
nextcloud-shares.ts ocsShareRequest -> ncRequest (gate, no redirects, no cookies, 4 concurrent/key)
|
||||||
|
v
|
||||||
|
Nextcloud /ocs/v2.php/apps/files_sharing/api/v1/... (+ /cloud/capabilities)
|
||||||
|
v
|
||||||
|
parsers -> NcShare / NcSharee / NcSharePolicy -> JSON to browser (no password, no raw object)
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended new files: `nextcloud-shares.ts`, `nextcloud-shares.spec.ts`, `nextcloud-files-shares.service.ts` (+ `.spec.ts`), `dto/nextcloud-files-shares.dto.ts`; web `lib/nextcloud-files-api.ts` additions (+ test), `components/ShareDialog.tsx`, `SharesView.tsx`, `ShareIndicator`, `components/nextcloud-files/share-policy.ts` (pure functions: permission choice -> bitmask, min/max date, password required, with unit tests).
|
||||||
|
|
||||||
|
Patterns to follow:
|
||||||
|
- **Policy DTO to browser** `NcSharePolicy`: `{enabled, linksEnabled, linkPasswordRequired, linkExpiry:{enabled,days,enforced}, internalExpiry:{enabled,days,enforced}, uploadAllowed, dropAllowed, groupsEnabled, minSearchLength, passwordMinLength|null}`. The API re-checks the same rules on write (do not trust the browser), but only for rules it can know from capabilities.
|
||||||
|
- **Permission DTO**: accept `access: 'view' | 'edit' | 'upload'` plus the entry type from the server (the API itself looks up the type via `PROPFIND Depth 0` or trusts nothing: simplest is `GET shares?path=` is not enough; use `dav.list`-style `parsePropfindSelf` that exists) and compute the bitmask server-side. Never accept raw bitmasks from the browser.
|
||||||
|
- **Update**: compute the diff in the browser, send only changed fields; empty string semantics: `password:""` removes, `expireDate:""` removes, `label`/`note` `""` clear.
|
||||||
|
- **Display of the link**: show `share.url` only if it parses as http/https; render in a read-only input + "Link kopieren" via `navigator.clipboard`; Tessera never requests it.
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't build | Use instead | Why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Password rules | A Tessera password policy | NC capabilities hint + show NC's 400 message | Policy, HIBP and common-password lists live in NC |
|
||||||
|
| Expiry rules | Own default/maximum | `public.expire_date{,_internal}.{enabled,days,enforced}` | Admin-configurable per server |
|
||||||
|
| User/group lookup | Own directory query (LDAP) | Sharee API | NC applies its enumeration restrictions (`shareapi_restrict_user_enumeration_*`) |
|
||||||
|
| Random password | A custom generator | NC `password_policy/api/v1/generate`, fallback `crypto.getRandomValues` 20 chars | Server-compliant. (10-char output from NC today; fine) |
|
||||||
|
| Link URL | Building `/s/<token>` yourself | `url` field from the share | NC knows its public host |
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
1. **Origin-wide pause from one user's create burst.** NC `UserRateLimit(20/600 s)` answers 429 without `Retry-After`; `ncRequest` calls `gate.pause(origin)` for 15 min for all users. Avoid with the Tessera limiter (15/10 min) and keep creates sequential in the UI (no "share with 30 people" parallel fan-out; batch recipients one POST at a time).
|
||||||
|
2. **`ocsRequest` swallows error bodies and maps 403 to `app-password-given`.** Using it would show "use browser login" for every policy error and lose NC's message.
|
||||||
|
3. **PUT hides the reason** (`Failed to update share.`): pre-validate; map PUT 400 without password/expiry fields to generic `shareRejected`, with them to the password/expiry texts.
|
||||||
|
4. **Lenient `expireDate` parsing and timezone.** Validate strictly; the server compares in its own timezone, so near midnight the browser's "today" may be a day off: let the server message through (`shareExpiryInvalid` with NC message) instead of failing the UX; do not set `min` to tomorrow.
|
||||||
|
5. **Re-POST is not an update** and re-sends mail. Disable already-shared recipients in the picker, change via PUT.
|
||||||
|
6. **Capabilities are per user and conditional.** Missing `public.password` / `expire_date.days` keys mean "no policy", not an error. Do not cache across users; cache per `credentialKey` for at most ~60 s or fetch on dialog open.
|
||||||
|
7. **401 handling.** A 401 on any share call marks the credential dead via `ncRequest` (`credential-dead`) and `mapNcFailure` turns it into `connectionExpired` + `markExpired`; the UI already returns to the connect screen (`onExpired`). Don't catch it locally.
|
||||||
|
8. **Received shares can't be edited.** `PUT` by a recipient is 403 (`canEditShare`); only show Ändern for shares where `can_edit` is true and `uid_owner`/`uid_file_owner` is the caller. "Mit mir geteilt" actions: open, accept (if pending), leave (DELETE).
|
||||||
|
9. **Link `url` host** is built from NC's request context. If the admin saved an internal address as the Tessera NC address, links show the internal host. Document; do not rewrite.
|
||||||
|
10. **File vs folder permissions.** "Nur hochladen" and `publicUpload` on a file give 400; "Bearbeiten" on a file is 3 not 15. Decide by `entry.type`.
|
||||||
|
11. **Hide others' types.** `GET shares` returns email/federated/Talk/circle shares too; filter to types 0/1/3 (show a count or note "weitere Freigaben nur in Nextcloud") so unknown `share_with` shapes never reach the UI.
|
||||||
|
12. **Folders containing received shares can't be shared** (403 `You cannot share a folder that contains other shares`): map to a German text, don't treat as outage.
|
||||||
|
13. **Existing front-end rules:** statics-before-params route order; `FileBrowser` swallows key events inside dialogs only via `Dialog`/`EntryMenu` (`stopPropagation`): the dialog must be built on `Dialog` or typing in the sharee search triggers Entf/F2 shortcuts.
|
||||||
|
|
||||||
|
## Security Domain (ASVS L1)
|
||||||
|
|
||||||
|
| ASVS | Applies | Control |
|
||||||
|
|---|---|---|
|
||||||
|
| V2/V3 | no new auth | Existing cookie auth + `@UseModule` |
|
||||||
|
| V4 Access control | yes | `tenantId`/`userId` from `req` only; every call via `getSession(tenantId,userId)`; recipient shares are not editable by the API (NC enforces, API doesn't pre-filter by assumption) |
|
||||||
|
| V5 Input validation | yes | class-validator DTOs: `path` through `parseUserPath`; `shareType` enum {0,1,3} (link only if capability); `shareWith` `@MaxLength(255)` + no control chars; `access` enum; `expireDate` regex + real date; `password` `@MaxLength(256)`; `label` <= 255; `note` <= 500; id `^\d{1,20}$` |
|
||||||
|
| V6 Crypto | no new | Don't hash/echo passwords; not stored in Tessera |
|
||||||
|
| V7 Logging | yes | Never log request bodies, passwords, `token`/`url`; log only operation + status |
|
||||||
|
| SSRF | yes | Never fetch `url`, `api.generate` or any URL from NC answers; only fixed `/ocs/v2.php/` paths |
|
||||||
|
| Data exposure | yes | Public link URL is shown only to the user who owns the share; parsers drop unknown fields; password field from NC is the literal `redacted` and must not be forwarded as if it were a value |
|
||||||
|
|
||||||
|
Threats: STRIDE Tampering (forged permission bitmask: mitigated by server-side mapping), Information disclosure (link URL/token in logs or error extra), DoS (create bursts: limiter, list caps), Elevation (sharing others' files: NC enforces; API only passes the caller's own session).
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| Framework | vitest 3.2.6 (apps/api), 4.1.9 (apps/web); Biome |
|
||||||
|
| Quick run | `pnpm --filter api exec vitest run src/nextcloud-files` / `pnpm --filter web exec vitest run src/lib/nextcloud-files-api.test.ts "src/app/(portal)/modules/nextcloud-files"` (verify filter names) |
|
||||||
|
| Full suite | `pnpm --filter api test`, `pnpm --filter web test`, `tsc`, `biome check` |
|
||||||
|
| Fake transport | the `setup()` helper pattern of `nextcloud-files.service.spec.ts` (`NextcloudTransport` returning `Readable.from([...])`, assert `calls[i].url/method/body/headers`); `NextcloudCallGate` real instance |
|
||||||
|
|
||||||
|
| Behaviour | Type | File |
|
||||||
|
|---|---|---|
|
||||||
|
| URL/body built exactly (`path` encoded, `shareType[0]` keys, JSON body, `expireDate:""`) | unit | `nextcloud-shares.spec.ts` (Wave 0) |
|
||||||
|
| Parsers: user share live JSON above, link share, array vs object, `redacted`, unknown types filtered | unit | same |
|
||||||
|
| Error mapping matrix (status x sent fields) incl. 403 not leaking as 403, 429 pauses gate, 401 marks expired | unit | `nextcloud-files-shares.service.spec.ts` (Wave 0) |
|
||||||
|
| Policy derivation incl. missing keys, links disabled, enforced days | unit | same / `share-policy.test.ts` |
|
||||||
|
| Create limiter 15/10 min | unit | login-guard spec extension |
|
||||||
|
| Route order, DTO bounds | unit | controller spec extension |
|
||||||
|
| `parsePropfind` returns `shareTypes` | unit | `nextcloud-propfind.spec.ts` |
|
||||||
|
| Dialog/List/Views | component | `FileBrowser.test.tsx` pattern, new `ShareDialog.test.tsx` |
|
||||||
|
| Live: create/list/update/delete user, group, link; password-enforced policy; expiry enforced | e2e shell | new `e2e/e2e-shares.sh` next to this task, `source` the existing `261008-mzu/e2e/e2e-lib.sh` (`e2e_login`, `e2e_activate`, `e2e_set_address`, `e2e_connect_anna`, `NC_OCC`). Set policy with `NC_OCC config:app:set core shareapi_enforce_links_password --value=yes` (and `config:app:set core shareapi_default_expire_date --value=yes`, `shareapi_enforce_expire_date`, `shareapi_expire_after_n_days`) and RESET it at the end with `trap`. Key names are `[ASSUMED]` from `Manager.php` getAppValue calls (`shareapi_default_internal_expire_date`, `shareapi_expire_after_n_days`, `shareapi_internal_expire_after_n_days` seen in source; the link-password and link-expiry flags are app-config lexicon keys `SHARE_LINK_PASSWORD_ENFORCED`/`SHARE_LINK_EXPIRE_DATE_*`, exact string unverified): check with `occ config:list core` after toggling in the admin UI |
|
||||||
|
Wave 0 e2e cleanup: delete every share created (`DELETE`), leave `GET shares` empty for `anna`/`zoe`.
|
||||||
|
|
||||||
|
## Environment Availability
|
||||||
|
|
||||||
|
| Dependency | Available | Version | Note |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `tessera-nc-test` | yes | Nextcloud 34.0.4, PHP 8.5, `password_policy`, `sharebymail`, `federation`, `circles` enabled | `http://172.17.0.1:18080`, users `anna / User1-Pass-12345`, `zoe` (2FA), `admin / Admin-Pass-12345`; groups `twofa`, `admin`. Brute-force whitelist set for the Docker range |
|
||||||
|
| Sharing policy in test NC | defaults | link password not enforced, no default expiry | Enforcement tests must toggle via `occ` (not yet verified which keys, see above) |
|
||||||
|
| `trusted domains` | `172.17.0.1` | | `url`/generate URLs echo the request host |
|
||||||
|
|
||||||
|
## Test-state note (honest disclosure)
|
||||||
|
I was told to stay read-only. One probe (`POST` user share with `expireDate:"31.12.2026x"`, meant to demonstrate a rejected date) was accepted by Nextcloud and created share id 1 (`/Projekte` to `zoe`). I deleted it (`DELETE shares/1`, then `GET shares` for `anna` returned `data: []`). Nothing else was changed: no `occ config` writes. Therefore the POST/PUT response shapes for LINK shares and the enforced-policy errors come from the NC source, not from a live round trip; the executor's e2e script must record them.
|
||||||
|
|
||||||
|
## Assumptions Log
|
||||||
|
|
||||||
|
| # | Claim | Section | Risk if wrong |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A1 | `S` in `oc:permissions` marks a received share, `R` = reshare allowed (letters other than the observed `RGDNVCK`) | API facts | Wrong indicator / wrongly hidden Teilen action; verify with a live received share in e2e |
|
||||||
|
| A2 | Password-policy `minLength` from capabilities applies to the sharing context | Capabilities | Only a hint in the form; server decides |
|
||||||
|
| A3 | Exact `occ` keys for enforcing link password / link expiry in the e2e script | Validation | E2E setup fails; fix by checking `occ config:list core` after toggling via UI |
|
||||||
|
| A4 | Email shares (type 4) use ids that fail `^\d{1,20}$`, so id validation would block removing them | Security / Pitfall 11 | Only matters if the planner decides to list type 4; recommended not to |
|
||||||
|
| A5 | NC 429 from `UserRateLimit` carries no `Retry-After` | Pitfall 1 | If it has one, gate pause is shorter but still origin-wide; the Tessera limiter is still right |
|
||||||
|
| A6 | `can_edit`/`uid_owner` suffice to decide whether a share is changeable | Pitfall 8 | Extra 403 from NC, mapped to `shareRejected` anyway |
|
||||||
|
| A7 | Reshare bit 16 can be left out without side effects for user/group shares | Permissions | Recipients cannot reshare; matches the "simple choice" decision |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Module version: extend 1.0.0 or bump to 1.1.0?** Recommendation: extend the unreleased 1.0.0 (documented rule + test pin). Memory note "Version im Seed hoch" is satisfied because the seed reads the changelog.
|
||||||
|
2. **List email/federated shares read-only?** Recommendation: no, filter to 0/1/3 and mention the remainder; keeps the id/shape surface small.
|
||||||
|
3. **Password field UX for links:** required (policy) vs optional with "Passwort setzen" toggle. Recommendation: required input + "Erzeugen" button when enforced; optional toggle otherwise; never prefill.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH)
|
||||||
|
- Nextcloud 34.0.4 server code inside `tessera-nc-test`: `/var/www/html/apps/files_sharing/lib/Controller/ShareAPIController.php` (createShare, updateShare, getShares, formatShare, parseDate, pendingShares), `.../ShareesAPIController.php`, `.../lib/Capabilities.php`, `.../appinfo/routes.php`, `/var/www/html/lib/private/Share20/Manager.php` (verifyPassword, validateExpirationDate{Internal,Link}, createShare), `/var/www/html/apps/password_policy/lib/PasswordValidator.php`
|
||||||
|
- Live probes against `http://172.17.0.1:18080` (capabilities, sharees, shares lists, error cases, PROPFIND share-types)
|
||||||
|
- Repo: `apps/api/src/nextcloud-files/{nextcloud-http,nextcloud-auth-client,nextcloud-upstream,nextcloud-call-gate,nextcloud-login-guard,nextcloud-propfind,nextcloud-dav,nextcloud-files.service,nextcloud-files.controller,nextcloud-files.types,nextcloud-files.changelog}.ts`, web `FileBrowser.tsx`, `EntryMenu.tsx`, `Dialog.tsx`, `error-text.ts`, `page.tsx`, `lib/nextcloud-files-api.ts`, `docs/anleitung-entwicklung.md` (changelog rules), `261008-mzu/e2e/{e2e-lib,nc-test-setup}.sh`
|
||||||
|
|
||||||
|
### Not consulted this session
|
||||||
|
Online Nextcloud developer manual (the running server's source was the stronger, version-exact source).
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
**Confidence:** Standard stack HIGH (no new deps); API facts HIGH for reads and errors, MEDIUM for link-share write shapes (source only); architecture HIGH; pitfalls HIGH.
|
||||||
|
**Research date:** 2026-10-09. **Valid until:** until the Nextcloud major used in production differs from 34 (re-run the capabilities and error probes against the production version before release; the production server's sharing policy was not available).
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
phase: quick-261009-dkv
|
||||||
|
reviewed: 2026-10-09
|
||||||
|
depth: standard
|
||||||
|
findings:
|
||||||
|
critical: 1
|
||||||
|
warning: 4
|
||||||
|
info: 6
|
||||||
|
total: 11
|
||||||
|
status: issues_found
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review: quick-261009-dkv (Dateien Etappe 2a, Teilen)
|
||||||
|
|
||||||
|
Gepruefte Commits: 207d37b, bf893af, fe429db, c3adf85, 0585fd1.
|
||||||
|
|
||||||
|
## Critical
|
||||||
|
|
||||||
|
### CR-01: Pfade und Kennungen werden durch `cleanText` veraendert
|
||||||
|
|
||||||
|
`apps/api/src/nextcloud-files/nextcloud-shares.ts` (~285-286, 310, 359): `parseShare` baut `path`/`target` (sowie `shareWith` und die Empfaenger-`id` in `shareeList`) mit `cleanText`. Die Funktion faltet Leerraum, schneidet ab und ersetzt Steuerzeichen. Diese Werte sind aber Bezeichner, die an die Nextcloud zurueckgehen (SharesView, shareTarget, ShareDialog listSharesForPath/createShare, onOpen/focusAfterLoad). `/Kunden/Mueller GmbH` (zwei Leerzeichen) wird zu `/Kunden/Mueller GmbH`; die Operation trifft dann einen anderen Eintrag oder keinen.
|
||||||
|
|
||||||
|
Fix: Kennungen und Pfade woertlich behalten (bei Steuerzeichen oder Ueberlaenge ablehnen bzw. leer), nur Anzeigetexte (name, label, *Name, message) bereinigen; den Namen aus dem woertlichen letzten Segment ableiten. Spec-Faelle mit doppeltem und nachgestelltem Leerzeichen in Pfaden und Kennungen.
|
||||||
|
|
||||||
|
## Warnings
|
||||||
|
|
||||||
|
### WR-01: Link-Zugriff wird beim Speichern stillschweigend auf "Ansehen" gesenkt
|
||||||
|
|
||||||
|
`LinkShareForm.tsx:58-63`, `share-policy.ts linkUpdateDiff`, Dienst ~422: Ist die bestehende Link-Berechtigung (Bearbeiten, Hochladen, eigene) nicht in `accessOptions`, faellt `initialAccess` auf `view` zurueck, und jedes Speichern (auch nur Bezeichnung oder Ablauf) senkt die Berechtigung. Fix: Zustand `null`, wenn der aktuelle Wert nicht angeboten wird; Zugriff nur senden, wenn der Benutzer gewaehlt hat; aktuellen Wert als deaktivierte gewaehlte Option zeigen ("Aktuell: ..." / "Eigene Berechtigung"). Tests.
|
||||||
|
|
||||||
|
### WR-02: Biome `organizeImports` schlaegt fehl
|
||||||
|
|
||||||
|
Betroffen: `nextcloud-files-shares.service.ts`, `nextcloud-files.controller.ts`, `nextcloud-files.module.ts`, `nextcloud-shares.spec.ts`, `components/FileList.tsx`, `components/ShareDialog.tsx`. Fix: `biome check --write` auf genau diesen Dateien; als Tor `biome check` (nicht nur lint) auf allen geaenderten Dateien.
|
||||||
|
|
||||||
|
### WR-03: Zaehler fuer neue Freigaben im Arbeitsspeicher, Vorpruefungen unbegrenzt
|
||||||
|
|
||||||
|
`nextcloud-login-guard.ts:191-200`, Dienst 346-373: Grenze auf 10 je 10 Minuten senken; ehrlich kommentieren, dass die Zusicherung je Prozess ohne Neustart gilt und Freigaben, die direkt in der Nextcloud entstehen, ebenfalls gegen deren 20 zaehlen; leere Map-Eintraege entfernen (shareCreates und flowStarts). Wiederholte abgelehnte Versuche (z. B. shareAlreadyExists) muessen begrenzt werden (billiger Versuchszaehler je Benutzer oder Pruefung vor den Nextcloud-Vorabfragen), damit sie die Nextcloud nicht belasten. Dokumente mit "15" anpassen.
|
||||||
|
|
||||||
|
### WR-04: `refreshAfterExpiry` pro Render neu, ueberlappende Ladevorgaenge
|
||||||
|
|
||||||
|
`page.tsx:138-139`, `SharesView.tsx:75-116`: Die Funktion wird je Render neu erzeugt, der Lade-Effekt der SharesView laeuft erneut (Flackern); ueberlappende Ladevorgaenge koennen in falscher Reihenfolge angewendet werden. Fix: stabiler Rueckruf (useCallback oder Ref) und Anforderungsnummer im Laden. Tests.
|
||||||
|
|
||||||
|
## Info
|
||||||
|
|
||||||
|
### IN-01: Ablaufgrenzen aus Browserdatum statt Serverdatum
|
||||||
|
|
||||||
|
`share-policy.ts todayLocal/expiryRule`: Die Antwort der Richtlinie der API soll das Serverdatum (JJJJ-MM-TT) enthalten, das Web nutzt es fuer Vorgabe und Hoechstwert.
|
||||||
|
|
||||||
|
### IN-02: Fehler ohne Abschnittszuordnung
|
||||||
|
|
||||||
|
`ShareDialog.tsx:105-112, 370-373`: `fail()` setzt `errorSection` nicht (Suchfehler erscheint unter Link); die Suche loescht per onChange alle Fehler. Fix: abschnittsbezogen setzen und loeschen.
|
||||||
|
|
||||||
|
### IN-03: `accessOf` zu grosszuegig
|
||||||
|
|
||||||
|
`nextcloud-shares.ts:242-248`: Bearbeiten nur bei (mask&15)==15 (Ordner) bzw. ==3 (Datei); sonst eigene Berechtigung (nur Lesen = Ansehen, nur Anlegen am Ordner = Hochladen). Tests.
|
||||||
|
|
||||||
|
### IN-04: `generatePassword` Obergrenze
|
||||||
|
|
||||||
|
`share-policy.ts`: Obergrenze max(64, minLength), hoechstens 256.
|
||||||
|
|
||||||
|
### IN-05: Kleinere Punkte
|
||||||
|
|
||||||
|
- Knopf "Aendern" am Link nur bei `share.canEdit`.
|
||||||
|
- `copiedId` nach etwa 2 s zuruecksetzen.
|
||||||
|
- Weiterfreigaben (Benutzer ist Initiator, `uid_owner` ist ein anderer): richtig einordnen, damit sie den Dialog nicht mit einem Pfad aus fremdem Baum oeffnen.
|
||||||
|
|
||||||
|
### IN-06: Live-Test fuer Ablehnen
|
||||||
|
|
||||||
|
`e2e-shares.sh`, Abschnitt `received`: offene Freigabe ueber Tessera ablehnen und pruefen, dass sie weg ist; messen, welche Nextcloud-Route fuer offene Freigaben gilt, den Dienst korrigieren, falls `DELETE shares/{id}` falsch ist.
|
||||||
|
|
||||||
|
## Zusaetzlich beauftragt
|
||||||
|
|
||||||
|
- EXTRA-1 (Etappe 1): In einem Ordner ohne Schreibrecht bietet die Dateiansicht "Neuer Ordner", "Hochladen" (und Ablegen) an. Eigene Rechte des geoeffneten Ordners auswerten; Umbenennen/Verschieben/Loeschen je Eintrag nach den Buchstaben.
|
||||||
|
- EXTRA-2: Anleitung sagt "mindestens zwei Buchstaben", der Code nutzt `max(1, minSearchLength)`. Anleitung angleichen.
|
||||||
|
|
||||||
|
## Fix status
|
||||||
|
|
||||||
|
Behoben in den Commits 78f6cf3 (Schnittstelle) und d487a00 (Oberfläche, Anleitungen).
|
||||||
|
|
||||||
|
| Befund | Stand | Wie |
|
||||||
|
|--------|-------|-----|
|
||||||
|
| CR-01 | fixed | Neue Hilfe `verbatimId` (`nextcloud-shares.ts`): Pfad, Ziel, `share_with` und Empfängerkennung der Suche bleiben wörtlich; Steuerzeichen oder Überlänge ergeben „unbrauchbar“ (Freigabe ohne brauchbaren Pfad zählt als versteckt). Nur Anzeigetexte laufen durch `cleanText`. `name` ist das wörtliche letzte Segment; `SharesView` nennt dem Ordner „Im Ordner zeigen/Öffnen“ den Namen aus dem Pfad. Spec-Fälle mit doppeltem und nachgestelltem Leerzeichen in Pfad, Ziel, Name, `share_with` und Suche; Live-Test prüft einen Pfad mit zwei Leerzeichen gegen die echte Nextcloud. |
|
||||||
|
| WR-01 | fixed | `LinkShareForm`: Zugriffsauswahl ist `null`, wenn der aktuelle Wert nicht angeboten wird; die Zeile „Aktuell: …“ steht gewählt und gesperrt da; `linkUpdateDiff` sendet `access` nur bei ausdrücklicher Wahl. Tests in `share-policy.test.ts`, `ShareDialog.test.tsx`, API-Spec (nur Bezeichnung ändert die Berechtigung nicht); im Browser mit eigener Berechtigung (21) bestätigt. |
|
||||||
|
| WR-02 | fixed | `biome check --write` auf genau den Dateien der Aufgabe; Tor ist `biome check` auf allen 31 geänderten Dateien: 0 Fehler, keine Warnungen. |
|
||||||
|
| WR-03 | fixed | Grenze 10 je 10 Minuten, ehrlicher Kommentar (je Prozess ohne Neustart, Freigaben direkt in der Nextcloud zählen gegen deren 20); neuer Versuchszähler `checkShareAttempt` (40 je 10 Minuten, auch abgelehnte) vor den Abfragen an die Nextcloud; ein Wartungslauf je Minute entfernt leere Einträge aus `flowStarts`, `shareCreates`, `shareAttempts`. Dokumente von 15 auf 10 angepasst (CHANGELOG, Anwender-, Administrations-, Betriebs-, Entwicklungshandbuch, Live-Test). |
|
||||||
|
| WR-04 | fixed | `page.tsx`: `refreshAfterExpiry` per `useCallback`; `SharesView`: Rückruf über Verweis, Ladenummer `loadSeq` (nur das jüngste Ergebnis gilt, Verlassen entwertet laufende). Tests: Neuzeichnen lädt nicht neu, aktueller Rückruf wird genutzt, überlappende Vorgänge, spätes Ergebnis nach dem Verlassen, Seitentest. |
|
||||||
|
| IN-01 | fixed | `parseSharePolicy(data, today)`: Antwort enthält `today` (Serverdatum, `serverDate`); Web nutzt `policyToday` für Vorgabe, Höchst- und Mindestdatum (Rückfall auf die Browseruhr nur ohne Wert). |
|
||||||
|
| IN-02 | fixed | `fail(err, section)`; Suchfehler gehören zu „Personen und Gruppen“; Tippen löscht nur Fehler dieses Abschnitts. Tests und Browserbild. |
|
||||||
|
| IN-03 | fixed | `accessOf(permissions, itemType)`: Bearbeiten nur bei Maske 15 (Ordner) oder 3 (Datei), Nur hochladen nur bei Maske 4 an Ordnern, Ansehen bei 1, alles andere „Eigene Berechtigung“; Tabelle mit 19 Fällen. |
|
||||||
|
| IN-04 | fixed | `generatePassword`: Länge `min(256, max(20, Mindestlänge))`, nie unter der Mindestlänge der Nextcloud. |
|
||||||
|
| IN-05 | fixed | „Ändern“ nur bei `canEdit`; „Kopiert“ geht nach 2 s zurück (auch beim Passwort-Hinweis); Weitergaben: gemessen ist `uid_owner` der Freigebende, `uid_file_owner` der Dateieigentümer, `path` im Baum des Aufrufers. Die Weitergabe gilt als eigene Freigabe in „Von mir geteilt“ mit eigenem Pfad (`target` = `path`) und dem Hinweis „Von Ihnen weitergegeben, Eigentümer: …“ (`fileOwnerName`); für die Dateieigentümerin erscheint sie als Freigabe des Freigebenden. Die Befundannahme „uid_owner ist der Dateieigentümer“ stimmt für die Nextcloud nicht. |
|
||||||
|
| IN-06 | fixed | Live-Test: offene Freigabe ablehnen über Tessera, Ergebnis geprüft. Gemessen: `DELETE shares/{id}` ist richtig (200, danach weder offen noch angenommen, bei Ben ist die Freigabe ganz weg); `DELETE shares/pending/{id}` ergibt 405. Dienst unverändert. |
|
||||||
|
| EXTRA-1 | fixed | Listenantwort trägt `permissions` des Ordners selbst; `entry-permissions.ts` (C, K, N, V, D); Dateiansicht blendet Neuer Ordner, Hochladen, Ablegen (auch auf Ordnerzeilen und per Tastatur), Umbenennen, Verschieben und Löschen aus (Menü, Auswahlleiste, Entf/F2), zeigt „Nur ansehen“; unbekannt ist nie verboten. Messwerte: eigener Ordner `RGDNVCK`, Nur-Ansehen-Ordner `SGDN`, Inhalt `SG`. Tests, Live-Test, Browserbild. |
|
||||||
|
| EXTRA-2 | fixed | Anwenderhandbuch: „je nach Einstellung Ihrer Nextcloud ab einem oder zwei Zeichen“. |
|
||||||
+247
@@ -0,0 +1,247 @@
|
|||||||
|
---
|
||||||
|
phase: quick-261009-dkv
|
||||||
|
plan: 01
|
||||||
|
status: complete
|
||||||
|
completed_tasks: [1, 2, 3]
|
||||||
|
remaining_tasks: []
|
||||||
|
task1_commits: [207d37b, bf893af]
|
||||||
|
task2_commits: [fe429db, c3adf85]
|
||||||
|
task3_commits: [0585fd1]
|
||||||
|
commits: 5
|
||||||
|
plan_head_before: 43a6a83
|
||||||
|
plan_head_after: 0585fd1
|
||||||
|
module_version: 1.0.0 (unchanged, per D-05 as corrected)
|
||||||
|
pushed: false
|
||||||
|
deployed: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick 261009-dkv: Dateien Etappe 2a (Teilen) - Fortschrittsnotizen
|
||||||
|
|
||||||
|
Alle drei Aufgaben sind erledigt (fünf Commits auf main, nichts gepusht, nichts ausgeliefert). Die Abschnitte zu Aufgabe 1 und 2 stammen aus deren Läufen, Aufgabe 3 steht am Ende.
|
||||||
|
|
||||||
|
## Aufgabe 1: Durchstich Personen und Gruppen (fertig, zwei Commits, nicht gepusht)
|
||||||
|
|
||||||
|
| Commit | Inhalt |
|
||||||
|
|--------|--------|
|
||||||
|
| 207d37b | Schnittstelle: Freigaben-Schicht, Dienst, DTOs, Routen, shareTypes im Listeneintrag, Begrenzung, Fehlercodes, Live-Test `e2e-shares.sh` (Abschnitt people) |
|
||||||
|
| bf893af | Oberfläche: Teilen-Dialog, Kennzeichen, Menüeintrag, Web-Client, Hilfen, Meldungen de/en; kleine API-Ergänzung `scope: 'groups'` am Fehler `sharingDisabled` |
|
||||||
|
|
||||||
|
### Gemessene Tore (auf dem neu gebauten Stack, `docker compose up -d --build api web`)
|
||||||
|
|
||||||
|
| Tor | Ergebnis |
|
||||||
|
|-----|----------|
|
||||||
|
| api vitest `src/nextcloud-files module-manage-handlers` | 17 Dateien, 570 Tests grün (neu: nextcloud-shares 39, Dienst 34, Guard +4, Propfind +1, Controller +4) |
|
||||||
|
| web vitest `modules/nextcloud-files components/nextcloud-files src/lib/nextcloud-files src/messages` | 16 Dateien, 214 Tests grün (neu: ShareDialog 14, share-policy 7, FileBrowser +6, Client +6) |
|
||||||
|
| tsc api / web | sauber |
|
||||||
|
| biome lint (api nextcloud-files, web Modul, Komponenten, Client) | 0 Fehler; 4 alte Warnungen in `nextcloud-files-account.service.spec.ts` (nicht von dieser Aufgabe) |
|
||||||
|
| Schlüsselgleichheit de/en `nextcloudFiles` und Wortsperre (mandant, tenant, lizenz, licens, Pfeil, Mittelpunkt) | ok |
|
||||||
|
| `nc-test-setup.sh` | `nc test ready` |
|
||||||
|
| `e2e-shares.sh people` | `e2e shares people ok` (nach dem Neubau mit dem Endstand zweimal grün) |
|
||||||
|
| Browser-Rauchtest (playwright-core, hell) | Zeilenmenü, Teilen, Suche `ben`, Auswahl legt Freigabe an, Kennzeichen erscheint, Entfernen: ok; Bilder lagen im Arbeitsverzeichnis der Sitzung (keine Beweisbilder, die kommen in Aufgabe 3) |
|
||||||
|
|
||||||
|
Live bestätigt gegen Nextcloud 34.0.4: Empfängersuche findet die Person `ben` über den Teilbegriff `ben` und die Gruppe über `tessera`; Anlegen als Bearbeiten ergibt 201 mit permissions 15, Ändern auf Ansehen ergibt 1 (auch in Bens eigener Liste); doppeltes Anlegen ergibt 409 `shareAlreadyExists` ohne zweite Freigabe; Hochladen für eine Person ergibt 400 `shareAccessInvalid`; Gruppenfreigabe einer Datei 201; `GET files` trägt `shareTypes` (0 am Ordner, 1 an der Datei, leer am nicht geteilten Ordner); Entfernen 200; unbekannte oder ungültige Kennung 404 `shareNotFound`. Kein 401/403 in allen Aufrufen des Laufs.
|
||||||
|
|
||||||
|
### Abweichungen
|
||||||
|
|
||||||
|
1. **[Rule 3] Zusätzliche Datei im Commit:** `nextcloud-files-page.test.tsx` (nicht in der Dateiliste von Aufgabe 1) bekam `shareTypes: []` in der Eintragsattrappe, sonst bricht das Kennzeichen (`entry.shareTypes.length`) im Seitentest. Aufgabe 2 ändert diese Datei ohnehin weiter.
|
||||||
|
2. **[Rule 2] `scope: 'groups'` am Fehler `sharingDisabled`:** Der Web-Text für die Gruppenvariante braucht ein Merkmal statt eines Textvergleichs; die API hängt `scope: 'groups'` an (Dienst und Spec angepasst, Text des Servers unverändert). Der Web-Code mappt `sharingDisabled` mit `scope === 'groups'` auf `codes.sharingDisabledGroups`.
|
||||||
|
3. **Skriptfehler im Live-Test, behoben:** (a) `ben_perm` las eine veraltete Antwort (holt jetzt selbst neu), (b) `occ group:list | grep -q` scheitert unter `pipefail` durch SIGPIPE (jetzt Ausgabe in Variable, dann `case`), (c) die gemerkten HTTP-Status gingen in der Unterschale von `$(...)` verloren (jetzt Datei `$SEEN`).
|
||||||
|
4. Der Formatierer (`biome format --write` auf ein ganzes Verzeichnis) fasste die ungeformten Dateien `nextcloud-server-info.ts` und `.spec.ts` an; beide wurden per `git checkout -- <datei>` zurückgenommen. Künftig nur die eigenen Dateien formatieren.
|
||||||
|
|
||||||
|
### Bekannte Lücken (absichtlich, für Aufgabe 2)
|
||||||
|
|
||||||
|
- `update()` im Dienst lehnt Links vorerst mit `shareAccessInvalid` ab (`if (current.kind === 'link')`); Link-Ändern, Link-Anlegen, `mine`, `received`, `accept` fehlen.
|
||||||
|
- `mapShareFailure` ist schon vollständig (auch die Passwort- und Ablauf-Zweige für create/update), aber die Passwort-/Ablauf-Zweige haben noch keinen Test (erst über Link-Aufrufe erreichbar).
|
||||||
|
- Der Dialog zeigt in der Personenliste nur `kind !== 'link'`; Links tauchen im Dialog erst mit dem Link-Abschnitt auf.
|
||||||
|
- Der Buchstabe `S` für eingehende Freigaben ist noch nicht live gemessen (Annahme A1); Kennzeichen und Spec nutzen ihn, Aufgabe 2 misst ihn im Abschnitt received und fällt sonst auf `nc:mount-type` zurück.
|
||||||
|
- Die gemessenen Nextcloud-Status für Ablauf jenseits des Höchstwerts beim Ändern und schwaches Passwort beim Ändern (Auftrag der Abschlusszusammenfassung) stehen noch aus (Aufgabe 2).
|
||||||
|
|
||||||
|
## Hinweise für Aufgabe 2
|
||||||
|
|
||||||
|
**Bausteine, die schon da sind**
|
||||||
|
- API `nextcloud-shares.ts`: `ocsShareRequest`, `parseShare` (url nur für eigene Links, `hasPassword`, nie `redacted`), `parseShareList(data, selfId)` liefert `{ shares, hidden, truncated }` (für `received` die Arten 0/1 danach selbst filtern oder einen Parameter ergänzen), `parseSharePolicy` (Links, Passwort, Ablauf, `uploadAllowed`, `multipleLinks`, `passwordMinLength`), `permissionsFor(access, itemType)` (liefert `null` für Hochladen bei einer Datei), `accessOf`, `isShareId`, `cleanText`.
|
||||||
|
- API Dienst `nextcloud-files-shares.service.ts`: private Helfer `run(tenantId, userId, session, opts, operation, sent)` (wirft über `mapShareFailure`), `loadPolicy`, `listByPath`, `loadShare`; `SentFields { kind, passwordNonEmpty, expireDateSent, expireDateNonEmpty }` steuert D-15. `checkShareCreate(userId)` sitzt unmittelbar vor dem POST und nach jeder Vorprüfung.
|
||||||
|
- Alle zwölf Fehlercodes samt Texten stehen in `nextcloud-files.types.ts`; im Web sind sie in `KNOWN` und `nextcloudFiles.codes` (de und en) vorhanden, dazu `codes.ncDetail` und `sharingDisabledGroups`.
|
||||||
|
- DTO `CreateShareDto.kind` erlaubt noch nur `user` und `group` (`SHARE_KINDS_PEOPLE`), `UpdateShareDto` nur `access`.
|
||||||
|
- Web: `share-policy.ts` hat schon `accessOptions` mit Link-Zweig und `generatePassword(minLength, fill?)`; es fehlen `linkPasswordMode`, `todayLocal`, `addDays`, `expiryRule`, `linkUpdateDiff`. `ShareDialog` hat `run()` (ein Aufruf zugleich, `busy`), `fail()` (connectionExpired führt zu `onExpired`) und den Fehlerblock mit zweiter Zeile; der Link-Abschnitt kommt neben `<section aria-labelledby=...-people>`.
|
||||||
|
- `FileBrowser` hat `sharingEnabled` (Standard true) und reicht `onShare` (optional) an `FileList`/`FileGrid`; `initialPath`/`initialFocus` fehlen noch. `page.tsx` ist unverändert (Tabs, Richtlinie, `browserStart` noch offen).
|
||||||
|
|
||||||
|
**Stolpersteine, die ich gemessen habe**
|
||||||
|
- Nextcloud antwortet auf `PUT` für eine Berechtigungsänderung 200 mit dem neuen Objekt; Bens eigene Liste (`shared_with_me`) zeigt `permissions` (Freigabe) und `item_permissions` (wirksam) getrennt (1 und 9 nach dem Ändern auf Ansehen bei einem Ordner), `parseShare` nutzt `permissions` für `access` und `item_permissions` für `itemWritable`.
|
||||||
|
- Im Live-Skript immer erst die Liste neu holen, bevor man eine Eigenschaft liest (`ben_shares`), sonst misst man einen alten Stand.
|
||||||
|
- Das Skript schaltet die Ratenbegrenzung der Test-Nextcloud nur für seinen Lauf aus (Schlüssel `ratelimit.protection.enabled`, im `trap` gelöscht; nach den Läufen mit `occ config:system:get ratelimit.protection.enabled` geprüft: nicht gesetzt). Es setzt/löscht außerdem im `trap` die Schlüssel `core shareapi_enforce_links_password`, `shareapi_default_expire_date`, `shareapi_enforce_expire_date`, `shareapi_expire_after_n_days` und `files_sharing default_accept` für `anna`. Aufgabe 2 muss sie dort setzen (Details im Plan, Abschnitt "Discovered facts").
|
||||||
|
- Der Zähler von Tessera für neue Freigaben liegt im Arbeitsspeicher; `docker compose restart api` setzt ihn zurück. Der Abschnitt people legt 2 Freigaben an (Person, Gruppe), die Abschnitte links und received dürfen insgesamt höchstens 6 je `all`-Lauf (Plan) anlegen.
|
||||||
|
- Der Abschnitt people braucht die Test-Nextcloud mit `ben` (`User3-Pass-12345`) und Gruppe `tessera-team`; beides legt das Skript selbst an (bleibt nach dem Lauf bestehen).
|
||||||
|
- Browser-Rauchtest ohne MCP: `playwright-core` aus `/home/vicolab/.npm/_npx/705bc6b22212b352/node_modules/playwright-core` mit `executablePath: '/home/vicolab/.cache/ms-playwright/chromium-1247/chrome-linux64/chrome'` (die Standard-Chromium-Version der Bibliothek ist nicht installiert); Anmeldung admin/admin123 auf http://localhost:3000, Anna ist im Admin-Konto verbunden. Skript lag im Arbeitsverzeichnis der Sitzung.
|
||||||
|
- Formatieren nur mit der Dateiliste der Aufgabe (`biome format --write <dateien>`), nie über ein ganzes Verzeichnis.
|
||||||
|
|
||||||
|
**Aufgabe 1 hat keine Dokumente angefasst:** SUMMARY, STATE und PLAN sind nicht committet; ROADMAP unverändert.
|
||||||
|
|
||||||
|
## Aufgabe 2: Links nach den Regeln der Nextcloud, Von mir geteilt, Mit mir geteilt (fertig, zwei Commits, nicht gepusht)
|
||||||
|
|
||||||
|
| Commit | Inhalt |
|
||||||
|
|--------|--------|
|
||||||
|
| fe429db | Schnittstelle: Links anlegen/ändern (Passwort, Ablauf, Bezeichnung, Vorprüfung aus den frischen Fähigkeiten, echte Datumsprüfung, nur geänderte Felder), `shares/mine`, `shares/received`, `POST shares/:id/accept`, DTO und Controller, Live-Test (Abschnitte links, received) |
|
||||||
|
| c3adf85 | Oberfläche: `LinkShareForm`, Link-Abschnitt im `ShareDialog`, `SharesView` (beide Ansichten), Reiter in `page.tsx`, `FileBrowser` `initialPath`/`initialFocus`, Hilfen in `share-policy.ts`, `clipboard.ts`, Web-Client, Meldungen de/en |
|
||||||
|
|
||||||
|
### Gemessene Tore (neu gebauter Stack, `docker compose up -d --build api web`)
|
||||||
|
|
||||||
|
| Tor | Ergebnis |
|
||||||
|
|-----|----------|
|
||||||
|
| api vitest `src/nextcloud-files module-manage-handlers` | 17 Dateien, 614 Tests grün (Dienst jetzt 73, Freigaben-Schicht 40, Controller +Routen) |
|
||||||
|
| web vitest `modules/nextcloud-files components/nextcloud-files src/lib/nextcloud-files src/messages` | 17 Dateien, 265 Tests grün (neu: SharesView 14, ShareDialog-Links 14, Seite +8, Hilfen +14, Client +3, FileBrowser +2) |
|
||||||
|
| tsc api / web | sauber |
|
||||||
|
| biome lint (Pfade des Plans) | 0 Fehler; dieselben 4 alten Warnungen in `nextcloud-files-account.service.spec.ts` |
|
||||||
|
| Schlüsselgleichheit de/en (310 Schlüssel) und Wortsperre | ok |
|
||||||
|
| `nc-test-setup.sh` | `nc test ready` |
|
||||||
|
| `e2e-shares.sh all` | people, links, received grün (`version` folgt in Aufgabe 3); zweimal hintereinander grün, jeweils nach `docker compose restart api` (Zähler zurückgesetzt) |
|
||||||
|
| Zurückgesetzt nach den Läufen | alle occ-Schlüssel (`shareapi_*`, `ratelimit.protection.enabled`, annas `default_accept`) geprüft: nicht gesetzt; Fixture-Ordner von anna und ben entfernt |
|
||||||
|
| Browser-Rauchtest (playwright-core, dunkel) | Reiter Dateien/Von mir geteilt/Mit mir geteilt, Teilen am Ordner, Link erstellen und anzeigen, „Link kopieren“ zeigt „Kopiert“, Von mir geteilt zeigt den Eintrag, Link löschen mit Rückfrage: ok (Bilder liegen im Arbeitsverzeichnis der Sitzung, keine Beweisbilder) |
|
||||||
|
|
||||||
|
### Gemessene Nextcloud-Werte (Nextcloud 34.0.4, Ausgabe des Live-Tests)
|
||||||
|
|
||||||
|
| Messung | Ergebnis | passt zu D-15? |
|
||||||
|
|---------|----------|----------------|
|
||||||
|
| PUT mit schwachem Passwort `abc` (direkt bei der Nextcloud) | Status **400** | ja (400 + Passwort gesendet = `sharePasswordRejected`) |
|
||||||
|
| PUT mit Ablauf jenseits des Höchstwerts (heute plus 30 bei 7 Tagen Pflicht, direkt) | Status **404** | ja (404 + Ablauf gesendet = `shareExpiryInvalid`) |
|
||||||
|
| POST über Tessera mit Ablauf jenseits des Höchstwerts | 400 `shareExpiryInvalid` | ja |
|
||||||
|
| PUT über Tessera mit Ablauf jenseits des Höchstwerts | 400 `shareExpiryInvalid` | ja |
|
||||||
|
| Berechtigungsbuchstaben eines eingehenden Ordners (Ansehen) | **`SGDNV`**: `S` ist da (Annahme A1 bestätigt, kein Rückfall auf `nc:mount-type` nötig); **`R` fehlt**, das Menü „Teilen“ (nur bei `R`) erscheint daher bei eingehenden Einträgen nicht | ja |
|
||||||
|
| Bens eigene Liste nach dem Verlassen durch anna | **0 Freigaben**: eine verlassene Personenfreigabe ist bei der Nextcloud ganz gelöscht, nicht nur ausgeblendet | Hinweis für die Anleitung |
|
||||||
|
| Link-Berechtigung bei Nextcloud | Ansehen kommt als `17` (Teilen-Bit 16 gesetzt) zurück, Nur hochladen als `20`; `accessOf` maskiert mit 15 und ordnet richtig zu | Hinweis |
|
||||||
|
| Link-Adresse | `http://172.17.0.1:18080/s/<Kennung>` (Nextcloud baut sie aus dem Aufrufhost, im Test intern; Hinweis für Anleitung/Betrieb) | Hinweis |
|
||||||
|
|
||||||
|
### Abweichungen
|
||||||
|
|
||||||
|
1. **[Rule 1 - Bug, Test] Fähigkeiten der Nextcloud nach occ-Änderung kurz veraltet:** die Nextcloud lieferte nach `occ config:app:set/delete` die Fähigkeiten für wenige Sekunden aus einem Zwischenspeicher (einmal gemessen: Pflicht-Passwort an, danach nach Löschen noch an). Tessera speichert nichts. Der Live-Test wartet deshalb mit `policy_wait` bis dreimal hintereinander die erwartete Richtlinie kommt, ehe er schreibt.
|
||||||
|
2. **[Rule 1] Live-Test-Erwartung korrigiert:** Links tragen bei der Nextcloud das Teilen-Bit (Ansehen 17, Nur hochladen 20); geprüft wird `permissions & 15` und `access`.
|
||||||
|
3. **[Rule 2] „Nur hochladen“ und „Bearbeiten“ bei Links brauchen auch Schreibrecht am Eintrag (API und Browser):** Plan nannte es nur für Bearbeiten; ein Ordner ohne Schreibrecht (`RG`) kann nicht zum Hochladen freigegeben werden, die Nextcloud lehnte es mit 403 ab. Jetzt `shareAccessInvalid` vorab, mit Test; `accessOptions` bietet es nicht an.
|
||||||
|
4. **[Rule 3] `isRealDate` exportiert** aus `nextcloud-shares.ts` (Dienst nutzt es für die Datumsprüfung), mit eigenem Test.
|
||||||
|
5. **Empfängerzeile „Gruppe {name}“:** die Empfängerzeile in „Von mir geteilt“ heißt „Gruppe team (Ansehen)“ statt verschachtelter Klammern.
|
||||||
|
6. Der Zähler der Begrenzung zählt auch einen Versuch, den die Nextcloud ablehnt (Ablauf jenseits des Höchstwerts, Abschnitt links); `all` verbraucht damit 7 Zähler (2 people, 5 links); höchstens zwei `all`-Läufe zwischen zwei `docker compose restart api`.
|
||||||
|
|
||||||
|
### Bekannte Lücken / Hinweise für Aufgabe 3
|
||||||
|
|
||||||
|
- Nicht umgesetzt wie im Plan beschrieben, weil nicht nötig: Passwort-Erzeugen-Endpunkt der Nextcloud (D-18, bleibt beim lokalen Zufallserzeuger).
|
||||||
|
- Das Passwort eines frisch angelegten (oder per „Passwort ändern“ gesetzten) Links steht nur solange im Dialog, wie er offen ist, in einem Hinweisfeld mit „Passwort kopieren“; es wird nie gespeichert. Der Anwenderleitfaden soll sagen: danach zeigt Tessera es nicht mehr.
|
||||||
|
- Der Dialog zeigt Fehler je Abschnitt (Personen, Link), nicht mehr nur unten; es gibt immer nur einen Fehlerblock.
|
||||||
|
- `SharesView` gruppiert „Von mir geteilt“ je Eintrag (Pfad); „Freigaben bearbeiten“ öffnet den bekannten `ShareDialog`; „Im Ordner zeigen“ öffnet den Ordner und fokussiert den Eintrag. „Mit mir geteilt“: „Öffnen“ bei einem Ordner geht auf `target`, bei einer Datei auf den Ordner darüber mit Fokus; offene Freigaben haben nur Annehmen/Ablehnen.
|
||||||
|
- Für die Bildbeweise: die Reiter heißen „Von mir geteilt“ / „Mit mir geteilt“ (Seite, Tab-Leiste); Datenattribute `nextcloud-files-shares-byMe` / `-withMe` am Abschnitt, `nextcloud-files-shared-by-me` / `-with-me` an der Hülle. Für Pflicht-Passwort-Bilder `occ config:app:set core shareapi_enforce_links_password --value=true --type=boolean` setzen und danach löschen; nach jeder occ-Änderung ein paar Sekunden warten (Zwischenspeicher der Fähigkeiten).
|
||||||
|
- Eingehende Freigaben tragen kein `R`: das Menü „Teilen“ erscheint dort nicht (richtig so).
|
||||||
|
- Für Aufgabe 3 sind `e2e-shares.sh` Abschnitt `version` (Stub) und der Modul-Changelog (1.0.0 um drei Punkte erweitern), CHANGELOG, vier Anleitungen und die Bildbeweise offen. Die Anleitungen sollen die gemessenen Werte nutzen: verlassene Freigabe ist weg, Link-Adresse stammt aus der Nextcloud (extern erreichbare Adresse in Tessera eintragen oder `overwritehost`), Fähigkeiten werden bei jeder Aktion frisch gelesen.
|
||||||
|
- Nicht committet: SUMMARY, STATE, PLAN; ROADMAP unverändert; die gestagten Löschungen von `.planning/HANDOFF.json` und `.planning/.continue-here.md` sind unberührt.
|
||||||
|
|
||||||
|
## Aufgabe 3: Modul-Changelog, Anleitungen, Gesamttore, Bildbeweise (fertig, ein Commit, nicht gepusht)
|
||||||
|
|
||||||
|
| Commit | Inhalt |
|
||||||
|
|--------|--------|
|
||||||
|
| 0585fd1 | Modul-Changelog 1.0.0 um drei Teilen-Punkte erweitert, `CHANGELOG.md` (zwei Punkte), vier Anleitungen, `e2e-shares.sh` Abschnitt `version`, zwei kleine Oberflächenkorrekturen aus der Bildprüfung |
|
||||||
|
|
||||||
|
### Version (Bestätigung zu D-05)
|
||||||
|
|
||||||
|
Die Modulversion von „Dateien“ **blieb 1.0.0** (Datum 2026-10-08 unverändert, die vier Punkte der Etappe 1 unverändert, drei `new`-Punkte angehängt, jetzt sieben). Das entspricht der Regel „Höchstens ein Sprung je Modul zwischen zwei Tessera-Freigaben“ (`docs/anleitung-entwicklung.md`): das Modul war noch in keiner Tessera-Freigabe (letzte Freigabe v1.10.1 vom 06.10.). Es gibt keine 1.1.0, das Seed-File blieb unverändert (die Version kommt aus dem Changelog), `CHANGELOG.md` trägt die beiden Punkte unter „Unveröffentlicht“ ohne „Modulversion …“. Der feste Test in `module-changelog.spec.ts` prüft jetzt zusätzlich: genau ein Eintrag 1.0.0, sieben Neu-Punkte, die letzten drei sind die Teilen-Punkte. Der Live-Test-Abschnitt `version` prüft über `GET /modules/changelog/nextcloud-files` Version 1.0.0 und den Punkt „Öffentliche Links“.
|
||||||
|
|
||||||
|
### Gemessene Tore (neu gebauter Stack: `docker compose up -d --build api web`, danach nach der Bildprüfung `--build web` noch einmal)
|
||||||
|
|
||||||
|
| Tor | Ergebnis |
|
||||||
|
|-----|----------|
|
||||||
|
| `pnpm --filter @tessera/api test` (voll) | 154 Dateien, 3182 Tests grün |
|
||||||
|
| `pnpm --filter @tessera/web test` (voll) | 151 Dateien, 1778 Tests grün |
|
||||||
|
| tsc api / web | sauber |
|
||||||
|
| biome lint (Pfade des Plans plus `module-changelog.spec.ts`) | 0 Fehler, 7 Warnungen, alle schon vorher da (4 in `nextcloud-files-account.service.spec.ts`, 3 `noNonNullAssertion` in `module-changelog.spec.ts`); mein neuer Test fügt keine hinzu |
|
||||||
|
| `biome format` auf den eigenen Dateien | sauber (`module-changelog.spec.ts` hat Altlasten im Wörterbuch oben, nicht angefasst) |
|
||||||
|
| Seed-Zeile im api-Log („Nextcloud files module seeded in registry“) | da |
|
||||||
|
| `nc-test-setup.sh`, `e2e-settings.sh`, `e2e-connect.sh`, `e2e-files.sh`, `e2e-transfer.sh` | alle grün (Etappe 1, Listenantwort geändert) |
|
||||||
|
| `e2e-shares.sh all` (nach `docker compose restart api`) | people, links, received, version grün; mehrfach, auch als letzter Lauf der Kette |
|
||||||
|
| Prüfkette aus `<automated>` | endet mit „final gates ok“ (Bilder: 14 vorhanden, 12 Pflicht plus 2 Zusatzbilder) |
|
||||||
|
| occ-Rücksetzung | `shareapi_enforce_links_password`, `ratelimit.protection.enabled`, annas `default_accept` geprüft: nicht gesetzt; Ordner Projekte, Angebote, Ben-Unterlagen, Ben-Offen von anna und ben gelöscht, Papierkörbe geleert; Annas Freigabenliste leer |
|
||||||
|
|
||||||
|
### Ergebnisse der Messungen (verlangt vom Plan, Abschnitt Ausgabe)
|
||||||
|
|
||||||
|
1. **Nextcloud-Status bei Änderung (e2e, Nextcloud 34.0.4):** PUT mit schwachem Passwort `abc` direkt bei der Nextcloud: **400**; PUT mit Ablauf jenseits des Höchstwerts direkt: **404**; über Tessera jeweils 400 `shareExpiryInvalid` beim Anlegen und Ändern. Alles passt zu D-15 (400 + Passwort gesendet = `sharePasswordRejected`, 404 + Ablauf gesendet = `shareExpiryInvalid`).
|
||||||
|
2. **Buchstabe S:** eingehende Ordner trugen `SGDNV`, also **S vorhanden** (Annahme A1 bestätigt, kein Rückfall auf `nc:mount-type`). `R` fehlt, daher kein Menüpunkt „Teilen“ an eingehenden Einträgen. Nach dem Verlassen durch anna zeigte **bens eigene Liste 0 Freigaben**: die verlassene Personenfreigabe ist bei der Nextcloud ganz gelöscht (steht so in der Anleitung).
|
||||||
|
3. **Version:** blieb 1.0.0 (siehe oben).
|
||||||
|
4. **Bildbeweise** (alle unter `/home/vicolab/projects/tessera-ctl/.playwright-mcp/nextcloud-files/`, im Git ignoriert), Auflösung 1440x900, Mobil 390x844:
|
||||||
|
- dunkel: `s2a-dark-menu.png`, `s2a-dark-people.png`, `s2a-dark-link-form.png`, `s2a-dark-links.png`, `s2a-dark-indicator.png`, `s2a-dark-by-me.png`, `s2a-dark-with-me.png` (mit „Noch nicht angenommen“), `s2a-dark-mobile.png`; zusätzlich `s2a-dark-link-password.png` (Passwort-Hinweis nach dem Erstellen) und `s2a-dark-link-expiry.png` (Ablaufdatum ändern)
|
||||||
|
- hell: `s2a-light-people.png`, `s2a-light-links.png`, `s2a-light-by-me.png`, `s2a-light-with-me.png`
|
||||||
|
- Im Browser ausgeführt und bestätigt: Teilen mit ben, Ändern auf Bearbeiten, Entfernen; Link erstellen (Pflicht-Passwort, „Passwort erzeugen“), Passwort kopieren, „Verstanden“, Link kopieren (Zwischenablage trug die Adresse `http://172.17.0.1:18080/s/<Kennung>`), Ablaufdatum ändern (Zeile zeigt „gültig bis …“), Link löschen mit Rückfrage; „Öffnen“ bei Ben-Unterlagen in „Mit mir geteilt“ landet im Ordner (Pfadleiste „Alle Dateien > Ben-Unterlagen“); offene Freigabe Ben-Offen angenommen, danach „Angenommen“ mit „von Ben Beispiel, Ansehen“.
|
||||||
|
5. **Checkliste für die echte Umgebung** steht weiter unten.
|
||||||
|
|
||||||
|
### Prüfung der Bilder gegen L-09 und Korrekturen
|
||||||
|
|
||||||
|
Jedes Bild wurde angesehen. Ruhige, dichte Listen; Kennzeichen in beiden Modi lesbar (Link-Symbol an „Projekte“, Personen-Symbol an „Ben-Unterlagen“); keine Großbuchstaben-Etiketten, keine Mittelpunkte in Meta-Zeilen, keine Pfeilknöpfe; Fokusring am Suchfeld sichtbar; Mobil als Bodenblatt mit gekürztem Gruppennamen und gekürzter Adresse, alle Knöpfe erreichbar. Zwei Mängel gefunden und behoben (im Commit):
|
||||||
|
|
||||||
|
1. **Linkformular verdeckt:** im Dialog stand das Formular unter den Personen, „Link erstellen“ und „Abbrechen“ lagen außerhalb des sichtbaren Bereichs. Das Formular scrollt jetzt beim Öffnen mit `scrollIntoView({ block: 'nearest' })` in den sichtbaren Bereich (`LinkShareForm.tsx`).
|
||||||
|
2. **Offene Freigabe zeigte „Eigene Berechtigung“:** die Nextcloud meldet eine noch nicht angenommene Freigabe mit permissions 0 (gemessen; `item_permissions` ist dort 9, das ist Lesen plus Löschen und deshalb nicht als Ansehen deutbar). „Mit mir geteilt“ zeigt bei offenen Freigaben jetzt keinen Berechtigungstext (`SharesView.tsx`, Test ergänzt); nach dem Annehmen steht „Ansehen“.
|
||||||
|
|
||||||
|
### Abweichungen
|
||||||
|
|
||||||
|
1. **[Rule 1 - Bug] zwei Oberflächenkorrekturen** aus der Bildprüfung (siehe oben), Dateien aus Aufgabe 2 im selben Commit.
|
||||||
|
2. **[Rule 3] Hell-Bilder im Modus „System“:** die Hell-Bilder entstanden mit der Themenwahl „System“ (Betriebssystem hell), nicht mit der Wahl „Hell“; das Aussehen ist gleich. Die Dunkel-Bilder entstanden über den Themenknopf.
|
||||||
|
3. **Beobachtung ohne Änderung (Etappe 1):** in einem eingehenden Nur-Ansehen-Ordner bietet die Dateiansicht „Neuer Ordner“ und „Hochladen“ an, die die Nextcloud ablehnen würde. Das war schon vor Etappe 2a so (der Ordner liegt im Hauptordner); nicht Teil dieser Aufgabe. Vorschlag für später: Schreibrecht des geöffneten Ordners auswerten.
|
||||||
|
4. Formatierer nur auf eigene Dateien angewendet.
|
||||||
|
|
||||||
|
### Bedrohungsstatus (Threat Model)
|
||||||
|
|
||||||
|
Alle Einträge T-dkv-01 bis -14 mit Disposition „mitigate“ sind in den Aufgaben 1 und 2 umgesetzt und durch Einheitstests und Live-Tests belegt; Aufgabe 3 ändert nur Dokumente, Changelog-Daten, einen Scroll-Aufruf im Formular und die Anzeige offener Freigaben. T-dkv-15 (Link-Adresse mit interner Adresse, „accept“) ist in Administrations- und Betriebshandbuch erklärt (externe Adresse eintragen oder `overwritehost`). Das Passwort eines Links erschien weder in einer Antwort noch im api-Log. Neue Angriffsfläche in Aufgabe 3: keine.
|
||||||
|
|
||||||
|
### Bekannte Stubs
|
||||||
|
|
||||||
|
Keine. Keine neuen Einträge im Fenster-Register nötig.
|
||||||
|
|
||||||
|
### Checkliste für die echte Umgebung (nach dem eigenen Pull auf alpha, Auslieferung macht der Benutzer)
|
||||||
|
|
||||||
|
- [ ] Version und Regeln der Firmen-Nextcloud: „Teilen“ an einer Datei öffnen, „Link erstellen“. Das Formular muss die Regeln der Firmen-Nextcloud so zeigen, wie sie dort eingestellt sind: Passwort Pflicht (Feld „Passwort (Ihre Nextcloud verlangt eines)“, „Passwort erzeugen“ geht), kein erzwungenes Ablaufdatum (nur „Ablaufdatum festlegen“ als Wahl).
|
||||||
|
- [ ] Link-Adresse: „Link kopieren“, in einem privaten Browserfenster öffnen. Die Adresse muss die **äußere** Nextcloud-Adresse tragen. Zeigt sie eine interne Adresse, entweder in den Einstellungen des Moduls die äußere Adresse eintragen oder in der `config.php` der Nextcloud `overwritehost`, `overwriteprotocol`, `overwrite.cli.url` setzen (steht in Administrations- und Betriebshandbuch); schon erstellte Links ändern sich nicht rückwirkend.
|
||||||
|
- [ ] Teilen mit einer Kollegin oder einem Kollegen: sie oder er sieht die Freigabe in der Nextcloud (und unter „Mit mir geteilt“ in Tessera, bei „Annehmen verlangt“ unter „Noch nicht angenommen“).
|
||||||
|
- [ ] Desktop-App: „Link kopieren“ und „Passwort kopieren“ legen in die Zwischenablage (Tauri-Fenster; falls die App-Zwischenablage nicht greift, fällt Tessera auf das Markieren des Felds zurück).
|
||||||
|
- [ ] Gruppenfreigabe: eine Gruppe suchen und teilen; „Teilen mit Gruppen“ in der Nextcloud muss erlaubt sein, sonst erscheint die deutsche Meldung dazu.
|
||||||
|
- [ ] Der Zähler (15 neue Freigaben in 10 Minuten je Benutzer) liegt im Arbeitsspeicher des `api`-Containers; nach einem Neustart beginnt er bei null.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- Commit 0585fd1 liegt auf main (`git merge-base --is-ancestor`), enthält genau die elf Dateien der Aufgabe, keine Löschungen; die gestagten Löschungen von `.planning/HANDOFF.json` und `.planning/.continue-here.md` sind unberührt.
|
||||||
|
- Dateien vorhanden: Changelog, Anleitungen (vier), `CHANGELOG.md`, `e2e-shares.sh`; alle zwölf Pflichtbilder vorhanden.
|
||||||
|
- Nicht committet, wie gefordert: SUMMARY, STATE, PLAN; ROADMAP unverändert. Nicht gepusht, nicht ausgeliefert.
|
||||||
|
|
||||||
|
## Review fixes (nach 261009-dkv-REVIEW.md)
|
||||||
|
|
||||||
|
Alle elf Befunde und die zwei Zusatzpunkte sind behoben (Einzelheiten in `261009-dkv-REVIEW.md`, Abschnitt „Fix status“). Zwei Commits, nicht gepusht, nicht ausgeliefert.
|
||||||
|
|
||||||
|
| Commit | Inhalt |
|
||||||
|
|--------|--------|
|
||||||
|
| 78f6cf3 | Schnittstelle: wörtliche Pfade und Kennungen (CR-01), `accessOf` nach Eintragsart (IN-03), Weitergaben (IN-05), Begrenzung 10/10 Minuten plus Versuchszähler und Aufräumen (WR-03), Serverdatum in den Regeln (IN-01), Berechtigungsbuchstaben des Ordners in der Liste (EXTRA-1), Live-Test (Ablehnen, Weitergabe, Ordner-Buchstaben) |
|
||||||
|
| d487a00 | Oberfläche und Dokumente: Link-Formular (WR-01), Abschnittsfehler und Kleinigkeiten (IN-02, IN-05), stabile Ansichten (WR-04), Ablaufgrenzen nach Serverdatum und Passwortlänge (IN-01, IN-04), Dateiansicht nach Rechten (EXTRA-1), Importreihenfolge (WR-02), CHANGELOG und vier Anleitungen (10 statt 15, Suche ab einem oder zwei Zeichen) |
|
||||||
|
|
||||||
|
### Tore (neu gebauter Stack: `docker compose up -d --build api web`)
|
||||||
|
|
||||||
|
| Tor | Ergebnis |
|
||||||
|
|-----|----------|
|
||||||
|
| `pnpm --filter @tessera/api test` (voll) | 154 Dateien, 3213 Tests grün (vorher 3182) |
|
||||||
|
| `pnpm --filter @tessera/web test` (voll) | 152 Dateien, 1812 Tests grün (vorher 1778) |
|
||||||
|
| tsc api / web | sauber |
|
||||||
|
| `biome check` (nicht nur lint) auf allen 31 geänderten Quelldateien, Tests und Meldungen | 0 Fehler, keine Warnungen; `organizeImports` in den sechs gemeldeten Dateien behoben |
|
||||||
|
| `nc-test-setup.sh`, `e2e-settings.sh`, `e2e-connect.sh`, `e2e-files.sh`, `e2e-transfer.sh` | alle grün |
|
||||||
|
| `e2e-shares.sh all` (nach `docker compose restart api`) | people, links, received, version grün; neu: Buchstaben des Ordners (`CK` an der Wurzel, `SGDN` im Nur-Ansehen-Ordner), Ablehnen, Weitergabe mit Pfad „Sub X“ |
|
||||||
|
| Browser (playwright-core, dunkel) | siehe unten |
|
||||||
|
| Zurückgesetzt | `shareapi_allow_public_upload`, `shareapi_*`, `ratelimit.protection.enabled`, `default_accept` nicht gesetzt; Fixtures (Fix-Link, Ben-RO, rs-test, rs-ro) und alle Freigaben gelöscht, Papierkörbe geleert; Freigabenlisten von anna und ben leer |
|
||||||
|
|
||||||
|
### Browserprüfung (dunkel, `.playwright-mcp/nextcloud-files/s2a-fix-*.png`)
|
||||||
|
|
||||||
|
- Eingehender Nur-Ansehen-Ordner „Ben-RO“ (`s2a-fix-dark-readonly.png`): kein „Neuer Ordner“, kein „Hochladen“, stattdessen „Nur ansehen“; Menü der Datei nur „Herunterladen“ und „In Nextcloud öffnen“ (`-readonly-menu`), Auswahlleiste nur „Herunterladen“ (`-readonly-selection`); die eigene Wurzel hat beide Knöpfe weiter (`-own-root`).
|
||||||
|
- Link mit eigener Berechtigung (Nextcloud-Maske 21): „Aktuell: Eigene Berechtigung“ gewählt und gesperrt (`-link-current`); nur die Bezeichnung geändert und gespeichert, in der Nextcloud blieb die Maske 21 und die Bezeichnung wurde übernommen.
|
||||||
|
- Suchfehler (Antwort 502 abgefangen) steht unter „Personen und Gruppen“, nicht unter „Link“ (`-search-error`).
|
||||||
|
|
||||||
|
### Messungen und Entscheidungen
|
||||||
|
|
||||||
|
1. **Weitergaben (IN-05):** `uid_owner` ist bei der Nextcloud der Freigebende, `uid_file_owner` der Dateieigentümer; die Annahme des Befunds (uid_owner = Dateieigentümer) stimmt nicht. Bens Weitergabe von Annas Ordner: bei Ben `uid_owner = ben`, `path` in Bens Baum (`/rs-test/Sub X`), `file_target` im Baum des Empfängers (`/Sub X`). Gewählt: Weitergabe bleibt in „Von mir geteilt“ mit dem eigenen `path` (`target` = `path`), mit Hinweis und Eigentümer; kein Ausschluss. Für Anna (Dateieigentümerin) liefert nur die Pfadliste mit `reshares=true` die Weitergabe, mit `uid_owner = ben`. Ohne die Änderung wäre `target` der Pfad im fremden Baum gewesen (nur dort, wo `target` benutzt wird: eingehende Ansicht).
|
||||||
|
2. **Ablehnen (IN-06):** `DELETE shares/{id}` ist richtig; `DELETE shares/pending/{id}` ergibt 405. Dienst blieb unverändert.
|
||||||
|
3. **Link mit Bearbeiten, nachdem die Nextcloud das öffentliche Hochladen verbot:** die Nextcloud lehnt dann schon eine reine Bezeichnungsänderung mit „Public upload is not allowed.“ ab (gemessen). Tessera bleibt dabei ehrlich: Berechtigung nicht still senken, die Meldung der Nextcloud steht als zweite Zeile; wer die Änderung will, wählt ausdrücklich „Ansehen“. Für den Fall ohne Konflikt (z. B. eigene Berechtigung bei erlaubtem Hochladen) bleibt die Berechtigung erhalten.
|
||||||
|
4. **Modulversion:** bleibt 1.0.0, kein neuer Punkt im Modul-Changelog (Korrekturen an noch nicht veröffentlichtem Code, die Punkte „Ordner durchsuchen“ und „Teilen“ gelten unverändert). Im `CHANGELOG.md` steht die Nummer 10 statt 15 und ein Satz zu den ausgeblendeten Aktionen bei „Arbeiten mit Dateien“.
|
||||||
|
5. **Meldungstext „tooManyShares“** gilt jetzt auch für den Versuchszähler („viele Freigaben angelegt“); Wortlaut unverändert gelassen, damit Oberfläche und API übereinstimmen.
|
||||||
|
6. Ohne Buchstaben des Ordners (ältere Antwort, fehlende Eigenschaft) blendet die Dateiansicht nichts aus; unbekannt ist nie verboten.
|
||||||
|
|
||||||
|
### Für die echte Umgebung (Checkliste)
|
||||||
|
|
||||||
|
- Die Begrenzung liegt jetzt bei 10 neuen Freigaben in 10 Minuten (Checkliste oben bei Aufgabe 3 sprach noch von 15).
|
||||||
|
- In einem Ordner, den jemand nur zum Ansehen geteilt hat, sind „Neuer Ordner“ und „Hochladen“ verschwunden; „Nur ansehen“ steht an deren Stelle.
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
phase: quick-261009-dkv
|
||||||
|
verified: 2026-10-09T12:00:00Z
|
||||||
|
status: human_needed
|
||||||
|
score: 7/7 must-haves verified
|
||||||
|
covered_files:
|
||||||
|
- ".planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/261009-dkv-PLAN.md"
|
||||||
|
- ".planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/261009-dkv-SUMMARY.md"
|
||||||
|
- "CHANGELOG.md"
|
||||||
|
- "apps/api/src/nextcloud-files/nextcloud-files-shares.service.ts"
|
||||||
|
- "apps/api/src/nextcloud-files/nextcloud-files.changelog.ts"
|
||||||
|
- "apps/api/src/nextcloud-files/nextcloud-files.controller.ts"
|
||||||
|
- "apps/api/src/nextcloud-files/nextcloud-shares.ts"
|
||||||
|
- "apps/web/src/app/(portal)/modules/nextcloud-files/components/ShareDialog.tsx"
|
||||||
|
- "apps/web/src/app/(portal)/modules/nextcloud-files/components/SharesView.tsx"
|
||||||
|
- "apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx"
|
||||||
|
covered_digest: "v3:sha256:b2a405e2df4a030f7ed36a98948410c09647f98f4ed01a67f0b421e85e0d61ea"
|
||||||
|
behavior_unverified: 0
|
||||||
|
overrides_applied: 0
|
||||||
|
human_verification:
|
||||||
|
- test: "Echte Firmen-Nextcloud: Teilen an einer Datei öffnen, Link erstellen"
|
||||||
|
expected: "Formular zeigt Passwort-Pflicht (Feld 'Passwort (Ihre Nextcloud verlangt eines)', 'Passwort erzeugen' funktioniert), Ablaufdatum nur als freiwillige Wahl"
|
||||||
|
why_human: "Die Richtlinie der Firmen-Nextcloud ist nur dort messbar; geprüft wurde gegen die Test-Nextcloud 34.0.4 (Pflicht-Passwort, Pflicht-Ablauf, nichts erzwungen)"
|
||||||
|
- test: "Link kopieren und in einem privaten Browserfenster öffnen"
|
||||||
|
expected: "Die Link-Adresse trägt die äußere Nextcloud-Adresse (nicht die interne)"
|
||||||
|
why_human: "Die Adresse baut die Nextcloud aus dem Aufrufhost; im Test war sie intern (172.17.0.1:18080)"
|
||||||
|
- test: "Teilen mit einer echten Kollegin/einem Kollegen und mit einer echten Gruppe"
|
||||||
|
expected: "Empfänger sieht die Freigabe in Nextcloud und unter 'Mit mir geteilt'; bei 'Annehmen verlangt' unter 'Noch nicht angenommen'"
|
||||||
|
why_human: "Echte Konten und Gruppen der Firma; 'Teilen mit Gruppen' muss dort erlaubt sein"
|
||||||
|
- test: "Desktop-App (Tauri): 'Link kopieren' und 'Passwort kopieren'"
|
||||||
|
expected: "Inhalt landet in der Zwischenablage, sonst Rückfall auf Markieren des Felds"
|
||||||
|
why_human: "Zwischenablage im Tauri-Fenster ist per Test nicht prüfbar"
|
||||||
|
- test: "Sichtprüfung der Beweisbilder in .playwright-mcp/nextcloud-files/ (s2a-*.png)"
|
||||||
|
expected: "Ruhige Optik, Mosaik-Regeln (keine Großbuchstaben-Etiketten, keine Mittelpunkte, keine Pfeilknöpfe), dunkel und hell lesbar"
|
||||||
|
why_human: "Optik; der Verifizierer hat nur s2a-dark-link-form.png angesehen (Dialog, Pflicht-Passwort, Passwort erzeugen, Personenliste lesbar)"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick 261009-dkv: Dateien Etappe 2a (Teilen) Verification Report
|
||||||
|
|
||||||
|
**Goal:** Modul Dateien (nextcloud-files) Etappe 2a: Teilen von Dateien und Ordnern über Nextcloud (Personen, Gruppen, öffentliche Links nach Nextcloud-Richtlinie), Ansichten Von mir geteilt / Mit mir geteilt, Freigabe-Kennzeichen, ändern/entfernen, Modul-Changelog (1.0.0 erweitert, Version bleibt 1.0.0), CHANGELOG, vier Anleitungen.
|
||||||
|
**Verified:** 2026-10-09
|
||||||
|
**Status:** human_needed (keine Lücken, nur Prüfungen, die nur in der echten Umgebung oder per Auge gehen)
|
||||||
|
**Re-verification:** Nein, erste Prüfung
|
||||||
|
|
||||||
|
## Goal Achievement
|
||||||
|
|
||||||
|
### Observable Truths
|
||||||
|
|
||||||
|
| # | Truth | Status | Evidence |
|
||||||
|
|---|-------|--------|----------|
|
||||||
|
| 1 | Teilen mit Personen und Gruppen (Sharee-Suche, Ansehen/Bearbeiten, ändern, entfernen), nur bei Buchstabe R | ✓ VERIFIED | `nextcloud-files-shares.service.ts` `sharees()` (shareType[0]/[1]), `create()` (stat für Art/Schreibrecht, `permissionsFor`), `update()`, `remove()`; `FileBrowser.tsx:693` Menüpunkt nur bei `permissions.includes('R')`, `kind: 'share'` Dialog. Spec grün (Dienst 73 Tests). Live-Lauf `e2e-shares.sh` laut SUMMARY grün, von mir nicht erneut ausgeführt (würde Nextcloud-Zustand ändern) |
|
||||||
|
| 2 | Öffentliche Links (Ansehen/Bearbeiten/Nur hochladen), Passwort/Ablauf aus den eigenen Fähigkeiten bei jedem Lesen/Schreiben frisch, `expireDate: ""` für ohne Ablauf | ✓ VERIFIED | `loadPolicy()` ruft `cloud/capabilities` bei jedem `policy()`, `create()` und Link-`update()` ohne Zwischenspeicher; `checkLinkRules`; `json.expireDate = input.expireDate` wird auch als `""` gesendet; `LinkShareForm.tsx` (366 Zeilen) vorhanden und in `ShareDialog` eingebunden; Tests grün |
|
||||||
|
| 3 | API prüft Passwort/Ablauf vor dem Aufruf, German-Fehlercodes mit `ncMessage`, nie 401/403, 401 markiert Verbindung abgelaufen, 429 behält Etappe-1-Sperre; Passwort in keiner Antwort/keinem Log | ✓ VERIFIED | `mapShareFailure` (create/update/remove/accept/read-*) nach D-15, alles ≥500/401/429 über `mapNcFailure` mit `onExpired`; `parseShare` liefert nur `hasPassword` (`nextcloud-shares.ts:321`), kein Logger in `nextcloud-shares.ts` und im Dienst (grep); DTOs mit `whitelist`, Datum `^\d{4}-\d{2}-\d{2}$` plus `isRealDate`; Spec-Tests grün |
|
||||||
|
| 4 | Ansichten Von mir geteilt / Mit mir geteilt für jeden verbundenen Benutzer, inkl. offener Freigaben mit Annehmen/Ablehnen, Verlassen, andere Arten nur als Zahl | ✓ VERIFIED | `mine()`, `received()` (accepted + pending, 404/405 bei pending = leer), `accept()`; `page.tsx` TabId `files|sharedByMe|sharedWithMe|settings`, `SharesView mode="byMe"/"withMe"`; `hidden`-Zähler für nicht gezeigte Arten; Controller-Routen vorhanden; SharesView-Tests (14) grün |
|
||||||
|
| 5 | Freigabe-Kennzeichen in Liste und Raster; ausgehend aus `oc:share-types`, öffnet den Dialog | ✓ VERIFIED | `nextcloud-propfind.ts:51,182,210` `shareTypes`; `ShareIndicator.tsx`, `FileList.tsx`/`FileGrid.tsx` mit `onShare`; Test in `FileBrowser.test.tsx`; Bild `s2a-dark-indicator.png` vorhanden |
|
||||||
|
| 6 | Tessera-Begrenzung 15 pro 10 Minuten (16. gibt 429 `tooManyShares` ohne Nextcloud-Aufruf); Duplikat gibt `shareAlreadyExists` | ✓ VERIFIED | `nextcloud-login-guard.ts:46-47,191-200` `checkShareCreate`, aufgerufen im Dienst unmittelbar vor dem POST, nach allen Vorprüfungen; Duplikatprüfung über `listByPath` in `create()`; Guard-Spec (23 Tests) und Dienst-Spec grün |
|
||||||
|
| 7 | Modulversion bleibt 1.0.0 mit drei neuen Teilen-Punkten; CHANGELOG.md und alle vier Anleitungen beschreiben das Teilen | ✓ VERIFIED | `nextcloud-files.changelog.ts`: genau ein Eintrag 1.0.0 vom 2026-10-08, sieben `new`-Punkte, davon die letzten drei zum Teilen („Öffentliche Links erstellen…“); Seed nutzt `latestVersion(...)` (`nextcloud-files.seed.ts:18`), keine feste Version; `module-changelog.spec.ts` (76 Tests) grün; `CHANGELOG.md` zwei Punkte unter „Unveröffentlicht“ ohne „Modulversion“; Anwender (+Teilen, Link erstellen, Übersichten, Grenzen), Administration (Richtlinie/Link-Adresse), Betrieb (Limit, Fehlerbilder), Entwicklung (+55 Zeilen) im Commit 0585fd1 |
|
||||||
|
|
||||||
|
**Score:** 7/7 Truths verifiziert (0 behavior-unverified)
|
||||||
|
|
||||||
|
### Required Artifacts
|
||||||
|
|
||||||
|
| Artifact | Status | Details |
|
||||||
|
|----------|--------|---------|
|
||||||
|
| `nextcloud-shares.ts` (464 Z.) | ✓ VERIFIED | `ocsShareRequest`, Parser, `permissionsFor`, `accessOf`, `isRealDate`; vom Dienst genutzt |
|
||||||
|
| `nextcloud-files-shares.service.ts` (538 Z.) | ✓ VERIFIED | in Modul und Controller eingebunden |
|
||||||
|
| `dto/nextcloud-files-shares.dto.ts` | ✓ VERIFIED | Aufzählungen `kind`/`access`, keine Bitmasken |
|
||||||
|
| `share-policy.ts`, `ShareDialog.tsx`, `LinkShareForm.tsx`, `SharesView.tsx`, `ShareIndicator.tsx` | ✓ VERIFIED | vorhanden, verdrahtet (`page.tsx`, `FileBrowser.tsx`) |
|
||||||
|
| `nextcloud-files.changelog.ts` | ✓ VERIFIED | siehe Truth 7 |
|
||||||
|
| `e2e/e2e-shares.sh` | ✓ VERIFIED (Existenz) | vorhanden; Lauf nicht wiederholt |
|
||||||
|
|
||||||
|
### Key Links
|
||||||
|
|
||||||
|
| Von | Nach | Status |
|
||||||
|
|-----|------|--------|
|
||||||
|
| Dienst `create` → `checkShareCreate` | Login-Guard | ✓ WIRED (`shares.service.ts:372`) |
|
||||||
|
| Dienst → `mapNcFailure` mit `onExpired` → `markExpired` | Etappe-1-Fehlervertrag | ✓ WIRED (`transportFail`) |
|
||||||
|
| Dienst → `cloud/capabilities` | frisch bei jeder Nutzung | ✓ WIRED (`loadPolicy`) |
|
||||||
|
| Propfind → `shareTypes` | Kennzeichen | ✓ WIRED |
|
||||||
|
| `FileBrowser` Menü → `ShareDialog` | `kind: 'share'`, nur bei R | ✓ WIRED |
|
||||||
|
| Seed → `latestVersion(NEXTCLOUD_FILES_CHANGELOG)` | 1.0.0 | ✓ WIRED |
|
||||||
|
| Controller: statische Routen vor `:id` | Reihenfolge | ✓ (`shares/policy…POST shares` Zeilen 307-347, `PUT/DELETE shares/:id`, `:id/accept` ab 414); Controller-Spec prüft Reihenfolge |
|
||||||
|
|
||||||
|
### Behavioral Spot-Checks
|
||||||
|
|
||||||
|
| Check | Ergebnis |
|
||||||
|
|-------|----------|
|
||||||
|
| api vitest `src/nextcloud-files`, `module-manage-handlers`, `module-changelog` | 18 Dateien, 690 Tests grün |
|
||||||
|
| web vitest `modules/nextcloud-files`, `components/nextcloud-files`, `lib/nextcloud-files`, `messages` | 17 Dateien, 265 Tests grün |
|
||||||
|
| `tsc --noEmit` api und web | sauber (keine Ausgabe) |
|
||||||
|
| Schuldenmarker (TBD/FIXME/XXX/TODO/HACK) in allen geänderten .ts/.tsx/.sh | keine |
|
||||||
|
|
||||||
|
### Probe Execution
|
||||||
|
|
||||||
|
Keine Sonde deklariert. `e2e-shares.sh` braucht die Test-Nextcloud und verändert deren Zustand; nicht erneut ausgeführt (SUMMARY meldet mehrfach grün, als Beleg nicht gewertet, aber durch Unit-Tests und Code gestützt).
|
||||||
|
|
||||||
|
### Requirements Coverage
|
||||||
|
|
||||||
|
QUICK-261009-dkv: ✓ erfüllt (D-01 bis D-10 im Code und in den Tests nachgewiesen; D-05 Version bleibt 1.0.0 ✓).
|
||||||
|
|
||||||
|
### Anti-Patterns
|
||||||
|
|
||||||
|
Keine Blocker. Hinweise:
|
||||||
|
|
||||||
|
| Datei | Befund | Schwere |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| `docs/anleitung-anwender.md` | Text sagt „mindestens zwei Buchstaben“ für die Suche; Code nutzt `max(1, minSearchLength)` aus der Nextcloud-Richtlinie. Auf typischen Servern (Standard 2) stimmt es, ist aber nicht fest | Info |
|
||||||
|
| SUMMARY | Gibt an, Passwort-Erzeuger der Nextcloud (D-18) bewusst nicht genutzt; deckt sich mit der Planvorgabe (lokaler CSPRNG) | Info |
|
||||||
|
| SUMMARY Beobachtung | Eingehender Nur-Ansehen-Ordner bietet „Neuer Ordner/Hochladen“ an (schon in Etappe 1), außerhalb des Auftrags | Info |
|
||||||
|
|
||||||
|
### Human Verification Required
|
||||||
|
|
||||||
|
1. **Firmen-Nextcloud, Linkformular** – Teilen an einer Datei, „Link erstellen“; erwartet: Passwort Pflicht mit „Passwort erzeugen“, Ablauf freiwillig. Grund: nur in der echten Umgebung messbar.
|
||||||
|
2. **Link-Adresse** – „Link kopieren“, im privaten Fenster öffnen; erwartet: äußere Adresse. Sonst Adresse in den Moduleinstellungen oder `overwritehost` in der Nextcloud ändern (steht in Administrations- und Betriebshandbuch).
|
||||||
|
3. **Echte Empfänger** – Person und Gruppe teilen; Freigabe in Nextcloud und unter „Mit mir geteilt“ sichtbar.
|
||||||
|
4. **Desktop-App** – „Link kopieren“, „Passwort kopieren“ in der Zwischenablage.
|
||||||
|
5. **Optik** – Beweisbilder `s2a-*.png` durchsehen (nur ein Bild vom Verifizierer geprüft).
|
||||||
|
|
||||||
|
### Gaps Summary
|
||||||
|
|
||||||
|
Keine Lücken. Alle sieben Wahrheiten sind durch Code, Verdrahtung und grüne Tests belegt; die Modulversion bleibt 1.0.0 gemäß D-05, Changelog, CHANGELOG und die vier Anleitungen sind vorhanden und inhaltlich auf den Code abgestimmt. Offen sind nur die Prüfungen in der echten Firmenumgebung und die Sichtprüfung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Verified: 2026-10-09_
|
||||||
|
_Verifier: Claude (gsd-verifier)_
|
||||||
Reference in New Issue
Block a user