feat(forms): M3b public-submit endpoint — schließt den Public-Loop

Server-side Public-Submit für unlisted-shared Forms (Plan
docs/plans/forms-module.md M3.b):

- POST /api/v1/forms/public/:token/submit (apps/api):
  - Token-resolve via unlistedSnapshots-Tabelle (eq, limit 1).
  - Hard-blocks: 404 unbekannt, 410 revoked/expired, 400 wrong
    collection, 400 invalid JSON.
  - Schema-validiert serverseitig: filtert eingehende answers auf
    field-IDs aus dem Snapshot (anti-injection), prüft required
    Antwort-Felder + required consent-Felder.
  - Hashed IP (SHA-256, hex) als Anti-Spam-Fingerprint, plus
    User-Agent + Referer truncated, in submitterMeta.
  - Schreibt sync_changes(table='formResponses', op='insert', data,
    field_meta, actor='system:forms-public-submit', origin='system')
    in einer Transaktion mit set_config('app.current_user_id') für
    RLS — mirror vom articles import-extractor.
  - Token-scoped rate-limit (10/min) + IP-scoped (30/min), gleiche
    Architektur wie unlisted/public-routes.
  - Returns { ok: true, responseId, submittedAt }.

- SharedFormView (apps/mana/apps/web): handleSubmit POSTet jetzt an
  ${PUBLIC_MANA_API_URL || origin:3060}/api/v1/forms/public/:token/submit.
  Submitting-State (Disabled-Button + "Sende ..."), Error-Block bei
  Server-Fehlern, Submitter-Block (Name + Email, beide optional). Der
  DEV-Hinweis ist weg.

Encryption: server speichert plaintext im sync_changes-Blob. Der
Client-side Decrypt-Path ist no-op für non-encrypted shapes
(record-helpers.ts:241), also kein Crash beim Pull. Encrypted-at-rest
für public submissions ist M6 ZK-Mode (eigener per-Form-Key der
Form-Owner client-seitig hält).

Mounted pre-auth in apps/api/src/index.ts neben unlisted/public.

apps/api buildet (1769 modules, no TS errors). svelte-check:
0 errors in forms/. Forms-Modul ist End-to-End nutzbar — User legt
Form an, publisht, setzt visibility=unlisted, kopiert Share-Link,
externe Person füllt aus + sendet, Antwort landet im
ResponsesView des Owners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-29 00:44:42 +02:00
parent 0d85d7c36b
commit e99fea1938
3 changed files with 363 additions and 26 deletions

View file

@ -52,6 +52,7 @@ import { websiteRoutes } from './modules/website/routes';
import { websitePublicRoutes } from './modules/website/public-routes';
import { unlistedRoutes } from './modules/unlisted/routes';
import { unlistedPublicRoutes } from './modules/unlisted/public-routes';
import { formsPublicRoutes } from './modules/forms/public-routes';
import { wetterRoutes } from './modules/wetter/routes';
const PORT = parseInt(process.env.PORT || '3060', 10);
@ -81,6 +82,7 @@ app.use('/api/*', rateLimitMiddleware({ max: 200, windowMs: 60_000 }));
app.route('/api/v1/wetter', wetterRoutes);
app.route('/api/v1/website/public', websitePublicRoutes);
app.route('/api/v1/unlisted/public', unlistedPublicRoutes);
app.route('/api/v1/forms/public', formsPublicRoutes);
app.use('/api/*', authMiddleware());