Repository navigation
Conversation
Builds a Zod schema from a FormDefinition that checks required answers, each type's value shape, short-text length, long-text word counts, number ranges and whole numbers, email, and US phone numbers (stored as digits). It rejects unknown question IDs and drops section headers. Also adds getFormAvailability for the open/close window and the headshot upload limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FormSubmissionService reads a form's definition for an org and writes a submission with its answers in one transaction, after checking for a duplicate and that each image answer's upload is this submitter's, for this form and org. The formSubmissions sub-app adds POST /forms/:formId/submissions and POST /forms/:formId/uploads. Both resolve the org from the host and return 404 when forms are off, accept only published forms inside their window, and require membership when the form requires login. Uploads take JPEG, PNG, or WebP up to 5 MB, check the file's bytes against its type, and are stored through IFileService without a media row. JunoFileService.upload now uploads through a presigned URL instead of throwing, so FileService.upload works for both implementations, and IFileService.upload takes only the file's location. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DynamicForm renders a FormDefinition in order through FORM_RENDERERS, typed per FormComponentType. Text, email, phone, and number questions use BogTextInput, long text gets a textarea with a live word count, and image questions upload on select with progress, replace, and remove. Answers are checked with buildSubmissionSchema on blur and on submit. Errors and help text are linked with aria-describedby, focus moves to the first invalid question, and a submission can't be sent twice or while an image is uploading. Preview mode hides the submit button and turns off uploads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
app/forms/[formId] resolves the org from the host and returns 404 when forms are off, the form is a draft, or it's another org's. Forms that require login send logged-out visitors to login and back, which the login and signup pages now support with a same-site redirect param. The page shows the org's logo on a brand panel beside the form, with no navbar, and submits through the typed RPC client. It covers the success, closed, not-yet-open, already-submitted, and not-found states, and ?preview=1 lets org admins view a draft or published form without submitting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
✅ Deploy Preview for servicestart ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| <div className="flex size-32 shrink-0 items-center justify-center overflow-hidden rounded-full bg-grey-fill-weak text-grey-icon-strong"> | ||
| {previewUrl ? ( | ||
| <img | ||
| src={previewUrl} |
|
Disclaimer: This comment was AI-generated and is not necessarily completely accurate. Please take code comments with a grain of salt.
|
| const next = { ...values }; | ||
| if (value === undefined) delete next[id]; | ||
| else next[id] = value; | ||
| setValues(next); |
There was a problem hiding this comment.
handleChange replaces all answers using the values captured when an upload started. If someone edits another answer while their image uploads, ImageQuestion later calls that old callback and erases those edits. Two image uploads can also overwrite each other.
Merge each change into the latest state with a functional setValues update. Add a test that edits another field during an upload.
There was a problem hiding this comment.
Fixed in b0d8084: changes now merge into the latest answers, with tests for an edit during an upload and two uploads finishing out of order
| const path = new URLSearchParams(search).get("redirect"); | ||
| return path && /^\/(?![/\\])/.test(path) ? path : "/"; |
There was a problem hiding this comment.
getRedirectPath accepts ?redirect=%2F%09%2Fevil.com. The decoded tab passes this regex, but URL parsing removes it and turns the path into //evil.com. The login and signup pages pass this value directly to router.replace for already signed-in users. A crafted link can therefore send them to another site without going through the server’s login checks.
Reject control characters and check the parsed URL’s origin before returning a local path.
How this was verified: The decoded redirect passes the regex and reaches the signed-in navigation calls; removing the tab during URL parsing makes it an external URL.
There was a problem hiding this comment.
Fixed in b2d9e63: getRedirectPath now parses the path and only returns it if it stays on this site, with tests for the tab, newline and carriage-return cases
| const id = await FormSubmissionService.createUpload({ | ||
| organizationId, | ||
| formId: form.id, | ||
| uploadedBy: userId, | ||
| fileName, | ||
| contentType, | ||
| sizeBytes: file.size, | ||
| }); |
There was a problem hiding this comment.
The upload route saves the image before createUpload records it, with no recovery if that insert fails. The headshot then has no database record, and retrying saves another copy. Replacing or removing an image also leaves its old upload behind.
Add a tracked cleanup path for failed and unused uploads, with a storage strategy that also works under Juno. This is a non-blocking improvement that avoids paying to store images no response uses.
An upload finishing called handleChange with the answers from when it started, so edits made during the upload were lost, and two uploads could overwrite each other. Changes now merge into the latest answers.
getRedirectPath accepted "/\t/evil.com": it passed the regex, but URL parsing drops the tab and leaves "//evil.com". It now parses the path and only returns it when it stays on this site.
The file used to be stored first, so a failed insert left it in storage with no row to find it by. The row now goes in first and is deleted if storing fails.
Closes #299
Adds the form fill-out page, form submissions, and headshot uploads.
What's new:
lib/forms/submission.ts:buildSubmissionSchema(definition)builds a Zod schema from a form's questions. It's a pure function, so the page and the submit route both use it.api/formSubmissions.ts: addsPOST /forms/:formId/submissionsandPOST /forms/:formId/uploads, backed byFormSubmissionService. Both routes:FormsEnabledis offIFileServicewithout amediarow, so they don't appear in the media library.components/forms/:DynamicFormshows each question through a registry with one renderer per question type. Answers are checked on blur and on submit, focus jumps to the first invalid question, and help text and errors are linked to their fields witharia-describedby.app/forms/[formId]: the page has a brand panel and no navbar, and handles these states: success, closed, not yet open, already submitted, not found. Admins can use?preview=1. Logged-out visitors go to login and come back to the form.Considerations
JunoFileService.uploadused to throw. It now uploads from the server through a presigned URL, so the file we store is the one the server checked. This also makes profile picture uploads work under Juno.BogTextInputhas no number type, so number questions use a text input with a numeric keypad. A wrapper adds the id and ARIA attributes, sinceBogTextInputcan't take them.components/bog/is unchanged.closedshows a "closed" message instead of a 404. Drafts return 404./forms/inNO_NAVBAR_PAGEShides the navbar on every page under/forms/, including any admin pages [Forms] Build the question builder for the application question types #298 adds there.Testing
POST /api/media"creates media record…", also fails onmainwithout this branch.Checklist: