397 lines
98 KiB
Markdown
397 lines
98 KiB
Markdown
---
|
||
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>
|