08 - Form Specifications (Webhooks Module)
- 1. Webhook form (S2 create / S4 edit)
- 2. Wire payloads
- 3. Event picker vocabulary (client-side)
- 4. Client-side validation order
- 5. Submit behaviour
- 6. Secret handling
- 7. Pause / Resume (non-form action)
The webhook form (create + edit share one model). Exact DTO contracts quoted from
create-webhook.dto.ts/update-webhook.dto.ts; validation mirrors class-validator rules so client and server agree.
1. Webhook form (S2 create / S4 edit)
| Field | Control | Required | Rules (server = source) | Server source |
|---|---|---|---|---|
name | AppTextField | yes (create) / optional (edit) | non-empty string | @IsString, create-webhook.dto.ts:13-14 |
url | AppTextField (.url keyboard) | yes / optional | valid URL; require_tld: false (local/private hosts allowed) | @IsUrl({ require_tld: false }), :17-18 |
events | EventPickerChipField | yes (≥ 1) | array of strings, min size 1 | @IsArray + @ArrayMinSize(1) + @IsString({ each: true }), :20-24 |
secret | SecretField | yes (create) / blank = keep (edit) | non-empty string | @IsString, :27-28 |
enabled | AppSwitch | no | boolean, default true | @IsOptional + @IsBoolean, :30-33; schema default webhook.schema.ts:22-23 |
Edit mode: UpdateWebhookDto = PartialType(CreateWebhookDto) - every field
optional (update-webhook.dto.ts:4); client sends only dirty fields.
2. Wire payloads
// POST /api/v1/webhooks (create-webhook.dto.ts:11-34)
{
"name": "SMS Gateway",
"url": "https://vendor.example.com/hooks/sms",
"events": ["AttendanceMarked", "HomeworkSubmitted"],
"secret": "whsec_9f2c…",
"enabled": true
}
// PATCH /api/v1/webhooks/:id (update-webhook.dto.ts:4)
{ "url": "https://vendor.example.com/hooks/sms/v2", "enabled": false }
3. Event picker vocabulary (client-side)
The API accepts any string, so the picker offers the emitted event registry
(event-queue-map.ts:6-43) grouped by module, plus WebhookTested
(webhooks.service.ts:134):
| Group | Events (event-queue-map.ts) |
|---|---|
| Auth / users | UserRegistered, UserLoggedIn, PasswordResetRequested, UserCreated, UserUpdated, UserDeleted (:7-12) |
| Organization | OrganizationCreated (:13) |
| Attendance | AttendanceMarked, AttendanceUpdated (:14-21) |
| Homework | HomeworkCreated, HomeworkUpdated, HomeworkSubmitted, HomeworkGraded, HomeworkDeleted (:22-26) |
| Results | ExamResultsPublished (:27) |
| People | StudentCreated/Updated/Deleted, TeacherCreated/Updated/Deleted, StaffCreated/Updated/Deleted, ParentCreated/Updated/Deleted (:28-39) |
| Fees / payments | FeeStructureCreated, InvoiceIssued, PaymentCompleted (:40-42) |
| Test | WebhookTested (test ping, webhooks.service.ts:134) |
"Custom event" input (optional, allowCustom): free text - matching is exact
string equality (webhook.repository.ts:27), typos silently never fire.
4. Client-side validation order
nameempty → "Name is required".urlnot parseable / no scheme → "Enter a valid URL" (mirror@IsUrl,create-webhook.dto.ts:17-18).eventsempty → "Select at least one event" (mirror@ArrayMinSize(1),:22-23).secretempty (create) → "Secret is required" (mirror@IsString,:27-28).enabledboolean coercion (switch) - no text validation.- Server 400s: map class-validator
messagearrays to fields; unknown messages → top-of-form error.
5. Submit behaviour
- Create:
POST /api/v1/webhooks(webhooks.controller.ts:21-25) → on success pop to detail (S3); on 400 keep form + field errors; on 401/500AppErrorState. - Edit:
PATCH /api/v1/webhooks/:id(webhooks.controller.ts:39-43) → same, pop to S3 with refreshed doc; 404 → error state (webhook deleted elsewhere). - Both: submitting lock (CTA spinner, fields disabled); no double submit.
6. Secret handling
- Create: required; "Generate" helper fills a strong random value client-side.
- Edit: blank = keep unchanged (never echo the stored secret into the field; show placeholder "Leave blank to keep current secret").
- Detail (S3): masked with reveal; copy-to-clipboard.
- Security: secret is stored and returned in plaintext by the API
(
webhooks.service.ts:31-47); rotation = PATCHsecret. Dedicated rotation flow with verification event(forward-looking).
7. Pause / Resume (non-form action)
- No form:
POST /webhooks/:id/pause→setEnabled(false),POST /webhooks/:id/resume→setEnabled(true)(webhooks.controller.ts:78-90;webhooks.service.ts:158-161). - Paused ≠ deleted: fan-out filter drops it (
webhook.repository.ts:23-28).