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
+
+ )}
+