From 56602612c827b1d746fc7134a5ace58882c52a33 Mon Sep 17 00:00:00 2001 From: Tom Peltonen Date: Sat, 24 Oct 2026 10:57:57 +1100 Subject: [PATCH] Added foreignkey Used checkboxes adnd not radio --- README.md | 2 +- backend/internal/api/generated.go | 11 +- backend/internal/api/server.go | 4 +- backend/internal/pdf/generator.go | 2 +- backend/openapi/openapi.yaml | 12 +- docs/AGENTS.md | 4 +- docs/ARCHITECTURE.md | 2 +- frontend/src/components/QuestionField.tsx | 6 +- frontend/src/lib/types.ts | 2 +- frontend/src/pages/FillFormPage.tsx | 16 ++- frontend/src/pages/HomePage.tsx | 140 +++++++++++----------- frontend/src/test/components.test.tsx | 2 +- 12 files changed, 111 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 4ac5c41..1a1c277 100644 --- a/README.md +++ b/README.md @@ -146,5 +146,5 @@ See [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) and [`deploy/k8s.yaml`](deploy/k8 Management endpoints require a bearer JWT. In local-password mode change the default `admin` password and set a stable `localTokenSecret`; in OIDC mode enforce MFA and role assignment in the identity provider. Use HTTPS in production, keep `corsOrigins` narrow, -and enable `requireSubmissionAuth` if forms must not be publicly accessible. Access codes protect +and enable `requireSubmissionAuth` if forms must not be publicly accessible. Access codes protect individual form submissions, not administration. diff --git a/backend/internal/api/generated.go b/backend/internal/api/generated.go index 0c6af86..6991adf 100644 --- a/backend/internal/api/generated.go +++ b/backend/internal/api/generated.go @@ -51,10 +51,11 @@ const ( // Defines values for UserIdType. const ( - Email UserIdType = "email" - Guid UserIdType = "guid" - Phone UserIdType = "phone" - Username UserIdType = "username" + Email UserIdType = "email" + Guid UserIdType = "guid" + Phone UserIdType = "phone" + Username UserIdType = "username" + ForeignKey UserIdType = "foreignkey" ) // AccessCode defines model for AccessCode. @@ -382,7 +383,7 @@ type SubmitFormRequest struct { // Tracking Client supplied tracking details. IP and user agent are captured server-side. Tracking *ClientTracking `json:"tracking,omitempty"` - // UserId Respondent identifier captured at the start (email, username, phone or GUID) + // UserId Respondent identifier captured at the start (email, username, phone, foreignkey or GUID) UserId string `json:"userId"` UserIdType UserIdType `json:"userIdType"` } diff --git a/backend/internal/api/server.go b/backend/internal/api/server.go index a55eec6..cee0422 100644 --- a/backend/internal/api/server.go +++ b/backend/internal/api/server.go @@ -449,7 +449,7 @@ func (s *Server) SubmitForm(ctx context.Context, req SubmitFormRequestObject) (S userIdType := UserIdType(strings.ToLower(strings.TrimSpace(string(req.Body.UserIdType)))) if !validUserIdType(userIdType) { - return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON("userIdType must be one of email, username, phone or guid", "validation_error")}, nil + return SubmitForm400JSONResponse{ErrorJSONResponse: errJSON("userIdType must be one of email, username, phone, foreign key or guid", "validation_error")}, nil } userId := strings.TrimSpace(req.Body.UserId) if userId == "" { @@ -1045,7 +1045,7 @@ func toFloat(v interface{}) (float64, bool) { func validUserIdType(t UserIdType) bool { switch t { - case Email, Username, Phone, Guid: + case Email, Username, Phone, ForeignKey, Guid: return true default: return false diff --git a/backend/internal/pdf/generator.go b/backend/internal/pdf/generator.go index 4ca16f0..df4f1e1 100644 --- a/backend/internal/pdf/generator.go +++ b/backend/internal/pdf/generator.go @@ -183,7 +183,7 @@ func renderSubmitterDetails(doc *fpdf.Fpdf, form *database.Form) { doc.SetXY(pdfLeftMargin, y) doc.CellFormat(26, 6, "Identifier type:", "", 0, "L", false, 0, "") x := pdfLeftMargin + 28 - for _, label := range []string{"Email", "Username", "Phone", "GUID"} { + for _, label := range []string{"Email", "Username", "Phone", "ForeignKey", "GUID"} { drawCheckbox(doc, x, y+1) doc.SetXY(x+5, y) doc.CellFormat(0, 6, label, "", 0, "L", false, 0, "") diff --git a/backend/openapi/openapi.yaml b/backend/openapi/openapi.yaml index b585f7b..fac0a32 100644 --- a/backend/openapi/openapi.yaml +++ b/backend/openapi/openapi.yaml @@ -5,9 +5,13 @@ info: description: | API for **Kererū forms**, a simple survey/form capture system. - Forms are versioned and can be inactivated. A form contains an ordered set of + Forms are versioned and can be inactivated. + + A form contains an ordered set of questions whose answers are one of: true/false, numeric, a drop-down selection, - or free-form text. Responses are identified by a user identifier captured at the + date or free-form text. + + Responses are identified by a user identifier captured at the start of the form. A form may optionally require a single-use access code. contact: name: Kererū Forms @@ -806,7 +810,7 @@ components: $ref: '#/components/schemas/QuestionInput' UserIdType: type: string - enum: [email, username, phone, guid] + enum: [email, username, phone, foreignkey guid] Answer: type: object required: [questionId, value] @@ -822,7 +826,7 @@ components: properties: userId: type: string - description: Respondent identifier captured at the start (email, username, phone or GUID) + description: Respondent identifier captured at the start (email, username, phone, foreignkey or GUID) userIdType: $ref: '#/components/schemas/UserIdType' accessCode: diff --git a/docs/AGENTS.md b/docs/AGENTS.md index f3afbde..1c2927e 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -82,8 +82,8 @@ the question along with the answwer. ## Database Schema The schema is to allow for multiple forms and capture the user input -using an user identifier such as email, username, international phine number or GUID. -The identifier is captured at the start. +using an user identifier such as email, username, international phone number, +foreign key or GUID. The identifier is captured at the start. Forms can be versioned and include a logo and company name plus contact details that is linked to the form. Existing forms can be edited which will generate a new version number diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7ad203c..98145b5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -90,7 +90,7 @@ All timestamps are stored as RFC 3339 UTC strings. IDs are UUIDs (TEXT). Form definitions: non-empty title, unique keys (`[A-Za-z][A-Za-z0-9_-]*`), supported question types, select questions require unique non-empty option values, numeric bounds ordered. -Submissions: identifier type must be `email|username|phone|guid`; email identifiers are parsed; answers must reference questions of that form; duplicates rejected; required questions enforced; type checks for boolean/number/select/text; numeric bounds; select values must exist; text length capped and single-line questions reject newlines. +Submissions: identifier type must be `email|username|phone|foreignkey|guid`; email identifiers are parsed; answers must reference questions of that form; duplicates rejected; required questions enforced; type checks for boolean/number/select/text; numeric bounds; select values must exist; text length capped and single-line questions reject newlines. Access codes are consumed in the same SQL transaction as the submission insert. The conditional `UPDATE ... WHERE used = 0` makes reuse safe under concurrency; failure rolls back the submission. diff --git a/frontend/src/components/QuestionField.tsx b/frontend/src/components/QuestionField.tsx index 3ed1a10..2abcacc 100644 --- a/frontend/src/components/QuestionField.tsx +++ b/frontend/src/components/QuestionField.tsx @@ -33,15 +33,15 @@ export function QuestionField({ question, value, onChange }: QuestionFieldProps) if (question.type === 'boolean') { control = ( -
+
{[ { label: 'True', checked: value === true }, { label: 'False', checked: value === false }, ].map((option) => (
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index cc9d2bf..e19a200 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -42,24 +42,28 @@ export function HomePage() {

Kererū Forms

-

Simple, versioned surveys

+

Simple, versioned forms

- Pick a form below to share your feedback. Forms are versioned, so your previous answers are - never shown again. + Pick a form below to complete. Forms can also be thought of as surveys.

{(user?.canAdmin || user?.canView) && ( -
- - Manage forms - - {user?.canAdmin && ( - - Create a form +
+

+ Use the buttons below to manage forms or view submitted forms. +

+
+ + Manage forms - )} + {user?.canAdmin && ( + + Create a form + + )} +
)}
@@ -67,65 +71,65 @@ export function HomePage() { {submissionLocked ? ( ) : ( -
-
-

Available forms

- - {forms.length} active {forms.length === 1 ? 'form' : 'forms'} - -
- - {loading &&

Loading forms…

} - - {error && ( -
- {error} +
+
+

Available forms

+ + {forms.length} active {forms.length === 1 ? 'form' : 'forms'} +
- )} - {!loading && !error && forms.length === 0 && ( -
-

No active forms yet

-

- Create one from the management section to get started. -

-
- )} + {loading &&

Loading forms…

} -
- {forms.map((form) => ( -
-
-
-
- {form.logoUrl && ( - - )} -

{form.title}

+ {error && ( +
+ {error} +
+ )} + + {!loading && !error && forms.length === 0 && ( +
+

No active forms yet

+

+ Create one from the management section to get started. +

+
+ )} + +
+ {forms.map((form) => ( +
+
+
+
+ {form.logoUrl && ( + + )} +

{form.title}

+
+
- -
- {form.description && ( -

- {form.description} + {form.description && ( +

+ {form.description} +

+ )} +

+ Version {form.version} · {form.questionCount} questions

- )} -

- Version {form.version} · {form.questionCount} questions -

-
- - Open form - -
- ))} -
-
+
+ + Open form + + + ))} +
+ )}
) diff --git a/frontend/src/test/components.test.tsx b/frontend/src/test/components.test.tsx index d8ef07c..0ee607b 100644 --- a/frontend/src/test/components.test.tsx +++ b/frontend/src/test/components.test.tsx @@ -29,7 +29,7 @@ describe('StatusBadge', () => { }) describe('QuestionField', () => { - it('renders boolean radios', () => { + it('renders boolean checkboxes', () => { const onChange = vi.fn() const { getByLabelText } = render(