initial commit
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/* ============================================================================
|
||||
FORM SUBMISSIONS -> STRAPI. SERVER ONLY.
|
||||
----------------------------------------------------------------------------
|
||||
The two public forms of this section write into their own collections. Both
|
||||
are ordinary <Form method="post"> actions, so they work with scripting off.
|
||||
|
||||
THE PAYLOAD IS THE WHITELIST, NOTHING MORE
|
||||
Each collection has a `sanitize-submission` middleware in the CMS that
|
||||
copies out a fixed set of fields and DROPS everything else, then sets the
|
||||
workflow state and the timestamps server-side. Sending a field that is not
|
||||
on the list is not merely ignored - a required field that is missing is a
|
||||
400 and the reader loses what they typed. So the two payloads below are
|
||||
exactly the whitelists, read from:
|
||||
|
||||
apps/cms/src/api/event-registration/middlewares/sanitize-submission.ts
|
||||
event · fullName · email · phone · organisation · role ·
|
||||
dietaryOrAccess · consentGdpr
|
||||
|
||||
apps/cms/src/api/application/middlewares/sanitize-submission.ts
|
||||
opportunity · applicantName · email · phone · organisation ·
|
||||
organisationType · answers
|
||||
|
||||
`attachments` exists on the `application` content type but is NOT on that
|
||||
whitelist and must never be sent: it is a media relation, so its value is a
|
||||
list of ids of files already in the Media Library, and an unauthenticated
|
||||
caller who could set it would bind arbitrary uploads onto a row reviewers
|
||||
open. A public upload flow needs its own authenticated endpoint first.
|
||||
|
||||
There is no `language` field on either type ("locale" is reserved by the
|
||||
i18n plugin and `language` was never modelled), and no `waitlist` boolean.
|
||||
|
||||
THE ENDPOINTS
|
||||
The URL is the content type's PLURAL name:
|
||||
api::event-registration.event-registration -> /api/event-registrations
|
||||
api::application.application -> /api/applications
|
||||
(not "opportunity-applications", which does not exist and 404s).
|
||||
|
||||
WAITLIST IS DERIVED SERVER-SIDE, NEVER SENT
|
||||
`registrationState` is not on the registration whitelist, and it is not an
|
||||
omission to work around: the middleware works it out from the event itself -
|
||||
places taken vs `capacity`, plus `registrationStatus` / `waitlistEnabled` -
|
||||
and answers `confirmed`, `waitlisted`, or a 400 when the event is full with
|
||||
no waitlist to offer. That decision needs a count no client can be trusted
|
||||
to report, so this module sends only what the registrant actually supplied
|
||||
and lets the CMS decide what it is worth.
|
||||
|
||||
PERMISSIONS
|
||||
`create` on both collections is granted to the Public role; `find` /
|
||||
`findOne` are NOT, or anyone could read everyone else's contact details.
|
||||
========================================================================= */
|
||||
|
||||
import type { ApplicationValues, RegistrationValues } from "./forms";
|
||||
import { createStrapiEntry } from "./strapi-write.server";
|
||||
|
||||
export const EVENT_REGISTRATIONS_RESOURCE = "event-registrations";
|
||||
export const APPLICATIONS_RESOURCE = "applications";
|
||||
|
||||
/** Strapi rejects "" on an email field; absent is the honest value. */
|
||||
function optional(value: string): string | undefined {
|
||||
return value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
export interface RegistrationSubmission {
|
||||
readonly values: RegistrationValues;
|
||||
/** documentId of the event being registered for. */
|
||||
readonly eventId: string;
|
||||
}
|
||||
|
||||
export async function submitRegistration(
|
||||
input: RegistrationSubmission,
|
||||
): Promise<void> {
|
||||
await createStrapiEntry(EVENT_REGISTRATIONS_RESOURCE, {
|
||||
event: input.eventId,
|
||||
fullName: input.values.fullName,
|
||||
email: input.values.email,
|
||||
phone: optional(input.values.phone),
|
||||
organisation: optional(input.values.organisation),
|
||||
// Required in the schema: without it every POST is a 400.
|
||||
consentGdpr: input.values.consentGdpr,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ApplicationSubmission {
|
||||
readonly values: ApplicationValues;
|
||||
/** documentId of the opportunity being applied to. */
|
||||
readonly opportunityId: string;
|
||||
}
|
||||
|
||||
/** The key the free-text answer is filed under inside `answers` (json). */
|
||||
export const APPLICATION_MESSAGE_KEY = "message";
|
||||
|
||||
export async function submitApplication(
|
||||
input: ApplicationSubmission,
|
||||
): Promise<void> {
|
||||
await createStrapiEntry(APPLICATIONS_RESOURCE, {
|
||||
opportunity: input.opportunityId,
|
||||
// `fullName` is the registration's field name; an application's is
|
||||
// `applicantName`.
|
||||
applicantName: input.values.fullName,
|
||||
email: input.values.email,
|
||||
organisation: optional(input.values.organisation),
|
||||
// `answers` is a json field - the form's one free-text question goes in
|
||||
// it, keyed, so more questions can be added without another migration.
|
||||
answers: { [APPLICATION_MESSAGE_KEY]: input.values.message },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user