feat(forms): space-scoped public URL /f/[space]/[slug] (P6)

- migration 028: drop global UNIQUE(forms.slug), add
  UNIQUE(space_id, slug); schema.sql mirrored. Form slugs are now
  unique per space (fixes cross-tenant slug squatting)
- getPublicForm now takes { space, slug }: resolves the space by its
  slug then the form by space_id + slug (maybeSingle)
- route moved app/f/[slug] -> app/f/[space]/[slug]; the public client
  submits by formId (slug is no longer globally unambiguous) and the
  sign-in link round-trips the full /f/space/slug path
- forms-guard returns the space slug; the builder + forms list show
  the full /f/<space>/<slug> share URL; new/edit pass it through
- middleware /f allowlist already covers /f/<space>/<slug>
- getPublicFormSchema + tests updated (333 pass)

build verified (Compiled successfully, exit 0; new route present). No deploy.
This commit is contained in:
Moheeb Zara
2026-05-16 17:21:39 -07:00
parent 5a49ecb2cf
commit 8bbe99d34f
13 changed files with 73 additions and 26 deletions
+5 -3
View File
@@ -1899,7 +1899,7 @@ CREATE TRIGGER trg_prevent_member_self_role_change
CREATE TABLE IF NOT EXISTS public.forms (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
slug text NOT NULL UNIQUE
slug text NOT NULL
CHECK (slug ~ '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'
AND char_length(slug) BETWEEN 1 AND 80),
title text NOT NULL CHECK (char_length(title) BETWEEN 1 AND 200),
@@ -1915,9 +1915,11 @@ CREATE TABLE IF NOT EXISTS public.forms (
version integer NOT NULL DEFAULT 1 CHECK (version >= 1),
created_by uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT forms_space_slug_key UNIQUE (space_id, slug)
);
-- slug already has a UNIQUE btree index from the column constraint.
-- The UNIQUE(space_id, slug) constraint above also serves slug lookups
-- scoped to a space (the /f/[space]/[slug] public route).
CREATE INDEX IF NOT EXISTS idx_forms_space ON public.forms (space_id, status);
CREATE TABLE IF NOT EXISTS public.form_submissions (