Skip to content

Commit 109d047

Browse files
thunpisitclaude
andauthored
feat(v2.0a): forms — first slice of v2.0 engagement (#43)
This is the first of four v2.0 PRs. Adds public-submittable forms with a CMS editor and an in-CMS moderation inbox. Schema (Drizzle migration 0007) ------------------------------- - forms: id, key (URL-safe, unique), label, fields (JSON array of FormField), enabled (bool), success_messages (per-locale JSON), created_by (FK users SET NULL), createdAt, updatedAt. - form_submissions: id, form_id (FK CASCADE), data (JSON), ip_hash (truncated SHA-256, 16 chars — never raw IP), status enum (new/read/spam/archived), note, submittedAt. Provider -------- - ContentProvider gains 11 new methods: list/get/getByKey/create/ update/delete for forms, list/get/create/update/delete for submissions, plus countRecentSubmissions for rate limiting. - D1 implementation in providers/d1.ts. Public endpoint (POST /api/forms/[key]) --------------------------------------- - Reads the form by key. Returns 404 if missing, 410 if disabled. - Parses multipart/url-encoded body. - Validates against form.fields: - honeypot (`_hp` field, expected empty), - per-field required + maxLength, - email kind: cheap regex `.+@.+\..+`, - checkbox: required-checkbox is GDPR consent pattern. - Rate limit: 3 submissions per minute per (form, ipHash). IP is hashed via SHA-256 truncated to 16 hex chars; raw IP is never stored. - Audit: writes a form.submit row with actorId=null (public). CMS UI ------ - /cms/forms — list table with label, public endpoint URL, field count, enabled badge. - /cms/forms/new + /cms/forms/[id] — shared FormEditor.svelte. Field list with add (text/email/textarea/checkbox), reorder (up/down), remove, per-field name + label + required toggle. - /cms/forms/[id] also embeds a submissions inbox underneath the editor: collapsible rows with status badges, mark-as (new/read/spam/archived), delete. Submissions are scoped to the current form via a defense-in-depth check on every action. - Sidebar gets a new "Forms" entry under the taxonomy group, gated to editor+. Audit ----- - New AuditAction members: form.create / form.update / form.delete / form.submit. Wired into create / update / delete actions and the public submission endpoint. i18n: 25 new cms_forms_* keys (EN + TH). Migration 0007 already applied to live D1. This PR doesn't touch the public site rendering — adding a \<form\> tag to a page or article is the editor's job. Future v2.x might ship a markdown shortcode that renders a form inline; out of scope here. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 68b91a5 commit 109d047

19 files changed

Lines changed: 3375 additions & 1 deletion

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
CREATE TABLE `form_submissions` (
2+
`id` text PRIMARY KEY NOT NULL,
3+
`form_id` text NOT NULL,
4+
`data` text NOT NULL,
5+
`submitted_at` text NOT NULL,
6+
`ip_hash` text,
7+
`status` text DEFAULT 'new' NOT NULL,
8+
`note` text,
9+
FOREIGN KEY (`form_id`) REFERENCES `forms`(`id`) ON UPDATE no action ON DELETE cascade
10+
);
11+
--> statement-breakpoint
12+
CREATE TABLE `forms` (
13+
`id` text PRIMARY KEY NOT NULL,
14+
`key` text NOT NULL,
15+
`label` text NOT NULL,
16+
`fields` text NOT NULL,
17+
`enabled` integer DEFAULT true NOT NULL,
18+
`success_messages` text,
19+
`created_by` text,
20+
`created_at` text NOT NULL,
21+
`updated_at` text NOT NULL,
22+
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null
23+
);
24+
--> statement-breakpoint
25+
CREATE UNIQUE INDEX `forms_key_unique` ON `forms` (`key`);

0 commit comments

Comments
 (0)