From 7f6c7bd81f806f10cabf48b11e078925c4b7cf77 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 29 Jul 2026 15:46:09 +1200 Subject: [PATCH 01/36] WIP start dry type constraint refactor --- application/backend/src/routes.ts | 125 +++++++--- application/backend/swagger.json | 229 ++++++++++-------- application/common/types/api/auth/login.ts | 17 +- application/common/types/api/auth/register.ts | 39 +-- .../types/api/auth/registerParticipant.ts | 97 +++----- .../types/api/users/ParticipantProfile.ts | 18 +- application/common/types/commonTypes.ts | 90 +++++++ application/common/types/invite.ts | 8 +- 8 files changed, 357 insertions(+), 266 deletions(-) create mode 100644 application/common/types/commonTypes.ts diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index d55fc09c3..2f6e492e0 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -521,14 +521,39 @@ const models: TsoaRoute.Models = { "enums": ["STANDARD","GUARDIAN","DEPENDENT_AGE","DEPENDENT_OTHER"], }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "FirstName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MiddleName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "LastName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Mobile": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Email": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":254}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "AlternativeContact": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true}, - "middleName": {"dataType":"string"}, - "lastName": {"dataType":"string","required":true}, - "mobile": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, - "email": {"dataType":"string","required":true}, + "firstName": {"ref":"FirstName","required":true}, + "middleName": {"ref":"MiddleName"}, + "lastName": {"ref":"LastName","required":true}, + "mobile": {"dataType":"union","subSchemas":[{"ref":"Mobile"},{"dataType":"enum","enums":[null]}]}, + "email": {"ref":"Email","required":true}, }, "additionalProperties": false, }, @@ -554,12 +579,17 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DoB": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"isDate":{"errorMsg":"Date of birth must be YYYY-MM-DD format"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "OnBehalf": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true}, - "lastName": {"dataType":"string","required":true}, - "dob": {"dataType":"string","required":true}, + "firstName": {"ref":"FirstName","required":true}, + "lastName": {"ref":"LastName","required":true}, + "dob": {"ref":"DoB","required":true}, "permanent": {"dataType":"boolean","required":true}, }, "additionalProperties": false, @@ -567,7 +597,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Partial_RegisterParticipantRequest_": { "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"pattern":{"errorMsg":"please provide valid email","value":"^(.+)@(.+)$"}}},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"password":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"errorMsg":"Password must be at least 14 characters","value":14}}},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"isDate":{"errorMsg":"Date of birth must be of date format"}}},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"dependents":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"}},{"dataType":"undefined"}]},"externalId":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]}},"validators":{}}, + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"password":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"dependents":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"}},{"dataType":"undefined"}],"validators":{"maxItems":{"value":15}}},"externalId":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"maxLength":{"value":255}}}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UpdateProfileRequest": { @@ -670,21 +700,21 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Email": { + "Partial_GetParticipantProfileResponse-at-data_": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"pattern":{"errorMsg":"Please provide valid email","value":"^(.+)@(.+)$"}}}, + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"id":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]},"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"familyMembers":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"}},{"dataType":"undefined"}]},"familyId":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Partial_GetParticipantProfileResponse-at-data_": { + "ExternalId": { "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"id":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]},"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"familyMembers":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"}},{"dataType":"undefined"}]},"familyId":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]}},"validators":{}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Prefill": { "dataType": "refObject", "properties": { "profile": {"ref":"Partial_GetParticipantProfileResponse-at-data_"}, - "studyParticipant": {"dataType":"nestedObjectLiteral","nestedProperties":{"externalId":{"dataType":"string"}}}, + "studyParticipant": {"dataType":"nestedObjectLiteral","nestedProperties":{"externalId":{"ref":"ExternalId"}}}, }, "additionalProperties": false, }, @@ -858,15 +888,25 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Password": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":14},"maxLength":{"value":128}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "RoleT": { + "dataType": "refAlias", + "type": {"ref":"Role","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RegisterRequest": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "middleName": {"dataType":"string","validators":{"minLength":{"value":1}}}, - "lastName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"Please provide valid email","value":"^(.+)@(.+)$"}}}, - "password": {"dataType":"string","required":true,"validators":{"minLength":{"errorMsg":"Password must be at least 14 characters","value":14}}}, - "role": {"ref":"Role","required":true}, + "firstName": {"ref":"FirstName","required":true}, + "middleName": {"ref":"MiddleName"}, + "lastName": {"ref":"LastName","required":true}, + "email": {"ref":"Email","required":true}, + "password": {"ref":"Password","required":true}, + "role": {"ref":"RoleT","required":true}, }, "additionalProperties": false, }, @@ -900,25 +940,40 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "AddressLine": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Suburb": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Postcode": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"pattern":{"value":"^\\d{4}$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RegisterParticipantRequest": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "middleName": {"dataType":"string","validators":{"minLength":{"value":1}}}, - "lastName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"please provide valid email","value":"^(.+)@(.+)$"}}}, - "mobile": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, + "firstName": {"ref":"FirstName","required":true}, + "middleName": {"ref":"MiddleName"}, + "lastName": {"ref":"LastName","required":true}, + "email": {"ref":"Email","required":true}, + "mobile": {"ref":"Mobile","required":true}, "preferredContact": {"ref":"ContactMethod","required":true}, - "addressLine": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "suburb": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "postcode": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, + "addressLine": {"ref":"AddressLine","required":true}, + "suburb": {"ref":"Suburb","required":true}, + "postcode": {"ref":"Postcode","required":true}, "state": {"ref":"StateTerritory","required":true}, - "password": {"dataType":"string","required":true,"validators":{"minLength":{"errorMsg":"Password must be at least 14 characters","value":14}}}, - "dob": {"dataType":"string","required":true,"validators":{"isDate":{"errorMsg":"Date of birth must be of date format"}}}, + "password": {"ref":"Password","required":true}, + "dob": {"ref":"DoB","required":true}, "participantType": {"ref":"ParticipantType","required":true}, "nextOfKin": {"ref":"AlternativeContact","required":true}, - "dependents": {"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"},"required":true}, - "externalId": {"dataType":"string"}, + "dependents": {"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"},"required":true,"validators":{"maxItems":{"value":15}}}, + "externalId": {"ref":"ExternalId","validators":{"maxLength":{"value":255}}}, }, "additionalProperties": false, }, @@ -928,7 +983,7 @@ const models: TsoaRoute.Models = { "properties": { "token": {"dataType":"string","required":true}, "id": {"dataType":"double","required":true}, - "role": {"dataType":"string","required":true}, + "role": {"ref":"RoleT","required":true}, }, "additionalProperties": false, }, @@ -959,8 +1014,8 @@ const models: TsoaRoute.Models = { "LoginRequest": { "dataType": "refObject", "properties": { - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"Please provide valid email","value":"^(.+)@(.+)$"}}}, - "password": {"dataType":"string","required":true}, + "email": {"ref":"Email","required":true}, + "password": {"ref":"Password","required":true}, }, "additionalProperties": false, }, diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 86634f451..472940710 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -1309,23 +1309,59 @@ ], "type": "string" }, + "FirstName": { + "type": "string", + "example": "John", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "MiddleName": { + "type": "string", + "example": "William", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "LastName": { + "type": "string", + "example": "Doe", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "Mobile": { + "type": "string", + "example": "0412341432", + "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" + }, + "Email": { + "type": "string", + "example": "john.doe@email.com", + "minLength": 1, + "maxLength": 254 + }, "AlternativeContact": { "properties": { "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "middleName": { - "type": "string" + "$ref": "#/components/schemas/MiddleName" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "mobile": { - "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/Mobile" + } + ], "nullable": true }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" } }, "required": [ @@ -1446,16 +1482,20 @@ "type": "object", "additionalProperties": false }, + "DoB": { + "type": "string", + "example": "2000-05-21" + }, "OnBehalf": { "properties": { "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "permanent": { "type": "boolean" @@ -1473,46 +1513,37 @@ "Partial_RegisterParticipantRequest_": { "properties": { "firstName": { - "type": "string", - "minLength": 1 + "type": "string" }, "middleName": { - "type": "string", - "minLength": 1 + "type": "string" }, "lastName": { - "type": "string", - "minLength": 1 + "type": "string" }, "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "type": "string" }, "mobile": { - "type": "string", - "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" + "type": "string" }, "preferredContact": { "$ref": "#/components/schemas/ContactMethod" }, "addressLine": { - "type": "string", - "minLength": 1 + "type": "string" }, "suburb": { - "type": "string", - "minLength": 1 + "type": "string" }, "postcode": { - "type": "string", - "minLength": 1 + "type": "string" }, "state": { "$ref": "#/components/schemas/StateTerritory" }, "password": { - "type": "string", - "minLength": 14 + "type": "string" }, "dob": { "type": "string" @@ -1527,10 +1558,12 @@ "items": { "$ref": "#/components/schemas/OnBehalf" }, - "type": "array" + "type": "array", + "maxItems": 15 }, "externalId": { - "type": "string" + "type": "string", + "maxLength": 255 } }, "type": "object", @@ -1969,10 +2002,6 @@ "type": "object", "additionalProperties": false }, - "Email": { - "type": "string", - "pattern": "^(.+)@(.+)$" - }, "Partial_GetParticipantProfileResponse-at-data_": { "properties": { "id": { @@ -2032,6 +2061,13 @@ "type": "object", "description": "Make all properties in T optional" }, + "ExternalId": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" + }, "Prefill": { "properties": { "profile": { @@ -2040,7 +2076,7 @@ "studyParticipant": { "properties": { "externalId": { - "type": "string" + "$ref": "#/components/schemas/ExternalId" } }, "type": "object" @@ -2487,30 +2523,35 @@ "type": "object", "additionalProperties": false }, + "Password": { + "type": "string", + "example": "Supersecret123", + "minLength": 14, + "maxLength": 128 + }, + "RoleT": { + "$ref": "#/components/schemas/Role", + "example": "OrganisationAdmin" + }, "RegisterRequest": { "properties": { "firstName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/FirstName" }, "middleName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/MiddleName" }, "lastName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/LastName" }, "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "password": { - "type": "string", - "minLength": 14 + "$ref": "#/components/schemas/Password" }, "role": { - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/RoleT" } }, "required": [ @@ -2521,14 +2562,7 @@ "role" ], "type": "object", - "additionalProperties": false, - "example": { - "firstName": "John", - "lastName": "Doe", - "email": "john.doe@email.com", - "password": "Supersecret123", - "role": "OrganisationAdmin" - } + "additionalProperties": false }, "SetupResponse": { "properties": { @@ -2628,52 +2662,62 @@ "type": "object", "additionalProperties": false }, + "AddressLine": { + "type": "string", + "example": "123 Sydney Street", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "Suburb": { + "type": "string", + "example": "Darlinghurst", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "Postcode": { + "type": "string", + "example": "1234", + "pattern": "^\\d{4}$" + }, "RegisterParticipantRequest": { "properties": { "firstName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/FirstName" }, "middleName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/MiddleName" }, "lastName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/LastName" }, "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "mobile": { - "type": "string", - "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" + "$ref": "#/components/schemas/Mobile" }, "preferredContact": { "$ref": "#/components/schemas/ContactMethod" }, "addressLine": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/AddressLine" }, "suburb": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/Suburb" }, "postcode": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/Postcode" }, "state": { "$ref": "#/components/schemas/StateTerritory" }, "password": { - "type": "string", - "minLength": 14 + "$ref": "#/components/schemas/Password" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "participantType": { "$ref": "#/components/schemas/ParticipantType" @@ -2685,10 +2729,11 @@ "items": { "$ref": "#/components/schemas/OnBehalf" }, - "type": "array" + "type": "array", + "maxItems": 15 }, "externalId": { - "type": "string" + "$ref": "#/components/schemas/ExternalId" } }, "required": [ @@ -2708,30 +2753,7 @@ "dependents" ], "type": "object", - "additionalProperties": false, - "example": { - "firstName": "John", - "middleName": "James", - "lastName": "Doe", - "email": "john.doe@email.com", - "password": "Supersecret123", - "dob": "2000-05-21", - "mobile": "0412341234", - "addressLine": "123 Sydney Street", - "suburb": "Sydney", - "postcode": "2000", - "state": "NSW", - "participantType": "STANDARD", - "preferredContact": "MOBILE", - "nextOfKin": { - "firstName": "Jeremy", - "middleName": "Jimmy", - "lastName": "Doe", - "mobile": "0412341432", - "email": "jeremydoe@email.com" - }, - "dependents": [] - } + "additionalProperties": false }, "LoginSuccessResponse": { "properties": { @@ -2743,7 +2765,7 @@ "format": "double" }, "role": { - "type": "string" + "$ref": "#/components/schemas/RoleT" } }, "required": [ @@ -2799,11 +2821,10 @@ "LoginRequest": { "properties": { "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "password": { - "type": "string" + "$ref": "#/components/schemas/Password" } }, "required": [ @@ -2811,11 +2832,7 @@ "password" ], "type": "object", - "additionalProperties": false, - "example": { - "email": "john.doe@email.com", - "password": "Supersecret123" - } + "additionalProperties": false }, "OTPLoginRequest": { "properties": { diff --git a/application/common/types/api/auth/login.ts b/application/common/types/api/auth/login.ts index d7c9913a5..67df688ef 100644 --- a/application/common/types/api/auth/login.ts +++ b/application/common/types/api/auth/login.ts @@ -1,15 +1,8 @@ -/** - * @example { - * "email": "john.doe@email.com", - * "password": "Supersecret123" - * } - */ +import { Email, Password, RoleT } from '../../commonTypes' + export interface LoginRequest { - /** - * @pattern ^(.+)@(.+)$ Please provide valid email - */ - email: string - password: string + email: Email + password: Password } export interface OTPLoginRequest { @@ -26,7 +19,7 @@ export interface OIDCLoginRequest { export interface LoginSuccessResponse { token: string id: number - role: string + role: RoleT } export interface LoginChallengeResponse { diff --git a/application/common/types/api/auth/register.ts b/application/common/types/api/auth/register.ts index dbafdb647..74c2aeef0 100644 --- a/application/common/types/api/auth/register.ts +++ b/application/common/types/api/auth/register.ts @@ -1,35 +1,12 @@ -import { Role } from '@prisma/client' -/** - * @example { - * "firstName": "John", - * "lastName": "Doe", - * "email": "john.doe@email.com", - * "password": "Supersecret123", - * "role": "OrganisationAdmin" - * } - */ +import { Email, FirstName, LastName, MiddleName, Password, RoleT } from '../../commonTypes' + export interface RegisterRequest { - /** - * @minLength 1 - */ - firstName: string - /** - * @minLength 1 - */ - middleName?: string - /** - * @minLength 1 - */ - lastName: string - /** - * @pattern ^(.+)@(.+)$ Please provide valid email - */ - email: string - /** - * @minLength 14 Password must be at least 14 characters - */ - password: string - role: Role + firstName: FirstName + middleName?: MiddleName + lastName: LastName + email: Email + password: Password + role: RoleT } export interface RegisterResponse { diff --git a/application/common/types/api/auth/registerParticipant.ts b/application/common/types/api/auth/registerParticipant.ts index 07829947c..abb779941 100644 --- a/application/common/types/api/auth/registerParticipant.ts +++ b/application/common/types/api/auth/registerParticipant.ts @@ -1,3 +1,16 @@ +import { + AddressLine, + DoB, + Email, + ExternalId, + FirstName, + LastName, + MiddleName, + Mobile, + Password, + Postcode, + Suburb, +} from '../../commonTypes' import type { AlternativeContact, ContactMethod, @@ -6,81 +19,29 @@ import type { OnBehalf, } from '../users/ParticipantProfile' -/** - * @example { - * "firstName": "John", - * "middleName": "James", - * "lastName": "Doe", - * "email": "john.doe@email.com", - * "password": "Supersecret123", - * "dob": "2000-05-21", - * "mobile": "0412341234", - * "addressLine": "123 Sydney Street", - * "suburb": "Sydney", - * "postcode": "2000", - * "state": "NSW", - * "participantType": "STANDARD", - * "preferredContact": "MOBILE", - * "nextOfKin": { - * "firstName": "Jeremy", - * "middleName": "Jimmy", - * "lastName": "Doe", - * "mobile": "0412341432", - * "email": "jeremydoe@email.com" - * }, - * "dependents": [] - * } - */ export interface RegisterParticipantRequest { - /** - * @minLength 1 - */ - firstName: string - /** - * @minLength 1 - */ - middleName?: string - /** - * @minLength 1 - */ - lastName: string - /** - * @pattern ^(.+)@(.+)$ please provide valid email - */ - email: string - /** - * @pattern ^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ please provide valid phone number - */ - mobile: string + firstName: FirstName + middleName?: MiddleName + lastName: LastName + email: Email + mobile: Mobile preferredContact: ContactMethod - /** - * @minLength 1 - */ - addressLine: string - /** - * @minLength 1 - */ - suburb: string - /** - * @minLength 1 - */ - postcode: string + addressLine: AddressLine + suburb: Suburb + postcode: Postcode state: StateTerritory + password: Password + dob: DoB + participantType: ParticipantType + nextOfKin: AlternativeContact /** - * @minLength 14 Password must be at least 14 characters - */ - password: string - /** - * @isDate Date of birth must be of date format + * @maxItems 15 */ - dob: string + dependents: OnBehalf[] /** - * @isBool + * @maxLength 255 */ - participantType: ParticipantType - nextOfKin: AlternativeContact - dependents: OnBehalf[] - externalId?: string + externalId?: ExternalId } export interface RegisterParticipantResponse { diff --git a/application/common/types/api/users/ParticipantProfile.ts b/application/common/types/api/users/ParticipantProfile.ts index 8a51e1989..bbaf28ef8 100644 --- a/application/common/types/api/users/ParticipantProfile.ts +++ b/application/common/types/api/users/ParticipantProfile.ts @@ -1,3 +1,5 @@ +import { DoB, Email, FirstName, LastName, MiddleName, Mobile } from '../../commonTypes' + export enum ContactMethod { EMAIL = 'EMAIL', MOBILE = 'MOBILE', @@ -23,16 +25,16 @@ export enum ParticipantType { } export interface AlternativeContact { - firstName: string - middleName?: string - lastName: string - mobile?: string | null - email: string + firstName: FirstName + middleName?: MiddleName + lastName: LastName + mobile?: Mobile | null + email: Email } export interface OnBehalf { - firstName: string - lastName: string - dob: string + firstName: FirstName + lastName: LastName + dob: DoB permanent: boolean } diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts new file mode 100644 index 000000000..b9f120e6b --- /dev/null +++ b/application/common/types/commonTypes.ts @@ -0,0 +1,90 @@ +// Common base types to be defined once here and used in many other places in `api/` +// +// Note: see docs for explanation of character class patterns (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) +import { Role } from '@prisma/client' + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[\p{L}\s\-\'\.]+$ + * @example "John" + */ +export type FirstName = string + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[\p{L}\s\-\'\.]+$ + * @example "William" + */ +export type MiddleName = string + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[\p{L}\s\-\'\.]+$ + * @example "Doe" + */ +export type LastName = string + +/** + * @minLength 1 + * @maxLength 254 + * @isEmail + * @example "john.doe@email.com" + */ +export type Email = string + +/** + * @minLength 14 + * @maxLength 128 + * @example "Supersecret123" + */ +export type Password = string + +/** + * @example "OrganisationAdmin" + */ +export type RoleT = Role + +/** + * @pattern ^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ please provide valid phone number + * @example "0412341432" + */ +export type Mobile = string + +/** + * @minLength 1 + * @maxLength 128 + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "123 Sydney Street" + */ +export type AddressLine = string + +/** + * @minLength 1 + * @maxLength 128 + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Darlinghurst" + */ +export type Suburb = string + +/** + * @pattern ^\d{4}$ + * @example "1234" + */ +export type Postcode = string + +/** + * @isDate Date of birth must be YYYY-MM-DD format + * @example "2000-05-21" + */ +export type DoB = string + +/** + * @minLength 1 + * @maxLength 128 + * @pattern ^[a-zA-Z0-9\-_=\.:]+$ + * @example "123e4567-e89b-12d3-a456-426614174000" + */ +export type ExternalId = string diff --git a/application/common/types/invite.ts b/application/common/types/invite.ts index 5e6a3d712..b3c98cbcb 100644 --- a/application/common/types/invite.ts +++ b/application/common/types/invite.ts @@ -1,14 +1,10 @@ import { GetParticipantProfileResponse } from './api/users' - -/** - * @pattern ^(.+)@(.+)$ Please provide valid email - */ -type Email = string +import { Email, ExternalId } from './commonTypes' export interface Prefill { profile?: Partial studyParticipant?: { - externalId?: string + externalId?: ExternalId } } From 93944beec9ac8da205494840d421c4c0c2340884 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 29 Jul 2026 15:46:09 +1200 Subject: [PATCH 02/36] WIP start dry type constraint refactor --- application/backend/src/routes.ts | 125 +++++++--- application/backend/swagger.json | 229 ++++++++++-------- application/common/types/api/auth/login.ts | 17 +- application/common/types/api/auth/register.ts | 39 +-- .../types/api/auth/registerParticipant.ts | 97 +++----- .../types/api/users/ParticipantProfile.ts | 18 +- application/common/types/commonTypes.ts | 90 +++++++ application/common/types/invite.ts | 8 +- 8 files changed, 357 insertions(+), 266 deletions(-) create mode 100644 application/common/types/commonTypes.ts diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index d55fc09c3..2f6e492e0 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -521,14 +521,39 @@ const models: TsoaRoute.Models = { "enums": ["STANDARD","GUARDIAN","DEPENDENT_AGE","DEPENDENT_OTHER"], }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "FirstName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MiddleName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "LastName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Mobile": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Email": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":254}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "AlternativeContact": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true}, - "middleName": {"dataType":"string"}, - "lastName": {"dataType":"string","required":true}, - "mobile": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, - "email": {"dataType":"string","required":true}, + "firstName": {"ref":"FirstName","required":true}, + "middleName": {"ref":"MiddleName"}, + "lastName": {"ref":"LastName","required":true}, + "mobile": {"dataType":"union","subSchemas":[{"ref":"Mobile"},{"dataType":"enum","enums":[null]}]}, + "email": {"ref":"Email","required":true}, }, "additionalProperties": false, }, @@ -554,12 +579,17 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DoB": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"isDate":{"errorMsg":"Date of birth must be YYYY-MM-DD format"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "OnBehalf": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true}, - "lastName": {"dataType":"string","required":true}, - "dob": {"dataType":"string","required":true}, + "firstName": {"ref":"FirstName","required":true}, + "lastName": {"ref":"LastName","required":true}, + "dob": {"ref":"DoB","required":true}, "permanent": {"dataType":"boolean","required":true}, }, "additionalProperties": false, @@ -567,7 +597,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Partial_RegisterParticipantRequest_": { "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"pattern":{"errorMsg":"please provide valid email","value":"^(.+)@(.+)$"}}},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"value":1}}},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"password":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"minLength":{"errorMsg":"Password must be at least 14 characters","value":14}}},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"isDate":{"errorMsg":"Date of birth must be of date format"}}},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"dependents":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"}},{"dataType":"undefined"}]},"externalId":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]}},"validators":{}}, + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"password":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"dependents":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"}},{"dataType":"undefined"}],"validators":{"maxItems":{"value":15}}},"externalId":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"maxLength":{"value":255}}}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UpdateProfileRequest": { @@ -670,21 +700,21 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Email": { + "Partial_GetParticipantProfileResponse-at-data_": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"pattern":{"errorMsg":"Please provide valid email","value":"^(.+)@(.+)$"}}}, + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"id":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]},"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"familyMembers":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"}},{"dataType":"undefined"}]},"familyId":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Partial_GetParticipantProfileResponse-at-data_": { + "ExternalId": { "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"id":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]},"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"familyMembers":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"}},{"dataType":"undefined"}]},"familyId":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]}},"validators":{}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Prefill": { "dataType": "refObject", "properties": { "profile": {"ref":"Partial_GetParticipantProfileResponse-at-data_"}, - "studyParticipant": {"dataType":"nestedObjectLiteral","nestedProperties":{"externalId":{"dataType":"string"}}}, + "studyParticipant": {"dataType":"nestedObjectLiteral","nestedProperties":{"externalId":{"ref":"ExternalId"}}}, }, "additionalProperties": false, }, @@ -858,15 +888,25 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Password": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":14},"maxLength":{"value":128}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "RoleT": { + "dataType": "refAlias", + "type": {"ref":"Role","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RegisterRequest": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "middleName": {"dataType":"string","validators":{"minLength":{"value":1}}}, - "lastName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"Please provide valid email","value":"^(.+)@(.+)$"}}}, - "password": {"dataType":"string","required":true,"validators":{"minLength":{"errorMsg":"Password must be at least 14 characters","value":14}}}, - "role": {"ref":"Role","required":true}, + "firstName": {"ref":"FirstName","required":true}, + "middleName": {"ref":"MiddleName"}, + "lastName": {"ref":"LastName","required":true}, + "email": {"ref":"Email","required":true}, + "password": {"ref":"Password","required":true}, + "role": {"ref":"RoleT","required":true}, }, "additionalProperties": false, }, @@ -900,25 +940,40 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "AddressLine": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Suburb": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Postcode": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"pattern":{"value":"^\\d{4}$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RegisterParticipantRequest": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "middleName": {"dataType":"string","validators":{"minLength":{"value":1}}}, - "lastName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"please provide valid email","value":"^(.+)@(.+)$"}}}, - "mobile": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, + "firstName": {"ref":"FirstName","required":true}, + "middleName": {"ref":"MiddleName"}, + "lastName": {"ref":"LastName","required":true}, + "email": {"ref":"Email","required":true}, + "mobile": {"ref":"Mobile","required":true}, "preferredContact": {"ref":"ContactMethod","required":true}, - "addressLine": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "suburb": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "postcode": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, + "addressLine": {"ref":"AddressLine","required":true}, + "suburb": {"ref":"Suburb","required":true}, + "postcode": {"ref":"Postcode","required":true}, "state": {"ref":"StateTerritory","required":true}, - "password": {"dataType":"string","required":true,"validators":{"minLength":{"errorMsg":"Password must be at least 14 characters","value":14}}}, - "dob": {"dataType":"string","required":true,"validators":{"isDate":{"errorMsg":"Date of birth must be of date format"}}}, + "password": {"ref":"Password","required":true}, + "dob": {"ref":"DoB","required":true}, "participantType": {"ref":"ParticipantType","required":true}, "nextOfKin": {"ref":"AlternativeContact","required":true}, - "dependents": {"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"},"required":true}, - "externalId": {"dataType":"string"}, + "dependents": {"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"},"required":true,"validators":{"maxItems":{"value":15}}}, + "externalId": {"ref":"ExternalId","validators":{"maxLength":{"value":255}}}, }, "additionalProperties": false, }, @@ -928,7 +983,7 @@ const models: TsoaRoute.Models = { "properties": { "token": {"dataType":"string","required":true}, "id": {"dataType":"double","required":true}, - "role": {"dataType":"string","required":true}, + "role": {"ref":"RoleT","required":true}, }, "additionalProperties": false, }, @@ -959,8 +1014,8 @@ const models: TsoaRoute.Models = { "LoginRequest": { "dataType": "refObject", "properties": { - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"Please provide valid email","value":"^(.+)@(.+)$"}}}, - "password": {"dataType":"string","required":true}, + "email": {"ref":"Email","required":true}, + "password": {"ref":"Password","required":true}, }, "additionalProperties": false, }, diff --git a/application/backend/swagger.json b/application/backend/swagger.json index c7297b140..07b42be37 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -1302,23 +1302,59 @@ ], "type": "string" }, + "FirstName": { + "type": "string", + "example": "John", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "MiddleName": { + "type": "string", + "example": "William", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "LastName": { + "type": "string", + "example": "Doe", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "Mobile": { + "type": "string", + "example": "0412341432", + "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" + }, + "Email": { + "type": "string", + "example": "john.doe@email.com", + "minLength": 1, + "maxLength": 254 + }, "AlternativeContact": { "properties": { "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "middleName": { - "type": "string" + "$ref": "#/components/schemas/MiddleName" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "mobile": { - "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/Mobile" + } + ], "nullable": true }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" } }, "required": [ @@ -1439,16 +1475,20 @@ "type": "object", "additionalProperties": false }, + "DoB": { + "type": "string", + "example": "2000-05-21" + }, "OnBehalf": { "properties": { "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "permanent": { "type": "boolean" @@ -1466,46 +1506,37 @@ "Partial_RegisterParticipantRequest_": { "properties": { "firstName": { - "type": "string", - "minLength": 1 + "type": "string" }, "middleName": { - "type": "string", - "minLength": 1 + "type": "string" }, "lastName": { - "type": "string", - "minLength": 1 + "type": "string" }, "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "type": "string" }, "mobile": { - "type": "string", - "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" + "type": "string" }, "preferredContact": { "$ref": "#/components/schemas/ContactMethod" }, "addressLine": { - "type": "string", - "minLength": 1 + "type": "string" }, "suburb": { - "type": "string", - "minLength": 1 + "type": "string" }, "postcode": { - "type": "string", - "minLength": 1 + "type": "string" }, "state": { "$ref": "#/components/schemas/StateTerritory" }, "password": { - "type": "string", - "minLength": 14 + "type": "string" }, "dob": { "type": "string" @@ -1520,10 +1551,12 @@ "items": { "$ref": "#/components/schemas/OnBehalf" }, - "type": "array" + "type": "array", + "maxItems": 15 }, "externalId": { - "type": "string" + "type": "string", + "maxLength": 255 } }, "type": "object", @@ -1962,10 +1995,6 @@ "type": "object", "additionalProperties": false }, - "Email": { - "type": "string", - "pattern": "^(.+)@(.+)$" - }, "Partial_GetParticipantProfileResponse-at-data_": { "properties": { "id": { @@ -2025,6 +2054,13 @@ "type": "object", "description": "Make all properties in T optional" }, + "ExternalId": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" + }, "Prefill": { "properties": { "profile": { @@ -2033,7 +2069,7 @@ "studyParticipant": { "properties": { "externalId": { - "type": "string" + "$ref": "#/components/schemas/ExternalId" } }, "type": "object" @@ -2480,30 +2516,35 @@ "type": "object", "additionalProperties": false }, + "Password": { + "type": "string", + "example": "Supersecret123", + "minLength": 14, + "maxLength": 128 + }, + "RoleT": { + "$ref": "#/components/schemas/Role", + "example": "OrganisationAdmin" + }, "RegisterRequest": { "properties": { "firstName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/FirstName" }, "middleName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/MiddleName" }, "lastName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/LastName" }, "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "password": { - "type": "string", - "minLength": 14 + "$ref": "#/components/schemas/Password" }, "role": { - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/RoleT" } }, "required": [ @@ -2514,14 +2555,7 @@ "role" ], "type": "object", - "additionalProperties": false, - "example": { - "firstName": "John", - "lastName": "Doe", - "email": "john.doe@email.com", - "password": "Supersecret123", - "role": "OrganisationAdmin" - } + "additionalProperties": false }, "SetupResponse": { "properties": { @@ -2621,52 +2655,62 @@ "type": "object", "additionalProperties": false }, + "AddressLine": { + "type": "string", + "example": "123 Sydney Street", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "Suburb": { + "type": "string", + "example": "Darlinghurst", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "Postcode": { + "type": "string", + "example": "1234", + "pattern": "^\\d{4}$" + }, "RegisterParticipantRequest": { "properties": { "firstName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/FirstName" }, "middleName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/MiddleName" }, "lastName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/LastName" }, "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "mobile": { - "type": "string", - "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" + "$ref": "#/components/schemas/Mobile" }, "preferredContact": { "$ref": "#/components/schemas/ContactMethod" }, "addressLine": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/AddressLine" }, "suburb": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/Suburb" }, "postcode": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/Postcode" }, "state": { "$ref": "#/components/schemas/StateTerritory" }, "password": { - "type": "string", - "minLength": 14 + "$ref": "#/components/schemas/Password" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "participantType": { "$ref": "#/components/schemas/ParticipantType" @@ -2678,10 +2722,11 @@ "items": { "$ref": "#/components/schemas/OnBehalf" }, - "type": "array" + "type": "array", + "maxItems": 15 }, "externalId": { - "type": "string" + "$ref": "#/components/schemas/ExternalId" } }, "required": [ @@ -2701,30 +2746,7 @@ "dependents" ], "type": "object", - "additionalProperties": false, - "example": { - "firstName": "John", - "middleName": "James", - "lastName": "Doe", - "email": "john.doe@email.com", - "password": "Supersecret123", - "dob": "2000-05-21", - "mobile": "0412341234", - "addressLine": "123 Sydney Street", - "suburb": "Sydney", - "postcode": "2000", - "state": "NSW", - "participantType": "STANDARD", - "preferredContact": "MOBILE", - "nextOfKin": { - "firstName": "Jeremy", - "middleName": "Jimmy", - "lastName": "Doe", - "mobile": "0412341432", - "email": "jeremydoe@email.com" - }, - "dependents": [] - } + "additionalProperties": false }, "LoginSuccessResponse": { "properties": { @@ -2736,7 +2758,7 @@ "format": "double" }, "role": { - "type": "string" + "$ref": "#/components/schemas/RoleT" } }, "required": [ @@ -2792,11 +2814,10 @@ "LoginRequest": { "properties": { "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "password": { - "type": "string" + "$ref": "#/components/schemas/Password" } }, "required": [ @@ -2804,11 +2825,7 @@ "password" ], "type": "object", - "additionalProperties": false, - "example": { - "email": "john.doe@email.com", - "password": "Supersecret123" - } + "additionalProperties": false }, "OTPLoginRequest": { "properties": { diff --git a/application/common/types/api/auth/login.ts b/application/common/types/api/auth/login.ts index d7c9913a5..67df688ef 100644 --- a/application/common/types/api/auth/login.ts +++ b/application/common/types/api/auth/login.ts @@ -1,15 +1,8 @@ -/** - * @example { - * "email": "john.doe@email.com", - * "password": "Supersecret123" - * } - */ +import { Email, Password, RoleT } from '../../commonTypes' + export interface LoginRequest { - /** - * @pattern ^(.+)@(.+)$ Please provide valid email - */ - email: string - password: string + email: Email + password: Password } export interface OTPLoginRequest { @@ -26,7 +19,7 @@ export interface OIDCLoginRequest { export interface LoginSuccessResponse { token: string id: number - role: string + role: RoleT } export interface LoginChallengeResponse { diff --git a/application/common/types/api/auth/register.ts b/application/common/types/api/auth/register.ts index dbafdb647..74c2aeef0 100644 --- a/application/common/types/api/auth/register.ts +++ b/application/common/types/api/auth/register.ts @@ -1,35 +1,12 @@ -import { Role } from '@prisma/client' -/** - * @example { - * "firstName": "John", - * "lastName": "Doe", - * "email": "john.doe@email.com", - * "password": "Supersecret123", - * "role": "OrganisationAdmin" - * } - */ +import { Email, FirstName, LastName, MiddleName, Password, RoleT } from '../../commonTypes' + export interface RegisterRequest { - /** - * @minLength 1 - */ - firstName: string - /** - * @minLength 1 - */ - middleName?: string - /** - * @minLength 1 - */ - lastName: string - /** - * @pattern ^(.+)@(.+)$ Please provide valid email - */ - email: string - /** - * @minLength 14 Password must be at least 14 characters - */ - password: string - role: Role + firstName: FirstName + middleName?: MiddleName + lastName: LastName + email: Email + password: Password + role: RoleT } export interface RegisterResponse { diff --git a/application/common/types/api/auth/registerParticipant.ts b/application/common/types/api/auth/registerParticipant.ts index 07829947c..abb779941 100644 --- a/application/common/types/api/auth/registerParticipant.ts +++ b/application/common/types/api/auth/registerParticipant.ts @@ -1,3 +1,16 @@ +import { + AddressLine, + DoB, + Email, + ExternalId, + FirstName, + LastName, + MiddleName, + Mobile, + Password, + Postcode, + Suburb, +} from '../../commonTypes' import type { AlternativeContact, ContactMethod, @@ -6,81 +19,29 @@ import type { OnBehalf, } from '../users/ParticipantProfile' -/** - * @example { - * "firstName": "John", - * "middleName": "James", - * "lastName": "Doe", - * "email": "john.doe@email.com", - * "password": "Supersecret123", - * "dob": "2000-05-21", - * "mobile": "0412341234", - * "addressLine": "123 Sydney Street", - * "suburb": "Sydney", - * "postcode": "2000", - * "state": "NSW", - * "participantType": "STANDARD", - * "preferredContact": "MOBILE", - * "nextOfKin": { - * "firstName": "Jeremy", - * "middleName": "Jimmy", - * "lastName": "Doe", - * "mobile": "0412341432", - * "email": "jeremydoe@email.com" - * }, - * "dependents": [] - * } - */ export interface RegisterParticipantRequest { - /** - * @minLength 1 - */ - firstName: string - /** - * @minLength 1 - */ - middleName?: string - /** - * @minLength 1 - */ - lastName: string - /** - * @pattern ^(.+)@(.+)$ please provide valid email - */ - email: string - /** - * @pattern ^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ please provide valid phone number - */ - mobile: string + firstName: FirstName + middleName?: MiddleName + lastName: LastName + email: Email + mobile: Mobile preferredContact: ContactMethod - /** - * @minLength 1 - */ - addressLine: string - /** - * @minLength 1 - */ - suburb: string - /** - * @minLength 1 - */ - postcode: string + addressLine: AddressLine + suburb: Suburb + postcode: Postcode state: StateTerritory + password: Password + dob: DoB + participantType: ParticipantType + nextOfKin: AlternativeContact /** - * @minLength 14 Password must be at least 14 characters - */ - password: string - /** - * @isDate Date of birth must be of date format + * @maxItems 15 */ - dob: string + dependents: OnBehalf[] /** - * @isBool + * @maxLength 255 */ - participantType: ParticipantType - nextOfKin: AlternativeContact - dependents: OnBehalf[] - externalId?: string + externalId?: ExternalId } export interface RegisterParticipantResponse { diff --git a/application/common/types/api/users/ParticipantProfile.ts b/application/common/types/api/users/ParticipantProfile.ts index 8a51e1989..bbaf28ef8 100644 --- a/application/common/types/api/users/ParticipantProfile.ts +++ b/application/common/types/api/users/ParticipantProfile.ts @@ -1,3 +1,5 @@ +import { DoB, Email, FirstName, LastName, MiddleName, Mobile } from '../../commonTypes' + export enum ContactMethod { EMAIL = 'EMAIL', MOBILE = 'MOBILE', @@ -23,16 +25,16 @@ export enum ParticipantType { } export interface AlternativeContact { - firstName: string - middleName?: string - lastName: string - mobile?: string | null - email: string + firstName: FirstName + middleName?: MiddleName + lastName: LastName + mobile?: Mobile | null + email: Email } export interface OnBehalf { - firstName: string - lastName: string - dob: string + firstName: FirstName + lastName: LastName + dob: DoB permanent: boolean } diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts new file mode 100644 index 000000000..b9f120e6b --- /dev/null +++ b/application/common/types/commonTypes.ts @@ -0,0 +1,90 @@ +// Common base types to be defined once here and used in many other places in `api/` +// +// Note: see docs for explanation of character class patterns (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) +import { Role } from '@prisma/client' + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[\p{L}\s\-\'\.]+$ + * @example "John" + */ +export type FirstName = string + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[\p{L}\s\-\'\.]+$ + * @example "William" + */ +export type MiddleName = string + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[\p{L}\s\-\'\.]+$ + * @example "Doe" + */ +export type LastName = string + +/** + * @minLength 1 + * @maxLength 254 + * @isEmail + * @example "john.doe@email.com" + */ +export type Email = string + +/** + * @minLength 14 + * @maxLength 128 + * @example "Supersecret123" + */ +export type Password = string + +/** + * @example "OrganisationAdmin" + */ +export type RoleT = Role + +/** + * @pattern ^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ please provide valid phone number + * @example "0412341432" + */ +export type Mobile = string + +/** + * @minLength 1 + * @maxLength 128 + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "123 Sydney Street" + */ +export type AddressLine = string + +/** + * @minLength 1 + * @maxLength 128 + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Darlinghurst" + */ +export type Suburb = string + +/** + * @pattern ^\d{4}$ + * @example "1234" + */ +export type Postcode = string + +/** + * @isDate Date of birth must be YYYY-MM-DD format + * @example "2000-05-21" + */ +export type DoB = string + +/** + * @minLength 1 + * @maxLength 128 + * @pattern ^[a-zA-Z0-9\-_=\.:]+$ + * @example "123e4567-e89b-12d3-a456-426614174000" + */ +export type ExternalId = string diff --git a/application/common/types/invite.ts b/application/common/types/invite.ts index 5e6a3d712..b3c98cbcb 100644 --- a/application/common/types/invite.ts +++ b/application/common/types/invite.ts @@ -1,14 +1,10 @@ import { GetParticipantProfileResponse } from './api/users' - -/** - * @pattern ^(.+)@(.+)$ Please provide valid email - */ -type Email = string +import { Email, ExternalId } from './commonTypes' export interface Prefill { profile?: Partial studyParticipant?: { - externalId?: string + externalId?: ExternalId } } From 5be124d836b5093a7f853143999d513d11da7888 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Tue, 4 Aug 2026 17:25:10 +1200 Subject: [PATCH 03/36] Refactor user types --- .../types/api/auth/registerParticipant.ts | 3 +- .../common/types/api/users/createUser.ts | 30 +++------------ .../common/types/api/users/getAllUsers.ts | 4 +- .../types/api/users/getParticipantProfile.ts | 38 ++++++++++++------- .../common/types/api/users/passwordReset.ts | 23 ++--------- .../common/types/api/users/updateProfile.ts | 24 ------------ .../common/types/api/users/updateUser.ts | 28 +++----------- .../common/types/api/users/updateUserRole.ts | 9 +---- application/common/types/commonTypes.ts | 14 ++++--- 9 files changed, 54 insertions(+), 119 deletions(-) diff --git a/application/common/types/api/auth/registerParticipant.ts b/application/common/types/api/auth/registerParticipant.ts index abb779941..e02f853bf 100644 --- a/application/common/types/api/auth/registerParticipant.ts +++ b/application/common/types/api/auth/registerParticipant.ts @@ -9,6 +9,7 @@ import { Mobile, Password, Postcode, + RoleT, Suburb, } from '../../commonTypes' import type { @@ -47,5 +48,5 @@ export interface RegisterParticipantRequest { export interface RegisterParticipantResponse { id: number token: string - role: string + role: RoleT } diff --git a/application/common/types/api/users/createUser.ts b/application/common/types/api/users/createUser.ts index 094e96151..1d110522a 100644 --- a/application/common/types/api/users/createUser.ts +++ b/application/common/types/api/users/createUser.ts @@ -1,30 +1,10 @@ -import { Role } from '@prisma/client' +import { Email, FirstName, LastName, RoleT } from '../../commonTypes' -/** - * @example { - * "firstName": "John", - * "lastName": "Doe", - * "email": "john.doe@email.com", - * "role": "Participant" - * } - */ export interface CreateUserRequest { - /** - * @minLength 1 - */ - firstName: string - /** - * @minLength 1 - */ - lastName: string - /** - * @pattern ^(.+)@(.+)$ please provide valid email - */ - email: string - /** - * @minLength 8 - */ - role: Role + firstName: FirstName + lastName: LastName + email: Email + role: RoleT } export interface CreateUserResponse { diff --git a/application/common/types/api/users/getAllUsers.ts b/application/common/types/api/users/getAllUsers.ts index 8bee5d3af..aa9994ee1 100644 --- a/application/common/types/api/users/getAllUsers.ts +++ b/application/common/types/api/users/getAllUsers.ts @@ -1,6 +1,6 @@ -import { User } from '@prisma/client' +import { UserT } from '../../commonTypes' -export type UserResponse = Omit +export type UserResponse = Omit export interface GetAllUsersResponse { data: UserResponse[] diff --git a/application/common/types/api/users/getParticipantProfile.ts b/application/common/types/api/users/getParticipantProfile.ts index 9ca74fb7c..b278d40eb 100644 --- a/application/common/types/api/users/getParticipantProfile.ts +++ b/application/common/types/api/users/getParticipantProfile.ts @@ -1,3 +1,15 @@ +import { + AddressLine, + DoB, + Email, + FirstName, + LastName, + MiddleName, + Mobile, + Postcode, + Suburb, +} from '../../commonTypes' + import { AlternativeContact, ContactMethod, @@ -6,10 +18,10 @@ import { } from './ParticipantProfile' export interface FamilyMember { - firstName: string - middleName?: string - lastName: string - dob: string + firstName: FirstName + middleName?: MiddleName + lastName: LastName + dob: DoB id: number participantType: ParticipantType } @@ -17,16 +29,16 @@ export interface FamilyMember { export interface GetParticipantProfileResponse { data: { id: number - firstName: string - middleName?: string - lastName: string - dob: string - email?: string - mobile: string - addressLine?: string - suburb?: string + firstName: FirstName + middleName?: MiddleName + lastName: LastName + dob: DoB + email?: Email + mobile: Mobile + addressLine?: AddressLine + suburb?: Suburb state?: StateTerritory - postcode?: string + postcode?: Postcode preferredContact: ContactMethod participantType: ParticipantType nextOfKin?: AlternativeContact diff --git a/application/common/types/api/users/passwordReset.ts b/application/common/types/api/users/passwordReset.ts index 74c599df8..f241c8f7b 100644 --- a/application/common/types/api/users/passwordReset.ts +++ b/application/common/types/api/users/passwordReset.ts @@ -1,25 +1,10 @@ -/** - * @example { - * "email": "john.doe@email.com" - * } - */ +import { Email, Password } from '../../commonTypes' + export interface GeneratePasswordResetLinkRequest { - /** - * @pattern ^(.+)@(.+)$ please provide valid email - */ - email: string + email: Email } -/** - * @example { - * "newPassword": "Newsupersecret123", - * "token": "1063e00e4a273e698577" - * } - */ export interface ResetPasswordRequest { - /** - * @minLength 14 Password must be at least 14 characters - */ - newPassword: string + newPassword: Password token: string } diff --git a/application/common/types/api/users/updateProfile.ts b/application/common/types/api/users/updateProfile.ts index 61ffe31d0..9617392ca 100644 --- a/application/common/types/api/users/updateProfile.ts +++ b/application/common/types/api/users/updateProfile.ts @@ -1,27 +1,3 @@ import { RegisterParticipantRequest } from '../auth' -/** - * @example { - * "firstName": "John", - * "middleName": "James", - * "lastName": "Doe", - * "email": "john.doe@email.com", - * "password": "Supersecret123", - * "dob": "2000-05-21", - * "mobile": "0412341234", - * "addressLine": "123 Sydney Street", - * "suburb": "Sydney", - * "postcode": "2000", - * "state": "NSW", - * "participantType": "STANDARD", - * "preferredContact": "MOBILE", - * "nextOfKin": { - * "firstName": "Jeremy", - * "middleName": "Jimmy", - * "lastName": "Doe", - * "mobile": "0412341432", - * "email": "jeremydoe@email.com" - * } - * } - */ export type UpdateProfileRequest = Partial diff --git a/application/common/types/api/users/updateUser.ts b/application/common/types/api/users/updateUser.ts index 5677d8db7..aca7b4124 100644 --- a/application/common/types/api/users/updateUser.ts +++ b/application/common/types/api/users/updateUser.ts @@ -1,24 +1,8 @@ -import { Role } from '@prisma/client' -/** - * @example { - * "firstName": "John", - * "lastName": "Doe", - * "email": "john.doe@email.com", - * "role": "User" - * } - */ +import { Email, FirstName, LastName, RoleT } from '../../commonTypes' + export interface UpdateUserRequest { - /** - * @minLength 1 - */ - firstName?: string - /** - * @minLength 1 - */ - lastName?: string - /** - * @pattern ^(.+)@(.+)$ please provide valid email - */ - email?: string - role?: Role + firstName?: FirstName + lastName?: LastName + email?: Email + role?: RoleT } diff --git a/application/common/types/api/users/updateUserRole.ts b/application/common/types/api/users/updateUserRole.ts index 4bcf992a3..819bb4b55 100644 --- a/application/common/types/api/users/updateUserRole.ts +++ b/application/common/types/api/users/updateUserRole.ts @@ -1,10 +1,5 @@ -import type { Role } from '@prisma/client' +import { RoleT } from '../../commonTypes' -/** - * @example { - * "newRole": "OperatorAdmin" - * } - */ export interface UpdateUserRoleRequest { - newRole: Role + newRole: RoleT } diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index b9f120e6b..0bc870f67 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -1,7 +1,7 @@ // Common base types to be defined once here and used in many other places in `api/` // // Note: see docs for explanation of character class patterns (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) -import { Role } from '@prisma/client' +import { Role, User } from '@prisma/client' /** * @minLength 1 @@ -42,11 +42,6 @@ export type Email = string */ export type Password = string -/** - * @example "OrganisationAdmin" - */ -export type RoleT = Role - /** * @pattern ^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ please provide valid phone number * @example "0412341432" @@ -88,3 +83,10 @@ export type DoB = string * @example "123e4567-e89b-12d3-a456-426614174000" */ export type ExternalId = string + +/** + * @example "OrganisationAdmin" + */ +export type RoleT = Role + +export type UserT = User From 2ef5a8d7d4b0e4a7e5968646cf474a66a23bb515 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 12:29:28 +1200 Subject: [PATCH 04/36] Make survey and study types more dry --- application/backend/src/routes.ts | 182 +++++----- application/backend/swagger.json | 317 ++++++++---------- .../common/types/api/studies/createStudy.ts | 12 +- .../common/types/api/studies/updateStudy.ts | 20 +- .../types/api/surveys/getAllResponses.ts | 3 +- application/common/types/commonTypes.ts | 32 ++ application/common/types/survey.ts | 2 +- 7 files changed, 291 insertions(+), 277 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 2f6e492e0..1aff1407f 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -94,19 +94,19 @@ const models: TsoaRoute.Models = { "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["OperatorAdmin"]},{"dataType":"enum","enums":["Participant"]},{"dataType":"enum","enums":["OrganisationAdmin"]},{"dataType":"enum","enums":["StudyAdmin"]}],"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Pick_User.Exclude_keyofUser.password-or-emailHash__": { + "Pick_UserT.Exclude_keyofUserT.password-or-emailHash__": { "dataType": "refAlias", "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"id":{"dataType":"double","required":true},"firstName":{"dataType":"string","required":true},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"lastName":{"dataType":"string","required":true},"email":{"dataType":"string","required":true},"role":{"ref":"_36_Enums.Role","required":true},"createdAt":{"dataType":"datetime","required":true},"updatedAt":{"dataType":"datetime","required":true},"agreedTermsAt":{"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"enum","enums":[null]}],"required":true},"lockedUntil":{"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"enum","enums":[null]}],"required":true},"retriesRemaining":{"dataType":"double","required":true},"deleted":{"dataType":"boolean","required":true}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Omit_User.password-or-emailHash_": { + "Omit_UserT.password-or-emailHash_": { "dataType": "refAlias", - "type": {"ref":"Pick_User.Exclude_keyofUser.password-or-emailHash__","validators":{}}, + "type": {"ref":"Pick_UserT.Exclude_keyofUserT.password-or-emailHash__","validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UserResponse": { "dataType": "refAlias", - "type": {"ref":"Omit_User.password-or-emailHash_","validators":{}}, + "type": {"ref":"Omit_UserT.password-or-emailHash_","validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "GetAllUsersResponse": { @@ -142,18 +142,38 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "FirstName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "LastName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Email": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":254}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Role": { "dataType": "refAlias", "type": {"ref":"_36_Enums.Role","validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "RoleT": { + "dataType": "refAlias", + "type": {"ref":"Role","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "CreateUserRequest": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "lastName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"please provide valid email","value":"^(.+)@(.+)$"}}}, - "role": {"ref":"Role","required":true,"validators":{"minLength":{"value":8}}}, + "firstName": {"ref":"FirstName","required":true}, + "lastName": {"ref":"LastName","required":true}, + "email": {"ref":"Email","required":true}, + "role": {"ref":"RoleT","required":true}, }, "additionalProperties": false, }, @@ -161,10 +181,10 @@ const models: TsoaRoute.Models = { "UpdateUserRequest": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","validators":{"minLength":{"value":1}}}, - "lastName": {"dataType":"string","validators":{"minLength":{"value":1}}}, - "email": {"dataType":"string","validators":{"pattern":{"errorMsg":"please provide valid email","value":"^(.+)@(.+)$"}}}, - "role": {"ref":"Role"}, + "firstName": {"ref":"FirstName"}, + "lastName": {"ref":"LastName"}, + "email": {"ref":"Email"}, + "role": {"ref":"RoleT"}, }, "additionalProperties": false, }, @@ -172,7 +192,7 @@ const models: TsoaRoute.Models = { "UpdateUserRoleRequest": { "dataType": "refObject", "properties": { - "newRole": {"ref":"Role","required":true}, + "newRole": {"ref":"RoleT","required":true}, }, "additionalProperties": false, }, @@ -180,15 +200,20 @@ const models: TsoaRoute.Models = { "GeneratePasswordResetLinkRequest": { "dataType": "refObject", "properties": { - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"please provide valid email","value":"^(.+)@(.+)$"}}}, + "email": {"ref":"Email","required":true}, }, "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Password": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":14},"maxLength":{"value":128}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ResetPasswordRequest": { "dataType": "refObject", "properties": { - "newPassword": {"dataType":"string","required":true,"validators":{"minLength":{"errorMsg":"Password must be at least 14 characters","value":14}}}, + "newPassword": {"ref":"Password","required":true}, "token": {"dataType":"string","required":true}, }, "additionalProperties": false, @@ -347,6 +372,11 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DoB": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"isDate":{"errorMsg":"Date of birth must be YYYY-MM-DD format"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyStepAnswerArray": { "dataType": "refAlias", "type": {"dataType":"array","array":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"boolean"},{"dataType":"enum","enums":[null]}]},"validators":{}}, @@ -365,7 +395,7 @@ const models: TsoaRoute.Models = { "ParticipantData": { "dataType": "refObject", "properties": { - "profile": {"dataType":"nestedObjectLiteral","nestedProperties":{"familyId":{"dataType":"double","required":true},"dob":{"dataType":"string","required":true},"lastName":{"dataType":"string","required":true},"firstName":{"dataType":"string","required":true}},"required":true}, + "profile": {"dataType":"nestedObjectLiteral","nestedProperties":{"familyId":{"dataType":"double","required":true},"dob":{"ref":"DoB","required":true},"lastName":{"ref":"LastName","required":true},"firstName":{"ref":"FirstName","required":true}},"required":true}, "answers": {"dataType":"array","array":{"dataType":"refObject","ref":"UserSurveyStepState"},"required":true}, "versionId": {"dataType":"double","required":true}, "participantId": {"dataType":"string","required":true}, @@ -460,22 +490,42 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "StudyName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "CreateStudyRequest": { "dataType": "refObject", "properties": { - "name": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, + "name": {"ref":"StudyName","required":true}, }, "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "StudyDescription": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "RedcapToken": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Url": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UpdateStudyRequest": { "dataType": "refObject", "properties": { - "name": {"dataType":"string","validators":{"minLength":{"value":1}}}, - "description": {"dataType":"string"}, - "redcapToken": {"dataType":"string"}, - "redcapURL": {"dataType":"string"}, - "contactUsEmail": {"dataType":"string"}, + "name": {"ref":"StudyName"}, + "description": {"ref":"StudyDescription"}, + "redcapToken": {"ref":"RedcapToken"}, + "redcapURL": {"ref":"Url"}, + "contactUsEmail": {"ref":"Email"}, }, "additionalProperties": false, }, @@ -506,44 +556,44 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "StateTerritory": { - "dataType": "refEnum", - "enums": ["ACT","NSW","NT","QLD","SA","TAS","VIC","WA"], + "MiddleName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "ContactMethod": { - "dataType": "refEnum", - "enums": ["EMAIL","MOBILE","MAIL"], + "Mobile": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "ParticipantType": { - "dataType": "refEnum", - "enums": ["STANDARD","GUARDIAN","DEPENDENT_AGE","DEPENDENT_OTHER"], + "AddressLine": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "FirstName": { + "Suburb": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "MiddleName": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + "StateTerritory": { + "dataType": "refEnum", + "enums": ["ACT","NSW","NT","QLD","SA","TAS","VIC","WA"], }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "LastName": { + "Postcode": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + "type": {"dataType":"string","validators":{"pattern":{"value":"^\\d{4}$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Mobile": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, + "ContactMethod": { + "dataType": "refEnum", + "enums": ["EMAIL","MOBILE","MAIL"], }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Email": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":254}}}, + "ParticipantType": { + "dataType": "refEnum", + "enums": ["STANDARD","GUARDIAN","DEPENDENT_AGE","DEPENDENT_OTHER"], }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "AlternativeContact": { @@ -561,10 +611,10 @@ const models: TsoaRoute.Models = { "FamilyMember": { "dataType": "refObject", "properties": { - "firstName": {"dataType":"string","required":true}, - "middleName": {"dataType":"string"}, - "lastName": {"dataType":"string","required":true}, - "dob": {"dataType":"string","required":true}, + "firstName": {"ref":"FirstName","required":true}, + "middleName": {"ref":"MiddleName"}, + "lastName": {"ref":"LastName","required":true}, + "dob": {"ref":"DoB","required":true}, "id": {"dataType":"double","required":true}, "participantType": {"ref":"ParticipantType","required":true}, }, @@ -574,16 +624,11 @@ const models: TsoaRoute.Models = { "GetParticipantProfileResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"nestedObjectLiteral","nestedProperties":{"familyId":{"dataType":"double","required":true},"familyMembers":{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"},"required":true},"nextOfKin":{"ref":"AlternativeContact"},"participantType":{"ref":"ParticipantType","required":true},"preferredContact":{"ref":"ContactMethod","required":true},"postcode":{"dataType":"string"},"state":{"ref":"StateTerritory"},"suburb":{"dataType":"string"},"addressLine":{"dataType":"string"},"mobile":{"dataType":"string","required":true},"email":{"dataType":"string"},"dob":{"dataType":"string","required":true},"lastName":{"dataType":"string","required":true},"middleName":{"dataType":"string"},"firstName":{"dataType":"string","required":true},"id":{"dataType":"double","required":true}},"required":true}, + "data": {"dataType":"nestedObjectLiteral","nestedProperties":{"familyId":{"dataType":"double","required":true},"familyMembers":{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"},"required":true},"nextOfKin":{"ref":"AlternativeContact"},"participantType":{"ref":"ParticipantType","required":true},"preferredContact":{"ref":"ContactMethod","required":true},"postcode":{"ref":"Postcode"},"state":{"ref":"StateTerritory"},"suburb":{"ref":"Suburb"},"addressLine":{"ref":"AddressLine"},"mobile":{"ref":"Mobile","required":true},"email":{"ref":"Email"},"dob":{"ref":"DoB","required":true},"lastName":{"ref":"LastName","required":true},"middleName":{"ref":"MiddleName"},"firstName":{"ref":"FirstName","required":true},"id":{"dataType":"double","required":true}},"required":true}, }, "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "DoB": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"isDate":{"errorMsg":"Date of birth must be YYYY-MM-DD format"}}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "OnBehalf": { "dataType": "refObject", "properties": { @@ -650,7 +695,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ParticipantWithProfile": { "dataType": "refAlias", - "type": {"dataType":"intersection","subSchemas":[{"ref":"Participant"},{"dataType":"nestedObjectLiteral","nestedProperties":{"profile":{"dataType":"nestedObjectLiteral","nestedProperties":{"familyId":{"dataType":"double","required":true},"familyMembers":{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"},"required":true},"nextOfKin":{"ref":"AlternativeContact"},"participantType":{"ref":"ParticipantType","required":true},"preferredContact":{"ref":"ContactMethod","required":true},"postcode":{"dataType":"string"},"state":{"ref":"StateTerritory"},"suburb":{"dataType":"string"},"addressLine":{"dataType":"string"},"mobile":{"dataType":"string","required":true},"email":{"dataType":"string"},"dob":{"dataType":"string","required":true},"lastName":{"dataType":"string","required":true},"middleName":{"dataType":"string"},"firstName":{"dataType":"string","required":true},"id":{"dataType":"double","required":true}},"required":true}}}],"validators":{}}, + "type": {"dataType":"intersection","subSchemas":[{"ref":"Participant"},{"dataType":"nestedObjectLiteral","nestedProperties":{"profile":{"dataType":"nestedObjectLiteral","nestedProperties":{"familyId":{"dataType":"double","required":true},"familyMembers":{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"},"required":true},"nextOfKin":{"ref":"AlternativeContact"},"participantType":{"ref":"ParticipantType","required":true},"preferredContact":{"ref":"ContactMethod","required":true},"postcode":{"ref":"Postcode"},"state":{"ref":"StateTerritory"},"suburb":{"ref":"Suburb"},"addressLine":{"ref":"AddressLine"},"mobile":{"ref":"Mobile","required":true},"email":{"ref":"Email"},"dob":{"ref":"DoB","required":true},"lastName":{"ref":"LastName","required":true},"middleName":{"ref":"MiddleName"},"firstName":{"ref":"FirstName","required":true},"id":{"dataType":"double","required":true}},"required":true}}}],"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "GetParticipantResponse": { @@ -888,16 +933,6 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Password": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":14},"maxLength":{"value":128}}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "RoleT": { - "dataType": "refAlias", - "type": {"ref":"Role","validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RegisterRequest": { "dataType": "refObject", "properties": { @@ -935,26 +970,11 @@ const models: TsoaRoute.Models = { "properties": { "id": {"dataType":"double","required":true}, "token": {"dataType":"string","required":true}, - "role": {"dataType":"string","required":true}, + "role": {"ref":"RoleT","required":true}, }, "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "AddressLine": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Suburb": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Postcode": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"pattern":{"value":"^\\d{4}$"}}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RegisterParticipantRequest": { "dataType": "refObject", "properties": { diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 07b42be37..03e09e966 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -113,7 +113,7 @@ "type": "object", "additionalProperties": false }, - "Pick_User.Exclude_keyofUser.password-or-emailHash__": { + "Pick_UserT.Exclude_keyofUserT.password-or-emailHash__": { "properties": { "id": { "type": "number", @@ -178,12 +178,12 @@ "type": "object", "description": "From T, pick a set of properties whose keys are in the union K" }, - "Omit_User.password-or-emailHash_": { - "$ref": "#/components/schemas/Pick_User.Exclude_keyofUser.password-or-emailHash__", + "Omit_UserT.password-or-emailHash_": { + "$ref": "#/components/schemas/Pick_UserT.Exclude_keyofUserT.password-or-emailHash__", "description": "Construct a type with the properties of T except for those in type K." }, "UserResponse": { - "$ref": "#/components/schemas/Omit_User.password-or-emailHash_" + "$ref": "#/components/schemas/Omit_UserT.password-or-emailHash_" }, "GetAllUsersResponse": { "properties": { @@ -244,6 +244,26 @@ "type": "object", "additionalProperties": false }, + "FirstName": { + "type": "string", + "example": "John", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "LastName": { + "type": "string", + "example": "Doe", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "Email": { + "type": "string", + "example": "john.doe@email.com", + "minLength": 1, + "maxLength": 254 + }, "Role": { "type": "string", "enum": [ @@ -253,22 +273,23 @@ "StudyAdmin" ] }, + "RoleT": { + "$ref": "#/components/schemas/Role", + "example": "OrganisationAdmin" + }, "CreateUserRequest": { "properties": { "firstName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/FirstName" }, "lastName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/LastName" }, "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "role": { - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/RoleT" } }, "required": [ @@ -278,77 +299,60 @@ "role" ], "type": "object", - "additionalProperties": false, - "example": { - "firstName": "John", - "lastName": "Doe", - "email": "john.doe@email.com", - "role": "Participant" - } + "additionalProperties": false }, "UpdateUserRequest": { "properties": { "firstName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/FirstName" }, "lastName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/LastName" }, "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "role": { - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/RoleT" } }, "type": "object", - "additionalProperties": false, - "example": { - "firstName": "John", - "lastName": "Doe", - "email": "john.doe@email.com", - "role": "User" - } + "additionalProperties": false }, "UpdateUserRoleRequest": { "properties": { "newRole": { - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/RoleT" } }, "required": [ "newRole" ], "type": "object", - "additionalProperties": false, - "example": { - "newRole": "OperatorAdmin" - } + "additionalProperties": false }, "GeneratePasswordResetLinkRequest": { "properties": { "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" } }, "required": [ "email" ], "type": "object", - "additionalProperties": false, - "example": { - "email": "john.doe@email.com" - } + "additionalProperties": false + }, + "Password": { + "type": "string", + "example": "Supersecret123", + "minLength": 14, + "maxLength": 128 }, "ResetPasswordRequest": { "properties": { "newPassword": { - "type": "string", - "minLength": 14 + "$ref": "#/components/schemas/Password" }, "token": { "type": "string" @@ -359,11 +363,7 @@ "token" ], "type": "object", - "additionalProperties": false, - "example": { - "newPassword": "Newsupersecret123", - "token": "1063e00e4a273e698577" - } + "additionalProperties": false }, "SurveyVersionStatus": { "type": "string", @@ -798,6 +798,10 @@ "type": "object", "additionalProperties": false }, + "DoB": { + "type": "string", + "example": "2000-05-21" + }, "SurveyStepAnswerArray": { "items": { "anyOf": [ @@ -840,13 +844,13 @@ "format": "double" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" } }, "required": [ @@ -1142,11 +1146,17 @@ "type": "object", "additionalProperties": false }, + "StudyName": { + "type": "string", + "example": "Acme Genomics Study", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, "CreateStudyRequest": { "properties": { "name": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/StudyName" } }, "required": [ @@ -1155,23 +1165,42 @@ "type": "object", "additionalProperties": false }, + "StudyDescription": { + "type": "string", + "example": "This is a short description of the study", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "RedcapToken": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" + }, + "Url": { + "type": "string", + "example": "https://redcap.orgname.com/api/", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" + }, "UpdateStudyRequest": { "properties": { "name": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/StudyName" }, "description": { - "type": "string" + "$ref": "#/components/schemas/StudyDescription" }, "redcapToken": { - "type": "string" + "$ref": "#/components/schemas/RedcapToken" }, "redcapURL": { - "type": "string" + "$ref": "#/components/schemas/Url" }, "contactUsEmail": { - "type": "string" + "$ref": "#/components/schemas/Email" } }, "type": "object", @@ -1272,6 +1301,32 @@ "type": "object", "additionalProperties": false }, + "MiddleName": { + "type": "string", + "example": "William", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + }, + "Mobile": { + "type": "string", + "example": "0412341432", + "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" + }, + "AddressLine": { + "type": "string", + "example": "123 Sydney Street", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "Suburb": { + "type": "string", + "example": "Darlinghurst", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, "StateTerritory": { "enum": [ "ACT", @@ -1285,6 +1340,11 @@ ], "type": "string" }, + "Postcode": { + "type": "string", + "example": "1234", + "pattern": "^\\d{4}$" + }, "ContactMethod": { "enum": [ "EMAIL", @@ -1302,38 +1362,6 @@ ], "type": "string" }, - "FirstName": { - "type": "string", - "example": "John", - "minLength": 1, - "maxLength": 100, - "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" - }, - "MiddleName": { - "type": "string", - "example": "William", - "minLength": 1, - "maxLength": 100, - "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" - }, - "LastName": { - "type": "string", - "example": "Doe", - "minLength": 1, - "maxLength": 100, - "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" - }, - "Mobile": { - "type": "string", - "example": "0412341432", - "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" - }, - "Email": { - "type": "string", - "example": "john.doe@email.com", - "minLength": 1, - "maxLength": 254 - }, "AlternativeContact": { "properties": { "firstName": { @@ -1368,16 +1396,16 @@ "FamilyMember": { "properties": { "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "middleName": { - "type": "string" + "$ref": "#/components/schemas/MiddleName" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "id": { "type": "number", @@ -1421,34 +1449,34 @@ "$ref": "#/components/schemas/ContactMethod" }, "postcode": { - "type": "string" + "$ref": "#/components/schemas/Postcode" }, "state": { "$ref": "#/components/schemas/StateTerritory" }, "suburb": { - "type": "string" + "$ref": "#/components/schemas/Suburb" }, "addressLine": { - "type": "string" + "$ref": "#/components/schemas/AddressLine" }, "mobile": { - "type": "string" + "$ref": "#/components/schemas/Mobile" }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "middleName": { - "type": "string" + "$ref": "#/components/schemas/MiddleName" }, "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "id": { "type": "number", @@ -1475,10 +1503,6 @@ "type": "object", "additionalProperties": false }, - "DoB": { - "type": "string", - "example": "2000-05-21" - }, "OnBehalf": { "properties": { "firstName": { @@ -1563,29 +1587,7 @@ "description": "Make all properties in T optional" }, "UpdateProfileRequest": { - "$ref": "#/components/schemas/Partial_RegisterParticipantRequest_", - "example": { - "firstName": "John", - "middleName": "James", - "lastName": "Doe", - "email": "john.doe@email.com", - "password": "Supersecret123", - "dob": "2000-05-21", - "mobile": "0412341234", - "addressLine": "123 Sydney Street", - "suburb": "Sydney", - "postcode": "2000", - "state": "NSW", - "participantType": "STANDARD", - "preferredContact": "MOBILE", - "nextOfKin": { - "firstName": "Jeremy", - "middleName": "Jimmy", - "lastName": "Doe", - "mobile": "0412341432", - "email": "jeremydoe@email.com" - } - } + "$ref": "#/components/schemas/Partial_RegisterParticipantRequest_" }, "ParticipantAnswerStatus": { "properties": { @@ -1758,34 +1760,34 @@ "$ref": "#/components/schemas/ContactMethod" }, "postcode": { - "type": "string" + "$ref": "#/components/schemas/Postcode" }, "state": { "$ref": "#/components/schemas/StateTerritory" }, "suburb": { - "type": "string" + "$ref": "#/components/schemas/Suburb" }, "addressLine": { - "type": "string" + "$ref": "#/components/schemas/AddressLine" }, "mobile": { - "type": "string" + "$ref": "#/components/schemas/Mobile" }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "middleName": { - "type": "string" + "$ref": "#/components/schemas/MiddleName" }, "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "id": { "type": "number", @@ -2516,16 +2518,6 @@ "type": "object", "additionalProperties": false }, - "Password": { - "type": "string", - "example": "Supersecret123", - "minLength": 14, - "maxLength": 128 - }, - "RoleT": { - "$ref": "#/components/schemas/Role", - "example": "OrganisationAdmin" - }, "RegisterRequest": { "properties": { "firstName": { @@ -2644,7 +2636,7 @@ "type": "string" }, "role": { - "type": "string" + "$ref": "#/components/schemas/RoleT" } }, "required": [ @@ -2655,25 +2647,6 @@ "type": "object", "additionalProperties": false }, - "AddressLine": { - "type": "string", - "example": "123 Sydney Street", - "minLength": 1, - "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" - }, - "Suburb": { - "type": "string", - "example": "Darlinghurst", - "minLength": 1, - "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" - }, - "Postcode": { - "type": "string", - "example": "1234", - "pattern": "^\\d{4}$" - }, "RegisterParticipantRequest": { "properties": { "firstName": { diff --git a/application/common/types/api/studies/createStudy.ts b/application/common/types/api/studies/createStudy.ts index 773bc8f45..386d3e63b 100644 --- a/application/common/types/api/studies/createStudy.ts +++ b/application/common/types/api/studies/createStudy.ts @@ -1,13 +1,7 @@ -/** - * @example { - * "name": "Acme Genomics Study", - * } - */ +import { StudyName } from '../../commonTypes' + export interface CreateStudyRequest { - /** - * @minLength 1 - */ - name: string + name: StudyName } export interface CreateStudyResponse { diff --git a/application/common/types/api/studies/updateStudy.ts b/application/common/types/api/studies/updateStudy.ts index f19743029..73c408858 100644 --- a/application/common/types/api/studies/updateStudy.ts +++ b/application/common/types/api/studies/updateStudy.ts @@ -1,15 +1,9 @@ -/** - * @example { - * "name": "UpdatedStudyName", - * } - */ +import { Email, RedcapToken, StudyDescription, StudyName, Url } from '../../commonTypes' + export interface UpdateStudyRequest { - /** - * @minLength 1 - */ - name?: string - description?: string - redcapToken?: string - redcapURL?: string - contactUsEmail?: string + name?: StudyName + description?: StudyDescription + redcapToken?: RedcapToken + redcapURL?: Url + contactUsEmail?: Email } diff --git a/application/common/types/api/surveys/getAllResponses.ts b/application/common/types/api/surveys/getAllResponses.ts index 61f7a671e..f6f813c22 100644 --- a/application/common/types/api/surveys/getAllResponses.ts +++ b/application/common/types/api/surveys/getAllResponses.ts @@ -1,7 +1,8 @@ +import { DoB, FirstName, LastName } from '../../commonTypes' import { SurveyStep, UserSurveyStepState } from '../../survey' export interface ParticipantData { - profile: { firstName: string; lastName: string; dob: string; familyId: number } + profile: { firstName: FirstName; lastName: LastName; dob: DoB; familyId: number } answers: UserSurveyStepState[] versionId: number participantId: string diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 0bc870f67..c9c0b2c62 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -90,3 +90,35 @@ export type ExternalId = string export type RoleT = Role export type UserT = User + +/** + * @minLength 1 + * @maxLength 128 + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Acme Genomics Study" + */ +export type StudyName = string + +/** + * @minLength 1 + * @maxLength 128 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "This is a short description of the study" + */ +export type StudyDescription = string + +/** + * @minLength 1 + * @maxLength 128 // TODO: verify max length + * @pattern ^[a-zA-Z0-9\-_=\.:]+$ + * @example "123e4567-e89b-12d3-a456-426614174000" // TODO: improve example + */ +export type RedcapToken = string + +/** + * @minLength 1 + * @maxLength 128 // TODO: verify max length + * @pattern ^[a-zA-Z0-9\-_=\.:]+$ + * @example "https://redcap.orgname.com/api/" + */ +export type Url = string diff --git a/application/common/types/survey.ts b/application/common/types/survey.ts index c3855794e..cfaed0b67 100644 --- a/application/common/types/survey.ts +++ b/application/common/types/survey.ts @@ -1,5 +1,5 @@ export interface DuoCode { - code: string + code: string // List used codes? relatedAnswer: string | boolean } From 9090112a0690a25587aa7f62fc1836b124d7daaf Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 12:44:08 +1200 Subject: [PATCH 05/36] Make settings types more dry --- application/backend/src/routes.ts | 4 ++-- application/backend/swagger.json | 14 +++++++++++--- .../common/types/api/settings/getSettings.ts | 10 ++++++---- application/common/types/api/settings/getTheme.ts | 6 ++++-- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 1aff1407f..dcd8c230d 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -533,7 +533,7 @@ const models: TsoaRoute.Models = { "GetSettingsResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"nestedObjectLiteral","nestedProperties":{"newsLink":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"tcLink":{"dataType":"string","required":true},"secondaryColour":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"primaryColour":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"logoSet":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, + "data": {"dataType":"nestedObjectLiteral","nestedProperties":{"newsLink":{"dataType":"union","subSchemas":[{"ref":"Url"},{"dataType":"enum","enums":[null]}],"required":true},"tcLink":{"ref":"Url","required":true},"secondaryColour":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"primaryColour":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"logoSet":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, }, "additionalProperties": false, }, @@ -551,7 +551,7 @@ const models: TsoaRoute.Models = { "GetUserPortalSettingsResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"nestedObjectLiteral","nestedProperties":{"newsLink":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"secondaryColour":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"primaryColour":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, + "data": {"dataType":"nestedObjectLiteral","nestedProperties":{"newsLink":{"dataType":"union","subSchemas":[{"ref":"Url"},{"dataType":"enum","enums":[null]}],"required":true},"secondaryColour":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"primaryColour":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, }, "additionalProperties": false, }, diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 03e09e966..c961e1f52 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -1211,11 +1211,15 @@ "data": { "properties": { "newsLink": { - "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/Url" + } + ], "nullable": true }, "tcLink": { - "type": "string" + "$ref": "#/components/schemas/Url" }, "secondaryColour": { "type": "string", @@ -1275,7 +1279,11 @@ "data": { "properties": { "newsLink": { - "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/Url" + } + ], "nullable": true }, "secondaryColour": { diff --git a/application/common/types/api/settings/getSettings.ts b/application/common/types/api/settings/getSettings.ts index d23f8b1ba..663712cc6 100644 --- a/application/common/types/api/settings/getSettings.ts +++ b/application/common/types/api/settings/getSettings.ts @@ -1,9 +1,11 @@ +import { Url } from '../../commonTypes' + export interface GetSettingsResponse { data: { - logoSet: string | null - primaryColour: string | null + logoSet: string | null // TODO: add type? + primaryColour: string | null // TODO: add type? secondaryColour: string | null - tcLink: string - newsLink: string | null + tcLink: Url + newsLink: Url | null } } diff --git a/application/common/types/api/settings/getTheme.ts b/application/common/types/api/settings/getTheme.ts index 3133fab3a..a3c77936b 100644 --- a/application/common/types/api/settings/getTheme.ts +++ b/application/common/types/api/settings/getTheme.ts @@ -1,7 +1,9 @@ +import { Url } from '../../commonTypes' + export interface GetUserPortalSettingsResponse { data: { - primaryColour: string | null + primaryColour: string | null // TODO: add type? secondaryColour: string | null - newsLink: string | null + newsLink: Url | null } } From 904b7476dcc44032bf4bd27a3a99aa7936f1ee23 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 13:18:53 +1200 Subject: [PATCH 06/36] Make participant types more dry --- application/backend/src/routes.ts | 30 +++++++---- application/backend/swagger.json | 54 ++++++++++--------- .../participants/getDeletedParticipants.ts | 10 ++-- .../types/api/participants/getInviteText.ts | 6 ++- .../types/api/participants/getInvites.ts | 13 ++--- .../common/types/api/participants/invite.ts | 1 + .../api/participants/inviteParticipant.ts | 13 ++--- .../types/api/participants/participant.ts | 7 +-- .../api/participants/updateParticipant.ts | 26 --------- application/common/types/commonTypes.ts | 16 ++++++ 10 files changed, 91 insertions(+), 85 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index dcd8c230d..634f8bd41 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -666,9 +666,9 @@ const models: TsoaRoute.Models = { "id": {"dataType":"double","required":true}, "participantId": {"dataType":"string","required":true}, "externalId": {"dataType":"string"}, - "email": {"dataType":"string"}, - "firstName": {"dataType":"string","required":true}, - "lastName": {"dataType":"string","required":true}, + "email": {"ref":"Email"}, + "firstName": {"ref":"FirstName","required":true}, + "lastName": {"ref":"LastName","required":true}, "familyId": {"dataType":"double","required":true}, "answers": {"dataType":"array","array":{"dataType":"refObject","ref":"ParticipantAnswerStatus"},"required":true}, "lastUpdated": {"dataType":"string"}, @@ -688,7 +688,7 @@ const models: TsoaRoute.Models = { "GetDeletedParticipantsResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"studyId":{"dataType":"double","required":true},"study":{"dataType":"string","required":true},"dob":{"dataType":"string","required":true},"lastName":{"dataType":"string","required":true},"firstName":{"dataType":"string","required":true},"profileId":{"dataType":"double","required":true},"id":{"dataType":"string","required":true}}},"required":true}, + "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"studyId":{"dataType":"double","required":true},"study":{"ref":"StudyName","required":true},"dob":{"ref":"DoB","required":true},"lastName":{"ref":"LastName","required":true},"firstName":{"ref":"FirstName","required":true},"profileId":{"dataType":"double","required":true},"id":{"dataType":"string","required":true}}},"required":true}, }, "additionalProperties": false, }, @@ -714,7 +714,7 @@ const models: TsoaRoute.Models = { "GetUserInvitesResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"nestedObjectLiteral","nestedProperties":{"invites":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"description":{"dataType":"string"},"studyName":{"dataType":"string","required":true},"sentAt":{"dataType":"string"},"expiresAt":{"dataType":"string","required":true},"createdAt":{"dataType":"string","required":true},"studyId":{"dataType":"double","required":true},"email":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true}},"required":true}, + "data": {"dataType":"nestedObjectLiteral","nestedProperties":{"invites":{"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"description":{"ref":"StudyDescription"},"studyName":{"ref":"StudyName","required":true},"sentAt":{"dataType":"string"},"expiresAt":{"dataType":"string","required":true},"createdAt":{"dataType":"string","required":true},"studyId":{"dataType":"double","required":true},"email":{"ref":"Email","required":true},"id":{"dataType":"string","required":true}}},"required":true}},"required":true}, }, "additionalProperties": false, }, @@ -727,7 +727,7 @@ const models: TsoaRoute.Models = { "GetInvitesResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"inviteStatus":{"ref":"InviteStatus","required":true},"sentAt":{"dataType":"string"},"expiresAt":{"dataType":"string","required":true},"createdAt":{"dataType":"string","required":true},"studyId":{"dataType":"double","required":true},"email":{"dataType":"string","required":true},"id":{"dataType":"string","required":true}}},"required":true}, + "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"inviteStatus":{"ref":"InviteStatus","required":true},"sentAt":{"dataType":"string"},"expiresAt":{"dataType":"string","required":true},"createdAt":{"dataType":"string","required":true},"studyId":{"dataType":"double","required":true},"email":{"ref":"Email","required":true},"id":{"dataType":"string","required":true}}},"required":true}, }, "additionalProperties": false, }, @@ -773,12 +773,22 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "InviteEmailSubject": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "InviteEmailText": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "InviteParticipantsRequest": { "dataType": "refObject", "properties": { "recipients": {"dataType":"array","array":{"dataType":"refObject","ref":"Recipient"},"required":true}, - "subjectText": {"dataType":"string","required":true}, - "explanatoryText": {"dataType":"string","required":true}, + "subjectText": {"ref":"InviteEmailSubject","required":true}, + "explanatoryText": {"ref":"InviteEmailText","required":true}, }, "additionalProperties": false, }, @@ -786,8 +796,8 @@ const models: TsoaRoute.Models = { "GetInviteTextResponse": { "dataType": "refObject", "properties": { - "inviteEmailSubject": {"dataType":"string","required":true}, - "inviteEmailText": {"dataType":"string","required":true}, + "inviteEmailSubject": {"ref":"InviteEmailSubject","required":true}, + "inviteEmailText": {"ref":"InviteEmailText","required":true}, }, "additionalProperties": false, }, diff --git a/application/backend/swagger.json b/application/backend/swagger.json index c961e1f52..41a74d18a 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -1637,13 +1637,13 @@ "type": "string" }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" }, "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "familyId": { "type": "number", @@ -1700,16 +1700,16 @@ "format": "double" }, "study": { - "type": "string" + "$ref": "#/components/schemas/StudyName" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "profileId": { "type": "number", @@ -1858,10 +1858,10 @@ "items": { "properties": { "description": { - "type": "string" + "$ref": "#/components/schemas/StudyDescription" }, "studyName": { - "type": "string" + "$ref": "#/components/schemas/StudyName" }, "sentAt": { "type": "string" @@ -1877,7 +1877,7 @@ "format": "double" }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" }, "id": { "type": "string" @@ -1940,7 +1940,7 @@ "format": "double" }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" }, "id": { "type": "string" @@ -2104,6 +2104,20 @@ "type": "object", "additionalProperties": false }, + "InviteEmailSubject": { + "type": "string", + "example": "Invitation to CTRL - Dynamic Consent Platform", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "InviteEmailText": { + "type": "string", + "example": "You have been invited to register with CTRL dynamic consent platform", + "minLength": 1, + "maxLength": 900, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, "InviteParticipantsRequest": { "properties": { "recipients": { @@ -2113,10 +2127,10 @@ "type": "array" }, "subjectText": { - "type": "string" + "$ref": "#/components/schemas/InviteEmailSubject" }, "explanatoryText": { - "type": "string" + "$ref": "#/components/schemas/InviteEmailText" } }, "required": [ @@ -2125,23 +2139,15 @@ "explanatoryText" ], "type": "object", - "additionalProperties": false, - "example": { - "emails": [ - "john.doe@email.com", - "jane@email.com" - ], - "subjectText": "Invitation to Study", - "explanatoryText": "You have been invited to participate in this Study. Please click this link to provide consent." - } + "additionalProperties": false }, "GetInviteTextResponse": { "properties": { "inviteEmailSubject": { - "type": "string" + "$ref": "#/components/schemas/InviteEmailSubject" }, "inviteEmailText": { - "type": "string" + "$ref": "#/components/schemas/InviteEmailText" } }, "required": [ diff --git a/application/common/types/api/participants/getDeletedParticipants.ts b/application/common/types/api/participants/getDeletedParticipants.ts index e9774d0ad..c551950e4 100644 --- a/application/common/types/api/participants/getDeletedParticipants.ts +++ b/application/common/types/api/participants/getDeletedParticipants.ts @@ -1,11 +1,13 @@ +import { FirstName, LastName, DoB, StudyName } from '../../commonTypes' + export interface GetDeletedParticipantsResponse { data: { id: string profileId: number - firstName: string - lastName: string - dob: string - study: string + firstName: FirstName + lastName: LastName + dob: DoB + study: StudyName studyId: number }[] } diff --git a/application/common/types/api/participants/getInviteText.ts b/application/common/types/api/participants/getInviteText.ts index a1e026137..f59e1103a 100644 --- a/application/common/types/api/participants/getInviteText.ts +++ b/application/common/types/api/participants/getInviteText.ts @@ -1,4 +1,6 @@ +import { InviteEmailSubject, InviteEmailText } from '../../commonTypes' + export interface GetInviteTextResponse { - inviteEmailSubject: string - inviteEmailText: string + inviteEmailSubject: InviteEmailSubject + inviteEmailText: InviteEmailText } diff --git a/application/common/types/api/participants/getInvites.ts b/application/common/types/api/participants/getInvites.ts index 54698c2b1..86eec9f31 100644 --- a/application/common/types/api/participants/getInvites.ts +++ b/application/common/types/api/participants/getInvites.ts @@ -1,10 +1,11 @@ +import { Email, StudyName, StudyDescription } from '../../commonTypes' import { InviteStatus } from './invite' // import { FamilyMember } from '../users/getParticipantProfile' export interface GetInvitesResponse { data: { - id: string // String because this is a uuid - email: string + id: string // String because this is a uuid TODO: add UUID type? + email: Email studyId: number createdAt: string expiresAt: string @@ -16,14 +17,14 @@ export interface GetInvitesResponse { export interface GetUserInvitesResponse { data: { invites: { - id: string // String because this is a uuid - email: string + id: string // String because this is a uuid TODO: add UUID type? + email: Email studyId: number createdAt: string expiresAt: string sentAt?: string - studyName: string // may be other fields here in future - description?: string + studyName: StudyName + description?: StudyDescription }[] // dependents: FamilyMember[] } diff --git a/application/common/types/api/participants/invite.ts b/application/common/types/api/participants/invite.ts index bd9cbef5d..144c3c81d 100644 --- a/application/common/types/api/participants/invite.ts +++ b/application/common/types/api/participants/invite.ts @@ -1,3 +1,4 @@ +// TODO: Is this needed given enum from schema.prisma? export enum InviteStatus { PENDING = 'PENDING', ACCEPTED = 'ACCEPTED', diff --git a/application/common/types/api/participants/inviteParticipant.ts b/application/common/types/api/participants/inviteParticipant.ts index 733712cef..e8bc52d1f 100644 --- a/application/common/types/api/participants/inviteParticipant.ts +++ b/application/common/types/api/participants/inviteParticipant.ts @@ -1,17 +1,10 @@ +import { InviteEmailText, InviteEmailSubject } from '../../commonTypes' import { Recipient } from '../../invite' -/** - * - * @example { - * "emails": ["john.doe@email.com", "jane@email.com"], - * "subjectText": "Invitation to Study", - * "explanatoryText": "You have been invited to participate in this Study. Please click this link to provide consent." - * } - */ export interface InviteParticipantsRequest { recipients: Recipient[] - subjectText: string - explanatoryText: string + subjectText: InviteEmailSubject + explanatoryText: InviteEmailText } export interface InviteParticipantsResponse { diff --git a/application/common/types/api/participants/participant.ts b/application/common/types/api/participants/participant.ts index 267deacc6..a7629419c 100644 --- a/application/common/types/api/participants/participant.ts +++ b/application/common/types/api/participants/participant.ts @@ -1,12 +1,13 @@ +import { Email, FirstName, LastName } from '../../commonTypes' import { GetParticipantProfileResponse } from '../users' export interface Participant { id: number participantId: string externalId?: string - email?: string - firstName: string - lastName: string + email?: Email + firstName: FirstName + lastName: LastName familyId: number answers: ParticipantAnswerStatus[] lastUpdated?: string diff --git a/application/common/types/api/participants/updateParticipant.ts b/application/common/types/api/participants/updateParticipant.ts index 576935139..b2317492d 100644 --- a/application/common/types/api/participants/updateParticipant.ts +++ b/application/common/types/api/participants/updateParticipant.ts @@ -1,31 +1,5 @@ import { UpdateProfileRequest } from '../users/updateProfile' -/** - * @example { - * externalId: 123, - * profile: { - * "firstName": "John", - * "middleName": "James", - * "lastName": "Doe", - * "email": "john.doe@email.com", - * "password": "Supersecret123", - * "dob": "2000-05-21", - * "mobile": "0412341234", - * "addressLine": "123 Sydney Street", - * "suburb": "Sydney", - * "postcode": "2000", - * "state": "NSW", - * "participantType": "STANDARD", - * "preferredContact": "MOBILE", - * "nextOfKin": { - * "firstName": "Jeremy", - * "middleName": "Jimmy", - * "lastName": "Doe", - * "mobile": "0412341432", - * "email": "jeremydoe@email.com" - * } - * } - */ export type UpdateParticipantRequest = { externalId: string profile: UpdateProfileRequest diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index c9c0b2c62..5459cbc68 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -122,3 +122,19 @@ export type RedcapToken = string * @example "https://redcap.orgname.com/api/" */ export type Url = string + +/** + * @minLength 1 + * @maxLength 128 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Invitation to CTRL - Dynamic Consent Platform" + */ +export type InviteEmailSubject = string + +/** + * @minLength 1 + * @maxLength 900 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "You have been invited to register with CTRL dynamic consent platform" + */ +export type InviteEmailText = string From d67cd8ea58db6bb824dfe713da852b0ede78d2fe Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 13:36:58 +1200 Subject: [PATCH 07/36] Make organisation types more dry --- application/backend/src/routes.ts | 21 ++--- application/backend/swagger.json | 89 +++---------------- .../api/organisations/createOrganisation.ts | 12 +-- .../api/organisations/getOrganisationUsers.ts | 4 +- .../api/organisations/updateOrganisation.ts | 12 +-- application/common/types/commonTypes.ts | 9 ++ 6 files changed, 35 insertions(+), 112 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 634f8bd41..234f35621 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -836,10 +836,15 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "OrganisationName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "CreateOrganisationRequest": { "dataType": "refObject", "properties": { - "name": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, + "name": {"ref":"OrganisationName","required":true}, }, "additionalProperties": false, }, @@ -847,25 +852,15 @@ const models: TsoaRoute.Models = { "UpdateOrganisationRequest": { "dataType": "refObject", "properties": { - "name": {"dataType":"string","validators":{"minLength":{"value":1}}}, + "name": {"ref":"OrganisationName"}, }, "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "DefaultSelection_Prisma._36_UserPayload_": { - "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"deleted":{"dataType":"boolean","required":true},"retriesRemaining":{"dataType":"double","required":true},"lockedUntil":{"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"enum","enums":[null]}],"required":true},"agreedTermsAt":{"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"enum","enums":[null]}],"required":true},"updatedAt":{"dataType":"datetime","required":true},"createdAt":{"dataType":"datetime","required":true},"role":{"ref":"_36_Enums.Role","required":true},"password":{"dataType":"string","required":true},"emailHash":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"email":{"dataType":"string","required":true},"lastName":{"dataType":"string","required":true},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},"firstName":{"dataType":"string","required":true},"id":{"dataType":"double","required":true}},"validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "User": { - "dataType": "refAlias", - "type": {"ref":"DefaultSelection_Prisma._36_UserPayload_","validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "GetOrganisationUsersResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"array","array":{"dataType":"refAlias","ref":"User"},"required":true}, + "data": {"dataType":"array","array":{"dataType":"refAlias","ref":"UserResponse"},"required":true}, }, "additionalProperties": false, }, diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 41a74d18a..b4dce32e5 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -2254,11 +2254,17 @@ "type": "object", "additionalProperties": false }, + "OrganisationName": { + "type": "string", + "example": "Acme Medical Research Institute", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, "CreateOrganisationRequest": { "properties": { "name": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/OrganisationName" } }, "required": [ @@ -2270,92 +2276,17 @@ "UpdateOrganisationRequest": { "properties": { "name": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/OrganisationName" } }, "type": "object", "additionalProperties": false }, - "User": { - "description": "Model User", - "properties": { - "deleted": { - "type": "boolean" - }, - "retriesRemaining": { - "type": "number", - "format": "double" - }, - "lockedUntil": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "agreedTermsAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "role": { - "$ref": "#/components/schemas/_36_Enums.Role" - }, - "password": { - "type": "string" - }, - "emailHash": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "middleName": { - "type": "string", - "nullable": true - }, - "firstName": { - "type": "string" - }, - "id": { - "type": "number", - "format": "double" - } - }, - "required": [ - "deleted", - "retriesRemaining", - "lockedUntil", - "agreedTermsAt", - "updatedAt", - "createdAt", - "role", - "password", - "emailHash", - "email", - "lastName", - "middleName", - "firstName", - "id" - ], - "type": "object" - }, "GetOrganisationUsersResponse": { "properties": { "data": { "items": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/UserResponse" }, "type": "array" } diff --git a/application/common/types/api/organisations/createOrganisation.ts b/application/common/types/api/organisations/createOrganisation.ts index 72dc24bd6..0986b57a1 100644 --- a/application/common/types/api/organisations/createOrganisation.ts +++ b/application/common/types/api/organisations/createOrganisation.ts @@ -1,13 +1,7 @@ -/** - * @example { - * "name": "ABC. Corp", - * } - */ +import { OrganisationName } from '../../commonTypes' + export interface CreateOrganisationRequest { - /** - * @minLength 1 - */ - name: string + name: OrganisationName } export interface CreateOrganisationResponse { diff --git a/application/common/types/api/organisations/getOrganisationUsers.ts b/application/common/types/api/organisations/getOrganisationUsers.ts index 58dbedff6..f2777763a 100644 --- a/application/common/types/api/organisations/getOrganisationUsers.ts +++ b/application/common/types/api/organisations/getOrganisationUsers.ts @@ -1,5 +1,5 @@ -import { User } from '@prisma/client' +import { UserResponse } from '../users' export interface GetOrganisationUsersResponse { - data: User[] + data: UserResponse[] } diff --git a/application/common/types/api/organisations/updateOrganisation.ts b/application/common/types/api/organisations/updateOrganisation.ts index 774f38ed0..9fa5fb930 100644 --- a/application/common/types/api/organisations/updateOrganisation.ts +++ b/application/common/types/api/organisations/updateOrganisation.ts @@ -1,11 +1,5 @@ -/** - * @example { - * "name": "UpdatedOrganisationName", - * } - */ +import { OrganisationName } from '../../commonTypes' + export interface UpdateOrganisationRequest { - /** - * @minLength 1 - */ - name?: string + name?: OrganisationName } diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 5459cbc68..34054cad1 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -89,6 +89,7 @@ export type ExternalId = string */ export type RoleT = Role +// TODO: add annotations? export type UserT = User /** @@ -138,3 +139,11 @@ export type InviteEmailSubject = string * @example "You have been invited to register with CTRL dynamic consent platform" */ export type InviteEmailText = string + +/** + * @minLength 1 + * @maxLength 128 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Acme Medical Research Institute" + */ +export type OrganisationName = string From 342f0ebe4474f2330ecc7b2e99094a09308a58c3 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 13:43:36 +1200 Subject: [PATCH 08/36] Make mailer types more dry --- application/backend/src/routes.ts | 7 ++++++- application/backend/swagger.json | 9 ++++++++- application/common/types/api/mailer/index.ts | 10 +++------- application/common/types/commonTypes.ts | 8 ++++++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 234f35621..1c0acb9ed 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -865,10 +865,15 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ContactUsText": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ContactUsRequest": { "dataType": "refObject", "properties": { - "content": {"dataType":"string","required":true}, + "content": {"ref":"ContactUsText","required":true}, "studyId": {"dataType":"double","required":true}, }, "additionalProperties": false, diff --git a/application/backend/swagger.json b/application/backend/swagger.json index b4dce32e5..9038ca684 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -2297,10 +2297,17 @@ "type": "object", "additionalProperties": false }, + "ContactUsText": { + "type": "string", + "example": "There was some problem with this thing that I was doing.\nBut I don't know why?\n\nCheers,\nJohn Doe", + "minLength": 1, + "maxLength": 900, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, "ContactUsRequest": { "properties": { "content": { - "type": "string" + "$ref": "#/components/schemas/ContactUsText" }, "studyId": { "type": "number", diff --git a/application/common/types/api/mailer/index.ts b/application/common/types/api/mailer/index.ts index f3c4a7854..0456bbf48 100644 --- a/application/common/types/api/mailer/index.ts +++ b/application/common/types/api/mailer/index.ts @@ -1,10 +1,6 @@ -/** - * @example { - * "content": "There was some problem with this thing that I was doing.\nBut I don't know why?\n\nCheers,\nJohn Doe" - * "studyId": 1 - * } - */ +import { ContactUsText } from '../../commonTypes' + export interface ContactUsRequest { - content: string + content: ContactUsText studyId: number } diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 34054cad1..54cf1a581 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -147,3 +147,11 @@ export type InviteEmailText = string * @example "Acme Medical Research Institute" */ export type OrganisationName = string + +/** + * @minLength 1 + * @maxLength 900 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "There was some problem with this thing that I was doing.\nBut I don't know why?\n\nCheers,\nJohn Doe" + */ +export type ContactUsText = string From ba0c0dbfa4ca59743da652ce26ba7f7d08ad7ad8 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 14:02:26 +1200 Subject: [PATCH 09/36] Make integration types more dry --- application/backend/src/routes.ts | 7 ++++++- application/backend/swagger.json | 11 ++++++++--- .../api/integrations/redcap/uploadInstrument.ts | 12 +++--------- application/common/types/commonTypes.ts | 12 ++++++++++++ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 1c0acb9ed..4d426e829 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -897,10 +897,15 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "RedcapFormName": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UploadRedcapInstrumentAPIRequest": { "dataType": "refObject", "properties": { - "formName": {"dataType":"string","required":true,"validators":{"minLength":{"value":1}}}, + "formName": {"ref":"RedcapFormName","required":true}, }, "additionalProperties": false, }, diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 9038ca684..0ad804c0c 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -2361,12 +2361,17 @@ "type": "object", "additionalProperties": false }, + "RedcapFormName": { + "type": "string", + "example": "Name of REDCap instrument (from Column B of data dictionary)", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, "UploadRedcapInstrumentAPIRequest": { - "description": "This is the name of the redcap instrument you are importing from.\nNOTE: These 'forms' are not the form label values that are seen on the webpages,\nbut instead they are the unique form names seen in Column B of the data dictionary.", "properties": { "formName": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/RedcapFormName" } }, "required": [ diff --git a/application/common/types/api/integrations/redcap/uploadInstrument.ts b/application/common/types/api/integrations/redcap/uploadInstrument.ts index 8a03a4973..6748b11be 100644 --- a/application/common/types/api/integrations/redcap/uploadInstrument.ts +++ b/application/common/types/api/integrations/redcap/uploadInstrument.ts @@ -1,13 +1,7 @@ -/** - * This is the name of the redcap instrument you are importing from. - * NOTE: These 'forms' are not the form label values that are seen on the webpages, - * but instead they are the unique form names seen in Column B of the data dictionary. - */ +import { RedcapFormName } from '../../../commonTypes' + export interface UploadRedcapInstrumentAPIRequest { - /** - * @minLength 1 - */ - formName: string // specify the form you want to accesss + formName: RedcapFormName } export interface UploadRedcapInstrumentResponse { diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 54cf1a581..f67ef562e 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -116,6 +116,18 @@ export type StudyDescription = string */ export type RedcapToken = string +// This is the name of the redcap instrument you are importing from. +// NOTE: These 'forms' are not the form label values that are seen on the webpages, +// but instead they are the unique form names seen in Column B of the data dictionary. + +/** + * @minLength 1 + * @maxLength 128 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Name of REDCap instrument (from Column B of data dictionary)" + */ +export type RedcapFormName = string + /** * @minLength 1 * @maxLength 128 // TODO: verify max length From 95bab83b01324c7753ec09577bc52b0de4b219f7 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 14:09:25 +1200 Subject: [PATCH 10/36] Make auth types more dry --- application/backend/src/routes.ts | 4 ++-- application/backend/swagger.json | 12 +++--------- .../common/types/api/auth/registerSetup.ts | 18 ++++-------------- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 4d426e829..fc336bc79 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -974,8 +974,8 @@ const models: TsoaRoute.Models = { "RegisterSetupRequest": { "dataType": "refObject", "properties": { - "email": {"dataType":"string","required":true,"validators":{"pattern":{"errorMsg":"Please provide valid email","value":"^(.+)@(.+)$"}}}, - "password": {"dataType":"string","required":true,"validators":{"minLength":{"errorMsg":"Password must be at least 14 characters","value":14}}}, + "email": {"ref":"Email","required":true}, + "password": {"ref":"Password","required":true}, }, "additionalProperties": false, }, diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 0ad804c0c..34f298019 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -2564,12 +2564,10 @@ "RegisterSetupRequest": { "properties": { "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "password": { - "type": "string", - "minLength": 14 + "$ref": "#/components/schemas/Password" } }, "required": [ @@ -2577,11 +2575,7 @@ "password" ], "type": "object", - "additionalProperties": false, - "example": { - "email": "john.doe@email.com", - "password": "Supersecret123" - } + "additionalProperties": false }, "RegisterParticipantResponse": { "properties": { diff --git a/application/common/types/api/auth/registerSetup.ts b/application/common/types/api/auth/registerSetup.ts index a70a6d893..818c31861 100644 --- a/application/common/types/api/auth/registerSetup.ts +++ b/application/common/types/api/auth/registerSetup.ts @@ -1,16 +1,6 @@ -/** - * @example { - * "email": "john.doe@email.com", - * "password": "Supersecret123" - * } - */ +import { Email, Password } from '../../commonTypes' + export interface RegisterSetupRequest { - /** - * @pattern ^(.+)@(.+)$ Please provide valid email - */ - email: string - /** - * @minLength 14 Password must be at least 14 characters - */ - password: string + email: Email + password: Password } From 31110f5e05a43001646b3f88a678697061520e7c Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 15:01:17 +1200 Subject: [PATCH 11/36] Make survey types more dry --- application/backend/src/routes.ts | 51 ++++++++++++++----- application/backend/swagger.json | 65 +++++++++++++++++++------ application/common/types/commonTypes.ts | 40 +++++++++++++++ application/common/types/survey.ts | 34 +++++++------ 4 files changed, 146 insertions(+), 44 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index fc336bc79..9c85a49c8 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -245,6 +245,26 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SurveyStepTitle": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SurveyStepDescription": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SurveyQuestionText": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SurveyQuestionTooltip": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "DuoCode": { "dataType": "refObject", "properties": { @@ -257,8 +277,8 @@ const models: TsoaRoute.Models = { "SurveyQuestionChoices": { "dataType": "refObject", "properties": { - "text": {"dataType":"string","required":true}, - "tooltip": {"dataType":"string"}, + "text": {"ref":"SurveyQuestionText","required":true}, + "tooltip": {"ref":"SurveyQuestionTooltip"}, "required": {"dataType":"boolean","required":true}, "choices": {"dataType":"array","array":{"dataType":"string"},"required":true}, "duoCodes": {"dataType":"array","array":{"dataType":"refObject","ref":"DuoCode"}}, @@ -269,26 +289,36 @@ const models: TsoaRoute.Models = { "SurveyQuestionCheckbox": { "dataType": "refObject", "properties": { - "text": {"dataType":"string","required":true}, - "tooltip": {"dataType":"string"}, + "text": {"ref":"SurveyQuestionText","required":true}, + "tooltip": {"ref":"SurveyQuestionTooltip"}, "required": {"dataType":"boolean","required":true}, "duoCodes": {"dataType":"array","array":{"dataType":"refObject","ref":"DuoCode"}}, }, "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Url": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyVideo": { "dataType": "refObject", "properties": { - "link": {"dataType":"string","required":true}, + "link": {"ref":"Url","required":true}, }, "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SurveySubHeadingText": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveySubHeading": { "dataType": "refObject", "properties": { - "text": {"dataType":"string","required":true}, + "text": {"ref":"SurveySubHeadingText","required":true}, }, "additionalProperties": false, }, @@ -306,8 +336,8 @@ const models: TsoaRoute.Models = { "SurveyStep": { "dataType": "refObject", "properties": { - "title": {"dataType":"string","required":true}, - "text": {"dataType":"string","required":true}, + "title": {"ref":"SurveyStepTitle","required":true}, + "text": {"ref":"SurveyStepDescription","required":true}, "last_updated": {"dataType":"string"}, "elements": {"dataType":"array","array":{"dataType":"refAlias","ref":"SurveyElement"},"required":true}, }, @@ -513,11 +543,6 @@ const models: TsoaRoute.Models = { "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Url": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UpdateStudyRequest": { "dataType": "refObject", "properties": { diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 34f298019..057973ce7 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -419,6 +419,34 @@ "type": "object", "additionalProperties": false }, + "SurveyStepTitle": { + "type": "string", + "example": "Introduction to CTRL", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "SurveyStepDescription": { + "type": "string", + "example": "Watch our short video about the consent process for taking part in medical research.", + "minLength": 1, + "maxLength": 900, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "SurveyQuestionText": { + "type": "string", + "example": "Do you consent to the genomic test?", + "minLength": 1, + "maxLength": 900, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, + "SurveyQuestionTooltip": { + "type": "string", + "example": "Here is an explanation of what genomic means", + "minLength": 1, + "maxLength": 900, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, "DuoCode": { "properties": { "code": { @@ -445,10 +473,10 @@ "SurveyQuestionChoices": { "properties": { "text": { - "type": "string" + "$ref": "#/components/schemas/SurveyQuestionText" }, "tooltip": { - "type": "string" + "$ref": "#/components/schemas/SurveyQuestionTooltip" }, "required": { "type": "boolean" @@ -477,10 +505,10 @@ "SurveyQuestionCheckbox": { "properties": { "text": { - "type": "string" + "$ref": "#/components/schemas/SurveyQuestionText" }, "tooltip": { - "type": "string" + "$ref": "#/components/schemas/SurveyQuestionTooltip" }, "required": { "type": "boolean" @@ -499,10 +527,17 @@ "type": "object", "additionalProperties": false }, + "Url": { + "type": "string", + "example": "https://redcap.orgname.com/api/", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" + }, "SurveyVideo": { "properties": { "link": { - "type": "string" + "$ref": "#/components/schemas/Url" } }, "required": [ @@ -511,10 +546,17 @@ "type": "object", "additionalProperties": false }, + "SurveySubHeadingText": { + "type": "string", + "example": "Section with questions about consent", + "minLength": 1, + "maxLength": 900, + "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + }, "SurveySubHeading": { "properties": { "text": { - "type": "string" + "$ref": "#/components/schemas/SurveySubHeadingText" } }, "required": [ @@ -628,10 +670,10 @@ "SurveyStep": { "properties": { "title": { - "type": "string" + "$ref": "#/components/schemas/SurveyStepTitle" }, "text": { - "type": "string" + "$ref": "#/components/schemas/SurveyStepDescription" }, "last_updated": { "type": "string" @@ -1178,13 +1220,6 @@ "maxLength": 128, "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" }, - "Url": { - "type": "string", - "example": "https://redcap.orgname.com/api/", - "minLength": 1, - "maxLength": 128, - "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" - }, "UpdateStudyRequest": { "properties": { "name": { diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index f67ef562e..b829c57d7 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -167,3 +167,43 @@ export type OrganisationName = string * @example "There was some problem with this thing that I was doing.\nBut I don't know why?\n\nCheers,\nJohn Doe" */ export type ContactUsText = string + +/** + * @minLength 1 + * @maxLength 128 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Introduction to CTRL" + */ +export type SurveyStepTitle = string + +/** + * @minLength 1 + * @maxLength 900 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Watch our short video about the consent process for taking part in medical research." + */ +export type SurveyStepDescription = string + +/** + * @minLength 1 + * @maxLength 900 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Do you consent to the genomic test?" + */ +export type SurveyQuestionText = string + +/** + * @minLength 1 + * @maxLength 900 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Here is an explanation of what genomic means" + */ +export type SurveyQuestionTooltip = string + +/** + * @minLength 1 + * @maxLength 900 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @example "Section with questions about consent" + */ +export type SurveySubHeadingText = string diff --git a/application/common/types/survey.ts b/application/common/types/survey.ts index cfaed0b67..35ffd317e 100644 --- a/application/common/types/survey.ts +++ b/application/common/types/survey.ts @@ -1,35 +1,38 @@ +import { + SurveyQuestionText, + SurveyQuestionTooltip, + SurveyStepDescription, + SurveyStepTitle, + SurveySubHeadingText, + Url, +} from './commonTypes' + export interface DuoCode { code: string // List used codes? relatedAnswer: string | boolean } export interface SurveyQuestionCheckbox { - text: string - tooltip?: string + text: SurveyQuestionText + tooltip?: SurveyQuestionTooltip required: boolean duoCodes?: DuoCode[] } export interface SurveyQuestionChoices { - text: string - tooltip?: string + text: SurveyQuestionText + tooltip?: SurveyQuestionTooltip required: boolean choices: string[] duoCodes?: DuoCode[] } export interface SurveySubHeading { - text: string + text: SurveySubHeadingText } export interface SurveyVideo { - link: string -} - -export interface RefusalText { - title: string - text: string - button_text: string + link: Url } export type SurveyElementType = 'question-choices' | 'question-checkbox' | 'subheading' | 'video' @@ -45,11 +48,10 @@ export type SurveyElement = | { type: SurveyElementType; data: any } export interface SurveyStep { - title: string - text: string - last_updated?: string + title: SurveyStepTitle + text: SurveyStepDescription + last_updated?: string // TODO: Why is this a string? Should be omitted for the update survey response elements: SurveyElement[] - //refusal_text: RefusalText } export type SurveyVersionStatus = 'PUBLISHED' | 'DRAFT' From 73b18c2e2447b854977f0e99732f44418b78a459 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 16:08:23 +1200 Subject: [PATCH 12/36] Tidy up regex --- application/backend/src/routes.ts | 40 +++++++++++------------ application/backend/swagger.json | 42 ++++++++++++------------- application/common/types/commonTypes.ts | 42 ++++++++++++------------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 9c85a49c8..bf74ea0d8 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -144,12 +144,12 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "FirstName": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "LastName": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Email": { @@ -247,22 +247,22 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyStepTitle": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyStepDescription": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyQuestionText": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyQuestionTooltip": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "DuoCode": { @@ -299,7 +299,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Url": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^https?:\\/\\/[a-zA-Z0-9\\-._~:/?#[\\]@!$&'()*+,;=%]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyVideo": { @@ -312,7 +312,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveySubHeadingText": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveySubHeading": { @@ -522,7 +522,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "StudyName": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "CreateStudyRequest": { @@ -535,12 +535,12 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "StudyDescription": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RedcapToken": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: verify max length","value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=.:]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UpdateStudyRequest": { @@ -583,7 +583,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "MiddleName": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[\\p{L}\\s\\-\\'\\.]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":100},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Mobile": { @@ -593,12 +593,12 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "AddressLine": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Suburb": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "StateTerritory": { @@ -777,7 +777,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ExternalId": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=\\.:]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=.:]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Prefill": { @@ -800,12 +800,12 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "InviteEmailSubject": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "InviteEmailText": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "InviteParticipantsRequest": { @@ -863,7 +863,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "OrganisationName": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "CreateOrganisationRequest": { @@ -892,7 +892,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ContactUsText": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ContactUsRequest": { @@ -924,7 +924,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RedcapFormName": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UploadRedcapInstrumentAPIRequest": { diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 057973ce7..44c0f3da8 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -249,14 +249,14 @@ "example": "John", "minLength": 1, "maxLength": 100, - "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$" }, "LastName": { "type": "string", "example": "Doe", "minLength": 1, "maxLength": 100, - "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$" }, "Email": { "type": "string", @@ -424,28 +424,28 @@ "example": "Introduction to CTRL", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "SurveyStepDescription": { "type": "string", "example": "Watch our short video about the consent process for taking part in medical research.", "minLength": 1, "maxLength": 900, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "SurveyQuestionText": { "type": "string", "example": "Do you consent to the genomic test?", "minLength": 1, "maxLength": 900, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "SurveyQuestionTooltip": { "type": "string", "example": "Here is an explanation of what genomic means", "minLength": 1, "maxLength": 900, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "DuoCode": { "properties": { @@ -532,7 +532,7 @@ "example": "https://redcap.orgname.com/api/", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" + "pattern": "^https?:\\/\\/[a-zA-Z0-9\\-._~:/?#[\\]@!$&'()*+,;=%]+$" }, "SurveyVideo": { "properties": { @@ -551,7 +551,7 @@ "example": "Section with questions about consent", "minLength": 1, "maxLength": 900, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "SurveySubHeading": { "properties": { @@ -1193,7 +1193,7 @@ "example": "Acme Genomics Study", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "CreateStudyRequest": { "properties": { @@ -1211,14 +1211,14 @@ "type": "string", "example": "This is a short description of the study", "minLength": 1, - "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "maxLength": 900, + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "RedcapToken": { "type": "string", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" + "pattern": "^[a-zA-Z0-9\\-_=.:]+$" }, "UpdateStudyRequest": { "properties": { @@ -1349,7 +1349,7 @@ "example": "William", "minLength": 1, "maxLength": 100, - "pattern": "^[\\p{L}\\s\\-\\'\\.]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$" }, "Mobile": { "type": "string", @@ -1361,14 +1361,14 @@ "example": "123 Sydney Street", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "Suburb": { "type": "string", "example": "Darlinghurst", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "StateTerritory": { "enum": [ @@ -2104,7 +2104,7 @@ "example": "123e4567-e89b-12d3-a456-426614174000", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-Z0-9\\-_=\\.:]+$" + "pattern": "^[a-zA-Z0-9\\-_=.:]+$" }, "Prefill": { "properties": { @@ -2144,14 +2144,14 @@ "example": "Invitation to CTRL - Dynamic Consent Platform", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "InviteEmailText": { "type": "string", "example": "You have been invited to register with CTRL dynamic consent platform", "minLength": 1, "maxLength": 900, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "InviteParticipantsRequest": { "properties": { @@ -2294,7 +2294,7 @@ "example": "Acme Medical Research Institute", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "CreateOrganisationRequest": { "properties": { @@ -2337,7 +2337,7 @@ "example": "There was some problem with this thing that I was doing.\nBut I don't know why?\n\nCheers,\nJohn Doe", "minLength": 1, "maxLength": 900, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "ContactUsRequest": { "properties": { @@ -2401,7 +2401,7 @@ "example": "Name of REDCap instrument (from Column B of data dictionary)", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÿ0-9\\s\\,\\.\\-\\/\\#]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, "UploadRedcapInstrumentAPIRequest": { "properties": { diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index b829c57d7..964eca487 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -6,7 +6,7 @@ import { Role, User } from '@prisma/client' /** * @minLength 1 * @maxLength 100 - * @pattern ^[\p{L}\s\-\'\.]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ\s\-'.]+$ * @example "John" */ export type FirstName = string @@ -14,7 +14,7 @@ export type FirstName = string /** * @minLength 1 * @maxLength 100 - * @pattern ^[\p{L}\s\-\'\.]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ\s\-'.]+$ * @example "William" */ export type MiddleName = string @@ -22,7 +22,7 @@ export type MiddleName = string /** * @minLength 1 * @maxLength 100 - * @pattern ^[\p{L}\s\-\'\.]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ\s\-'.]+$ * @example "Doe" */ export type LastName = string @@ -51,7 +51,7 @@ export type Mobile = string /** * @minLength 1 * @maxLength 128 - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "123 Sydney Street" */ export type AddressLine = string @@ -59,7 +59,7 @@ export type AddressLine = string /** * @minLength 1 * @maxLength 128 - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Darlinghurst" */ export type Suburb = string @@ -79,7 +79,7 @@ export type DoB = string /** * @minLength 1 * @maxLength 128 - * @pattern ^[a-zA-Z0-9\-_=\.:]+$ + * @pattern ^[a-zA-Z0-9\-_=.:]+$ * @example "123e4567-e89b-12d3-a456-426614174000" */ export type ExternalId = string @@ -95,15 +95,15 @@ export type UserT = User /** * @minLength 1 * @maxLength 128 - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Acme Genomics Study" */ export type StudyName = string /** * @minLength 1 - * @maxLength 128 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @maxLength 900 // TODO: align this with the maxLength of the field + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "This is a short description of the study" */ export type StudyDescription = string @@ -111,7 +111,7 @@ export type StudyDescription = string /** * @minLength 1 * @maxLength 128 // TODO: verify max length - * @pattern ^[a-zA-Z0-9\-_=\.:]+$ + * @pattern ^[a-zA-Z0-9\-_=.:]+$ * @example "123e4567-e89b-12d3-a456-426614174000" // TODO: improve example */ export type RedcapToken = string @@ -123,7 +123,7 @@ export type RedcapToken = string /** * @minLength 1 * @maxLength 128 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Name of REDCap instrument (from Column B of data dictionary)" */ export type RedcapFormName = string @@ -131,7 +131,7 @@ export type RedcapFormName = string /** * @minLength 1 * @maxLength 128 // TODO: verify max length - * @pattern ^[a-zA-Z0-9\-_=\.:]+$ + * @pattern ^https?:\/\/[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]+$ * @example "https://redcap.orgname.com/api/" */ export type Url = string @@ -139,7 +139,7 @@ export type Url = string /** * @minLength 1 * @maxLength 128 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Invitation to CTRL - Dynamic Consent Platform" */ export type InviteEmailSubject = string @@ -147,7 +147,7 @@ export type InviteEmailSubject = string /** * @minLength 1 * @maxLength 900 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "You have been invited to register with CTRL dynamic consent platform" */ export type InviteEmailText = string @@ -155,7 +155,7 @@ export type InviteEmailText = string /** * @minLength 1 * @maxLength 128 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Acme Medical Research Institute" */ export type OrganisationName = string @@ -163,7 +163,7 @@ export type OrganisationName = string /** * @minLength 1 * @maxLength 900 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "There was some problem with this thing that I was doing.\nBut I don't know why?\n\nCheers,\nJohn Doe" */ export type ContactUsText = string @@ -171,7 +171,7 @@ export type ContactUsText = string /** * @minLength 1 * @maxLength 128 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Introduction to CTRL" */ export type SurveyStepTitle = string @@ -179,7 +179,7 @@ export type SurveyStepTitle = string /** * @minLength 1 * @maxLength 900 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Watch our short video about the consent process for taking part in medical research." */ export type SurveyStepDescription = string @@ -187,7 +187,7 @@ export type SurveyStepDescription = string /** * @minLength 1 * @maxLength 900 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Do you consent to the genomic test?" */ export type SurveyQuestionText = string @@ -195,7 +195,7 @@ export type SurveyQuestionText = string /** * @minLength 1 * @maxLength 900 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Here is an explanation of what genomic means" */ export type SurveyQuestionTooltip = string @@ -203,7 +203,7 @@ export type SurveyQuestionTooltip = string /** * @minLength 1 * @maxLength 900 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÿ0-9\s\,\.\-\/\#]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Section with questions about consent" */ export type SurveySubHeadingText = string From f34eafce3a59b16bcbce404d2a1f8db8343decd9 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 16:16:11 +1200 Subject: [PATCH 13/36] Remove illegal character from backend integration/Studies test --- .../backend/tests/integration/Studies.test.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/application/backend/tests/integration/Studies.test.ts b/application/backend/tests/integration/Studies.test.ts index df05b645f..aaac27601 100644 --- a/application/backend/tests/integration/Studies.test.ts +++ b/application/backend/tests/integration/Studies.test.ts @@ -72,8 +72,8 @@ describe('Studies tests', () => { state: StateTerritory.ACT, suburb: 'ABCKDF', dependents: [ - { firstName: 'Child1', lastName: 'K', dob: '2020-01-01', permanent: true }, - { firstName: 'Child2', lastName: 'K', dob: '2020-01-02', permanent: false }, + { firstName: 'ChildA', lastName: 'K', dob: '2020-01-01', permanent: true }, + { firstName: 'ChildB', lastName: 'K', dob: '2020-01-02', permanent: false }, ], } const reqBody2 = { ...reqBody, email: parent2Email, firstName: 'X' } @@ -87,7 +87,7 @@ describe('Studies tests', () => { .send(reqBody2) expect(regRes.statusCode).toBe(201) - const deps1 = await prisma.participantProfile.findMany({ where: { firstName: 'Child1' } }) + const deps1 = await prisma.participantProfile.findMany({ where: { firstName: 'ChildA' } }) expect(deps1).toHaveLength(1) // Answer a question on survey 1 @@ -106,7 +106,7 @@ describe('Studies tests', () => { expect( ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child1' } }, + where: { profile: { firstName: 'ChildA' } }, }) ).answers[1].answers, ).toEqual([true, 'Choice 1']) @@ -114,7 +114,7 @@ describe('Studies tests', () => { expect( ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child2' } }, + where: { profile: { firstName: 'ChildB' } }, }) ).answers[1].answers, ).toEqual([true, 'Choice 1']) @@ -124,8 +124,8 @@ describe('Studies tests', () => { OR: [ { firstName: 'J' }, { firstName: 'X' }, - { firstName: 'Child1' }, - { firstName: 'Child2' }, + { firstName: 'ChildA' }, + { firstName: 'ChildB' }, ], studies: { some: { @@ -143,8 +143,8 @@ describe('Studies tests', () => { OR: [ { firstName: 'J' }, { firstName: 'X' }, - { firstName: 'Child1' }, - { firstName: 'Child2' }, + { firstName: 'ChildA' }, + { firstName: 'ChildB' }, ], studies: { some: { @@ -218,8 +218,8 @@ describe('Studies tests', () => { OR: [ { firstName: 'J' }, { firstName: 'X' }, - { firstName: 'Child1' }, - { firstName: 'Child2' }, + { firstName: 'ChildA' }, + { firstName: 'ChildB' }, ], studies: { some: { @@ -247,7 +247,7 @@ describe('Studies tests', () => { // get dependent answers from study1 const dep1AnswersBefore = ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child1' } }, + where: { profile: { firstName: 'ChildA' } }, }) ).answers[1].last_updated @@ -282,7 +282,7 @@ describe('Studies tests', () => { // get dependent answers. get parent 1 answsers from study1 again const dep1AnswersAfter = ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child1' } }, + where: { profile: { firstName: 'ChildA' } }, }) ).answers[1].last_updated // ensure they are the same @@ -295,7 +295,7 @@ describe('Studies tests', () => { it('Second parent is added to study and answers differently to Parent1. Dependent answers (in study 1) do not get modified', async () => { const dep1AnswersBefore = ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child1' } }, + where: { profile: { firstName: 'ChildA' } }, }) ).answers[1].last_updated @@ -346,7 +346,7 @@ describe('Studies tests', () => { // get dependent answers. get parent 1 answsers from study1 again const dep1AnswersAfter = ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child1' } }, + where: { profile: { firstName: 'ChildA' } }, }) ).answers[1].last_updated // ensure they are the same From 7e472930aadbedf861d224ffec11097cfda7cc4c Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 16:25:22 +1200 Subject: [PATCH 14/36] Remove illegal character from backend integration/Dependents test --- .../tests/integration/Dependents.test.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/application/backend/tests/integration/Dependents.test.ts b/application/backend/tests/integration/Dependents.test.ts index ce900abef..171690809 100644 --- a/application/backend/tests/integration/Dependents.test.ts +++ b/application/backend/tests/integration/Dependents.test.ts @@ -62,8 +62,8 @@ describe('Survey tests', () => { state: StateTerritory.ACT, suburb: 'ABCKDF', dependents: [ - { firstName: 'Child1', lastName: 'K', dob: '2020-01-01', permanent: true }, - { firstName: 'Child2', lastName: 'K', dob: '2020-01-02', permanent: false }, + { firstName: 'ChildA', lastName: 'K', dob: '2020-01-01', permanent: true }, + { firstName: 'ChildB', lastName: 'K', dob: '2020-01-02', permanent: false }, ], } const reqBody2 = { ...reqBody, email: 'parent2@gmail.com', firstName: 'X' } @@ -77,7 +77,7 @@ describe('Survey tests', () => { .send(reqBody2) expect(regRes.statusCode).toBe(201) - const deps1 = await prisma.participantProfile.findMany({ where: { firstName: 'Child1' } }) + const deps1 = await prisma.participantProfile.findMany({ where: { firstName: 'ChildA' } }) console.log('FAMILY', deps1[0].familyId) expect(deps1).toHaveLength(1) }) @@ -98,7 +98,7 @@ describe('Survey tests', () => { expect( ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child1' } }, + where: { profile: { firstName: 'ChildA' } }, }) ).answers[1].answers, ).toEqual([true, 'Choice 1']) @@ -106,7 +106,7 @@ describe('Survey tests', () => { expect( ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child2' } }, + where: { profile: { firstName: 'ChildB' } }, }) ).answers[1].answers, ).toEqual([true, 'Choice 1']) @@ -130,7 +130,7 @@ describe('Survey tests', () => { await prisma.surveyVersionAnswers.findFirstOrThrow({ where: { profile: { - firstName: 'Child1', + firstName: 'ChildA', }, version: { studyId: 1, @@ -143,7 +143,7 @@ describe('Survey tests', () => { expect( ( await prisma.surveyVersionAnswers.findFirstOrThrow({ - where: { profile: { firstName: 'Child2' } }, + where: { profile: { firstName: 'ChildB' } }, }) ).answers[1].answers, ).toEqual([null, 'Choice 1']) @@ -182,13 +182,13 @@ describe('Survey tests', () => { .set({ Authorization: `Bearer ${adminToken}` }) .send({ firstName: 'New', - lastName: 'Dependent2', + lastName: 'DependentB', dob: '1990-01-02', permanent: true, }) const prof = await prisma.participantProfile.findFirstOrThrow({ - where: { firstName: 'New', lastName: 'Dependent2' }, + where: { firstName: 'New', lastName: 'DependentB' }, }) let part = await prisma.surveyVersionAnswers.findFirstOrThrow({ @@ -220,7 +220,7 @@ describe('Survey tests', () => { expect(res.status).toBe(204) const depProfile = await prisma.participantProfile.findFirstOrThrow({ - where: { firstName: 'New', lastName: 'Dependent2' }, + where: { firstName: 'New', lastName: 'DependentB' }, }) let part = await prisma.surveyVersionAnswers.findFirstOrThrow({ @@ -252,7 +252,7 @@ describe('Survey tests', () => { .set({ Authorization: `Bearer ${adminToken}` }) const depProfile = await prisma.participantProfile.findFirstOrThrow({ - where: { firstName: 'New', lastName: 'Dependent2' }, + where: { firstName: 'New', lastName: 'DependentB' }, }) const part = await prisma.surveyVersionAnswers.findFirstOrThrow({ From 8844012995d38277e3dfab1b165e756f82ccefc2 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 17:05:16 +1200 Subject: [PATCH 15/36] Get integration tests passing --- application/backend/src/routes.ts | 8 ++++---- application/backend/swagger.json | 8 ++------ application/common/types/commonTypes.ts | 14 ++++++++------ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index bf74ea0d8..0bb3ca0f5 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -247,12 +247,12 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyStepTitle": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, + "type": {"dataType":"string","validators":{"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]*$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyStepDescription": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, + "type": {"dataType":"string","validators":{"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]*$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyQuestionText": { @@ -522,7 +522,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "StudyName": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, + "type": {"dataType":"string","validators":{"maxLength":{"value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "CreateStudyRequest": { @@ -535,7 +535,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "StudyDescription": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, + "type": {"dataType":"string","validators":{"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":900},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RedcapToken": { diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 44c0f3da8..0812905f3 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -422,16 +422,14 @@ "SurveyStepTitle": { "type": "string", "example": "Introduction to CTRL", - "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]*$" }, "SurveyStepDescription": { "type": "string", "example": "Watch our short video about the consent process for taking part in medical research.", - "minLength": 1, "maxLength": 900, - "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]*$" }, "SurveyQuestionText": { "type": "string", @@ -1191,7 +1189,6 @@ "StudyName": { "type": "string", "example": "Acme Genomics Study", - "minLength": 1, "maxLength": 128, "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, @@ -1210,7 +1207,6 @@ "StudyDescription": { "type": "string", "example": "This is a short description of the study", - "minLength": 1, "maxLength": 900, "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" }, diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 964eca487..b54ec031d 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -92,16 +92,16 @@ export type RoleT = Role // TODO: add annotations? export type UserT = User +// * @minLength 1 //TODO: check if we want minLength for study name /** - * @minLength 1 * @maxLength 128 * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "Acme Genomics Study" */ export type StudyName = string +// * @minLength 1 //TODO: check if we want minLength for study description /** - * @minLength 1 * @maxLength 900 // TODO: align this with the maxLength of the field * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ * @example "This is a short description of the study" @@ -168,18 +168,20 @@ export type OrganisationName = string */ export type ContactUsText = string +// * @minLength 1 TODO: confirm that an empty title is okay +// * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ /** - * @minLength 1 * @maxLength 128 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]*$ * @example "Introduction to CTRL" */ export type SurveyStepTitle = string +// * @minLength 1 TODO: confirm that an empty description is okay +// * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ /** - * @minLength 1 * @maxLength 900 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]*$ * @example "Watch our short video about the consent process for taking part in medical research." */ export type SurveyStepDescription = string From 1d6b64087324c2f8377fb90c968e5ce6c4d06486 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Mon, 24 Aug 2026 18:42:51 +1200 Subject: [PATCH 16/36] Get auth and profiles tests passing --- .../src/controllers/AuthController.test.ts | 5 +- application/backend/src/routes.ts | 39 +++++++++----- application/backend/swagger.json | 52 +++++++++---------- .../common/types/api/users/updateProfile.ts | 46 +++++++++++++++- application/common/types/commonTypes.ts | 8 +-- 5 files changed, 102 insertions(+), 48 deletions(-) diff --git a/application/backend/src/controllers/AuthController.test.ts b/application/backend/src/controllers/AuthController.test.ts index 009f1152f..0511837d6 100644 --- a/application/backend/src/controllers/AuthController.test.ts +++ b/application/backend/src/controllers/AuthController.test.ts @@ -562,6 +562,7 @@ describe('AuthController', () => { expect(body.message).toBe('Validation Failed') expect(body.details).toEqual({ 'bodyRequest.email': { message: 'Invalid value provided' }, + 'bodyRequest.password': { message: 'Invalid value provided' }, }) }) @@ -598,7 +599,7 @@ describe('AuthController', () => { }) const loginRequest: LoginRequest = { email: TestUsers.PARTICIPANT_UNANSWERED.email, - password: 'wrong', + password: 'wrong123412345', } await request(app).post('/auth/login').send(loginRequest) const user = await prisma.user.findFirstOrThrow({ @@ -615,7 +616,7 @@ describe('AuthController', () => { }) const loginRequest: LoginRequest = { email: TestUsers.PARTICIPANT_UNANSWERED.email, - password: 'wrong', + password: 'wrong1234124312', } const loginResponse = await request(app).post('/auth/login').send(loginRequest) expect(loginResponse.body.details).toBe('Retries exceeded, account locked for 24 hours') diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 0bb3ca0f5..537c1ed9d 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -154,7 +154,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Email": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":254}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":254},"pattern":{"value":"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Role": { @@ -404,7 +404,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "DoB": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"isDate":{"errorMsg":"Date of birth must be YYYY-MM-DD format"}}}, + "type": {"dataType":"string","validators":{"pattern":{"value":"^\\d{4}-\\d{2}-\\d{2}$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "SurveyStepAnswerArray": { @@ -588,7 +588,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Mobile": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"pattern":{"errorMsg":"please provide valid phone number","value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, + "type": {"dataType":"string","validators":{"pattern":{"value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "AddressLine": { @@ -665,14 +665,32 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Partial_RegisterParticipantRequest_": { + "ExternalId": { "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"password":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"dependents":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"}},{"dataType":"undefined"}],"validators":{"maxItems":{"value":15}}},"externalId":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}],"validators":{"maxLength":{"value":255}}}},"validators":{}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=.:]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UpdateProfileRequest": { - "dataType": "refAlias", - "type": {"ref":"Partial_RegisterParticipantRequest_","validators":{}}, + "dataType": "refObject", + "properties": { + "firstName": {"ref":"FirstName"}, + "middleName": {"ref":"MiddleName"}, + "lastName": {"ref":"LastName"}, + "email": {"ref":"Email"}, + "mobile": {"ref":"Mobile"}, + "preferredContact": {"ref":"ContactMethod"}, + "addressLine": {"ref":"AddressLine"}, + "suburb": {"ref":"Suburb"}, + "postcode": {"ref":"Postcode"}, + "state": {"ref":"StateTerritory"}, + "password": {"ref":"Password"}, + "dob": {"ref":"DoB"}, + "participantType": {"ref":"ParticipantType"}, + "nextOfKin": {"ref":"AlternativeContact"}, + "dependents": {"dataType":"array","array":{"dataType":"refObject","ref":"OnBehalf"},"validators":{"maxItems":{"value":15}}}, + "externalId": {"ref":"ExternalId","validators":{"maxLength":{"value":255}}}, + }, + "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ParticipantAnswerStatus": { @@ -775,11 +793,6 @@ const models: TsoaRoute.Models = { "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"id":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]},"firstName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"middleName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"lastName":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"dob":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"email":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"mobile":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"addressLine":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"suburb":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"state":{"dataType":"union","subSchemas":[{"ref":"StateTerritory"},{"dataType":"undefined"}]},"postcode":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"undefined"}]},"preferredContact":{"dataType":"union","subSchemas":[{"ref":"ContactMethod"},{"dataType":"undefined"}]},"participantType":{"dataType":"union","subSchemas":[{"ref":"ParticipantType"},{"dataType":"undefined"}]},"nextOfKin":{"dataType":"union","subSchemas":[{"ref":"AlternativeContact"},{"dataType":"undefined"}]},"familyMembers":{"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refObject","ref":"FamilyMember"}},{"dataType":"undefined"}]},"familyId":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"undefined"}]}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "ExternalId": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=.:]+$"}}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Prefill": { "dataType": "refObject", "properties": { @@ -924,7 +937,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "RedcapFormName": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$"}}}, + "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"errorMsg":"// TODO: align this with the maxLength of the field","value":128},"pattern":{"value":"^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€_+]+$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UploadRedcapInstrumentAPIRequest": { diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 0812905f3..76360f77d 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -262,7 +262,8 @@ "type": "string", "example": "john.doe@email.com", "minLength": 1, - "maxLength": 254 + "maxLength": 254, + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" }, "Role": { "type": "string", @@ -840,7 +841,8 @@ }, "DoB": { "type": "string", - "example": "2000-05-21" + "example": "2000-05-21", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "SurveyStepAnswerArray": { "items": { @@ -1566,43 +1568,50 @@ "type": "object", "additionalProperties": false }, - "Partial_RegisterParticipantRequest_": { + "ExternalId": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-zA-Z0-9\\-_=.:]+$" + }, + "UpdateProfileRequest": { "properties": { "firstName": { - "type": "string" + "$ref": "#/components/schemas/FirstName" }, "middleName": { - "type": "string" + "$ref": "#/components/schemas/MiddleName" }, "lastName": { - "type": "string" + "$ref": "#/components/schemas/LastName" }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" }, "mobile": { - "type": "string" + "$ref": "#/components/schemas/Mobile" }, "preferredContact": { "$ref": "#/components/schemas/ContactMethod" }, "addressLine": { - "type": "string" + "$ref": "#/components/schemas/AddressLine" }, "suburb": { - "type": "string" + "$ref": "#/components/schemas/Suburb" }, "postcode": { - "type": "string" + "$ref": "#/components/schemas/Postcode" }, "state": { "$ref": "#/components/schemas/StateTerritory" }, "password": { - "type": "string" + "$ref": "#/components/schemas/Password" }, "dob": { - "type": "string" + "$ref": "#/components/schemas/DoB" }, "participantType": { "$ref": "#/components/schemas/ParticipantType" @@ -1618,15 +1627,11 @@ "maxItems": 15 }, "externalId": { - "type": "string", - "maxLength": 255 + "$ref": "#/components/schemas/ExternalId" } }, "type": "object", - "description": "Make all properties in T optional" - }, - "UpdateProfileRequest": { - "$ref": "#/components/schemas/Partial_RegisterParticipantRequest_" + "additionalProperties": false }, "ParticipantAnswerStatus": { "properties": { @@ -2095,13 +2100,6 @@ "type": "object", "description": "Make all properties in T optional" }, - "ExternalId": { - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000", - "minLength": 1, - "maxLength": 128, - "pattern": "^[a-zA-Z0-9\\-_=.:]+$" - }, "Prefill": { "properties": { "profile": { @@ -2397,7 +2395,7 @@ "example": "Name of REDCap instrument (from Column B of data dictionary)", "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€_+]+$" }, "UploadRedcapInstrumentAPIRequest": { "properties": { diff --git a/application/common/types/api/users/updateProfile.ts b/application/common/types/api/users/updateProfile.ts index 9617392ca..54cceb890 100644 --- a/application/common/types/api/users/updateProfile.ts +++ b/application/common/types/api/users/updateProfile.ts @@ -1,3 +1,45 @@ -import { RegisterParticipantRequest } from '../auth' +import { + AddressLine, + DoB, + Email, + ExternalId, + FirstName, + LastName, + MiddleName, + Mobile, + Password, + Postcode, + Suburb, +} from '../../commonTypes' +import type { + AlternativeContact, + ContactMethod, + StateTerritory, + ParticipantType, + OnBehalf, +} from '../users/ParticipantProfile' -export type UpdateProfileRequest = Partial +export interface UpdateProfileRequest { + firstName?: FirstName + middleName?: MiddleName + lastName?: LastName + email?: Email + mobile?: Mobile + preferredContact?: ContactMethod + addressLine?: AddressLine + suburb?: Suburb + postcode?: Postcode + state?: StateTerritory + password?: Password + dob?: DoB + participantType?: ParticipantType + nextOfKin?: AlternativeContact + /** + * @maxItems 15 + */ + dependents?: OnBehalf[] + /** + * @maxLength 255 + */ + externalId?: ExternalId +} diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index b54ec031d..5d4c77c74 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -30,7 +30,7 @@ export type LastName = string /** * @minLength 1 * @maxLength 254 - * @isEmail + * @pattern ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ * @example "john.doe@email.com" */ export type Email = string @@ -43,7 +43,7 @@ export type Email = string export type Password = string /** - * @pattern ^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ please provide valid phone number + * @pattern ^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ * @example "0412341432" */ export type Mobile = string @@ -71,7 +71,7 @@ export type Suburb = string export type Postcode = string /** - * @isDate Date of birth must be YYYY-MM-DD format + * @pattern ^\d{4}-\d{2}-\d{2}$ * @example "2000-05-21" */ export type DoB = string @@ -123,7 +123,7 @@ export type RedcapToken = string /** * @minLength 1 * @maxLength 128 // TODO: align this with the maxLength of the field - * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€_+]+$ * @example "Name of REDCap instrument (from Column B of data dictionary)" */ export type RedcapFormName = string From 459e7aafab575ac12b16188963a141e527323332 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 10:30:41 +1200 Subject: [PATCH 17/36] Move REGEX to commonTypes so all in one place --- .../src/components/InviteModal.tsx | 4 +-- .../src/pages/participants/edit.tsx | 6 ++--- .../admin-client/src/pages/settings/index.tsx | 6 ++--- .../admin-client/src/pages/setup/index.tsx | 4 +-- .../admin-client/src/pages/studies/index.tsx | 8 +++--- .../admin-client/src/pages/users/create.tsx | 4 +-- .../admin-client/src/pages/users/edit.tsx | 4 +-- .../src/controllers/SettingsController.ts | 4 +-- application/common/src/regex.ts | 4 --- application/common/src/validation.ts | 25 +++++++++++++++++++ application/common/types/commonTypes.ts | 12 +++++++++ .../user-client/src/pages/Register.tsx | 4 +-- 12 files changed, 58 insertions(+), 27 deletions(-) delete mode 100644 application/common/src/regex.ts create mode 100644 application/common/src/validation.ts diff --git a/application/admin-client/src/components/InviteModal.tsx b/application/admin-client/src/components/InviteModal.tsx index 7a0a755ce..a7ea507e8 100644 --- a/application/admin-client/src/components/InviteModal.tsx +++ b/application/admin-client/src/components/InviteModal.tsx @@ -15,7 +15,7 @@ import { Recipient } from '@common/types/invite' import { axiosInstance } from '../providers/dataProvider' import { GetInviteTextResponse } from '@common/types/api/participants' import { useCurrentStudyId } from '../studyStore' -import { emailRegex } from '@common/src/regex' +import { REGEX } from '@common/types/commonTypes' interface InviteModalProps { onSend: (recipients: Recipient[], subjectText: string, explanatoryText: string) => void @@ -25,7 +25,7 @@ interface InviteModalProps { export function InviteModal({ onSend, onCancel, initialRecipients = [] }: InviteModalProps) { const validateEmail = (email: string) => { - const r = new RegExp(emailRegex) //eslint-disable-line + const r = new RegExp(REGEX.EMAIL) //eslint-disable-line return r.test(email) } diff --git a/application/admin-client/src/pages/participants/edit.tsx b/application/admin-client/src/pages/participants/edit.tsx index 741d35c6a..e02195481 100644 --- a/application/admin-client/src/pages/participants/edit.tsx +++ b/application/admin-client/src/pages/participants/edit.tsx @@ -13,7 +13,7 @@ import { useForm } from '@refinedev/react-hook-form' import { Controller } from 'react-hook-form' import { ContactMethod, StateTerritory } from '@common/types/api/users/ParticipantProfile' import { GetParticipantResponse, UpdateParticipantRequest } from '@common/types/api/participants' -import { emailRegex } from '@common/src/regex' +import { REGEX } from '@common/types/commonTypes' export const ParticipantEdit = () => { const { id } = useParsed() @@ -79,7 +79,7 @@ export const ParticipantEdit = () => { {...register('profile.email', { required: 'This field is required', pattern: { - value: emailRegex, + value: REGEX.EMAIL, message: 'Invalid email', }, })} @@ -263,7 +263,7 @@ export const ParticipantEdit = () => { {...register('profile.nextOfKin.email', { required: 'This field is required', pattern: { - value: emailRegex, + value: REGEX.EMAIL, message: 'Invalid email', }, })} diff --git a/application/admin-client/src/pages/settings/index.tsx b/application/admin-client/src/pages/settings/index.tsx index 151f43ace..1cd59e4c5 100644 --- a/application/admin-client/src/pages/settings/index.tsx +++ b/application/admin-client/src/pages/settings/index.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router' import { axiosInstance } from '../../providers/dataProvider' import { Info } from '@mui/icons-material' import { LogoUploader } from '../../components/LogoUploader' -import { urlRegex } from '@common/src/regex' +import { REGEX } from '@common/types/commonTypes' import { RESOURCES } from '../../constants' const SettingsPage = () => { @@ -89,7 +89,7 @@ const SettingsPage = () => { { { const { @@ -72,7 +72,7 @@ export const SetupPage = () => { {...register('email', { required: true, pattern: { - value: emailRegex, + value: REGEX.EMAIL, message: 'Enter a valid email', }, })} diff --git a/application/admin-client/src/pages/studies/index.tsx b/application/admin-client/src/pages/studies/index.tsx index 061b4665b..330b4ab59 100644 --- a/application/admin-client/src/pages/studies/index.tsx +++ b/application/admin-client/src/pages/studies/index.tsx @@ -25,7 +25,7 @@ import { SensitiveTextField } from '../../components/SensitiveTextField' import { useSearchParams } from 'react-router' import { LogoUploader } from '../../components/LogoUploader' import { RESOURCES } from '../../constants' -import { emailRegex } from '@common/src/regex' +import { REGEX } from '@common/types/commonTypes' const StudyCard = ({ studyIdx, @@ -110,16 +110,14 @@ const StudyCard = ({ } const handleSettingsApply = () => { - const urlRegex = - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/ - if (redcapURL && !urlRegex.test(redcapURL)) { + if (redcapURL && !REGEX.URL.test(redcapURL)) { open?.({ type: 'error', message: "Invalid Redcap API URL format. Must start with 'http(s)://'", }) return } - if (contactUsEmail && !emailRegex.test(contactUsEmail)) { + if (contactUsEmail && !REGEX.EMAIL.test(contactUsEmail)) { open?.({ type: 'error', message: 'Invalid Email Address.', diff --git a/application/admin-client/src/pages/users/create.tsx b/application/admin-client/src/pages/users/create.tsx index 66967e920..9c64601ca 100644 --- a/application/admin-client/src/pages/users/create.tsx +++ b/application/admin-client/src/pages/users/create.tsx @@ -1,4 +1,4 @@ -import { emailRegex } from '@common/src/regex' +import { REGEX } from '@common/types/commonTypes' import { Box, MenuItem, TextField } from '@mui/material' import { useGetIdentity } from '@refinedev/core' import { Create } from '@refinedev/mui' @@ -49,7 +49,7 @@ export const UserCreate = () => { emailRegex.test(email) || 'Invalid email address', + validate: (email: string) => REGEX.EMAIL.test(email) || 'Invalid email address', })} error={!!(errors as any)?.email} helperText={(errors as any)?.email?.message} diff --git a/application/admin-client/src/pages/users/edit.tsx b/application/admin-client/src/pages/users/edit.tsx index 3eb6cd6fb..9c1a12d8f 100644 --- a/application/admin-client/src/pages/users/edit.tsx +++ b/application/admin-client/src/pages/users/edit.tsx @@ -14,7 +14,7 @@ import { useStudyStore } from '../../studyStore' import { useGetIdentity, useInvalidate, useNotification, useParsed, useShow } from '@refinedev/core' import { Controller } from 'react-hook-form' import { axiosInstance } from '../../providers/dataProvider' -import { emailRegex } from '@common/src/regex' +import { REGEX } from '@common/types/commonTypes' export const UserEdit = () => { type FieldValues = UpdateUserRequest @@ -116,7 +116,7 @@ export const UserEdit = () => { {...register('email', { required: 'This field is required', validate: (email: string | undefined) => - emailRegex.test(email || '') || 'Invalid email address', + REGEX.EMAIL.test(email || '') || 'Invalid email address', })} error={!!(errors as any)?.email} helperText={(errors as any)?.email?.message} diff --git a/application/backend/src/controllers/SettingsController.ts b/application/backend/src/controllers/SettingsController.ts index ae1fa52fb..e9467c83e 100644 --- a/application/backend/src/controllers/SettingsController.ts +++ b/application/backend/src/controllers/SettingsController.ts @@ -31,12 +31,12 @@ import { NotFoundErrorResponse } from 'common/types/api/errors' import { NotFoundError } from '../middlewares/ErrorHandler' import { auditLog } from '../middlewares/AuditLog' import { processLogoImage } from 'common/src/imageHelpers' -import { urlRegex } from 'common/src/regex' +import { REGEX } from 'common/types/commonTypes' import { sanitizeUrl } from '@braintree/sanitize-url' function sanitiseAndValidateUrl(link: string) { const validatedUrl = sanitizeUrl(link) - if (urlRegex.test(validatedUrl)) { + if (REGEX.URL.test(validatedUrl)) { return validatedUrl } else { throw new Error('urlRegex failed') diff --git a/application/common/src/regex.ts b/application/common/src/regex.ts deleted file mode 100644 index 2c349b68d..000000000 --- a/application/common/src/regex.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const emailRegex = /^[\w.-]+@([\w-]+\.)+[\w-]{2,63}$/ - -export const urlRegex = - /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/ diff --git a/application/common/src/validation.ts b/application/common/src/validation.ts new file mode 100644 index 000000000..94d8772e1 --- /dev/null +++ b/application/common/src/validation.ts @@ -0,0 +1,25 @@ +import { REGEX } from 'types/commonTypes' + +export const nameRules = (required = true) => ({ + required: required ? 'This field is required' : false, + pattern: { + value: REGEX.NAME, + message: 'Contains invalid characters (only letters, spaces, hyphens allowed)', + }, +}) + +export const addressRules = (required = true) => ({ + required: required ? 'This field is required' : false, + pattern: { + value: REGEX.ADDRESS, + message: 'Contains invalid characters', + }, +}) + +export const mobileRules = (required = true) => ({ + required: required ? 'This field is required' : false, + pattern: { + value: REGEX.MOBILE, + message: 'Contains invalid characters', + }, +}) diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 5d4c77c74..1997d8712 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -3,6 +3,18 @@ // Note: see docs for explanation of character class patterns (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) import { Role, User } from '@prisma/client' +// @common/types/commonTypes.ts + +// Specifying these here, so that they can be defined in one place (even though its not DRY) +export const REGEX = { + NAME: /^[a-zA-ZÀ-ÖØ-öø-ɏ\s\-'.]+$/, + ADDRESS: /^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$/, + MOBILE: /^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/, + POSTCODE: /^\d{4}$/, + EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/, +} + /** * @minLength 1 * @maxLength 100 diff --git a/application/user-client/src/pages/Register.tsx b/application/user-client/src/pages/Register.tsx index 787529e6c..8e186c155 100644 --- a/application/user-client/src/pages/Register.tsx +++ b/application/user-client/src/pages/Register.tsx @@ -29,7 +29,7 @@ import { } from '@common/types/api/users/ParticipantProfile' import { AddCircle, Close } from '@mui/icons-material' import { useEffect, useState } from 'react' -import { emailRegex } from '@common/src/regex' +import { REGEX } from '@common/types/commonTypes' interface FormValues { firstName: string @@ -224,7 +224,7 @@ export default function Register() { {...register('email', { required: 'This field is required', pattern: { - value: emailRegex, //eslint-disable-line + value: REGEX.EMAIL, //eslint-disable-line message: 'Enter a valid email', }, })} From bb436f452bab92315d5115100ba9a527b0ceff86 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 11:40:51 +1200 Subject: [PATCH 18/36] Use original mobile regex pattern (created issue for country code) --- application/backend/src/routes.ts | 2 +- application/backend/swagger.json | 2 +- application/common/types/commonTypes.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index 537c1ed9d..cb195cb03 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -588,7 +588,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Mobile": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"pattern":{"value":"^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$"}}}, + "type": {"dataType":"string","validators":{"pattern":{"value":"^04\\d{8}$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "AddressLine": { diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 76360f77d..4e8787a61 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -1352,7 +1352,7 @@ "Mobile": { "type": "string", "example": "0412341432", - "pattern": "^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$" + "pattern": "^04\\d{8}$" }, "AddressLine": { "type": "string", diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 1997d8712..02b1d2560 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -9,7 +9,7 @@ import { Role, User } from '@prisma/client' export const REGEX = { NAME: /^[a-zA-ZÀ-ÖØ-öø-ɏ\s\-'.]+$/, ADDRESS: /^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$/, - MOBILE: /^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/, + MOBILE: /^04\d{8}$/, POSTCODE: /^\d{4}$/, EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/, @@ -55,7 +55,7 @@ export type Email = string export type Password = string /** - * @pattern ^(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$ + * @pattern ^04\d{8}$ * @example "0412341432" */ export type Mobile = string From 0e085e5b05dfcbccda2757b69fd4e2234d7198bf Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 12:38:19 +1200 Subject: [PATCH 19/36] Get user-client tests passing before adding new tests --- application/user-client/cypress/e2e/login.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/user-client/cypress/e2e/login.cy.js b/application/user-client/cypress/e2e/login.cy.js index 83cc42bd5..f1f7e3ac1 100644 --- a/application/user-client/cypress/e2e/login.cy.js +++ b/application/user-client/cypress/e2e/login.cy.js @@ -18,7 +18,7 @@ describe('Login', () => { it('Gets correct message if password is incorrect', () => { cy.visit('/') cy.get('[data-cy="login-email"]').type(TestUsers.PARTICIPANT_UNANSWERED.email) - cy.get('[data-cy="login-password"]').type('passwordwrong') + cy.get('[data-cy="login-password"]').type('Passwordwrong67') cy.contains('Log In').click() cy.contains('Invalid credentials').should('exist') }) From 60bb6f5a75628553a527b82a87ab3994fdb3339c Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 12:56:09 +1200 Subject: [PATCH 20/36] Get admin-client tests passing before adding new tests --- application/admin-client/cypress/e2e/invites.cy.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/admin-client/cypress/e2e/invites.cy.js b/application/admin-client/cypress/e2e/invites.cy.js index 2a0ab8cb5..9dcd92d0d 100644 --- a/application/admin-client/cypress/e2e/invites.cy.js +++ b/application/admin-client/cypress/e2e/invites.cy.js @@ -84,7 +84,8 @@ describe('', () => { it('Sent invites appear in invites list', () => { cy.visit('/participants') cy.get('[data-cy="invite-button"]').click() - cy.get('[data-cy="email-field"]').clear().type('valid1@g.co').type('{enter}') + cy.get('[data-cy="email-field"]').clear().type('valid1@g.co') + cy.get('[data-cy="id-field"]').clear().type('1').type('{enter}') cy.get('[data-cy="send-button"]').click() cy.get('[data-cy="pending-list"]', { timeout: 10000 }).should('contain.text', 'valid1@g.co') }) From 061076fa05fb205890d67d1b45d180547819459a Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 13:13:09 +1200 Subject: [PATCH 21/36] Fix email invite ExternalID minLength bug --- application/admin-client/cypress/e2e/invites.cy.js | 3 +-- application/backend/src/routes.ts | 2 +- application/backend/swagger.json | 3 +-- application/common/types/commonTypes.ts | 3 +-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/application/admin-client/cypress/e2e/invites.cy.js b/application/admin-client/cypress/e2e/invites.cy.js index 9dcd92d0d..2a0ab8cb5 100644 --- a/application/admin-client/cypress/e2e/invites.cy.js +++ b/application/admin-client/cypress/e2e/invites.cy.js @@ -84,8 +84,7 @@ describe('', () => { it('Sent invites appear in invites list', () => { cy.visit('/participants') cy.get('[data-cy="invite-button"]').click() - cy.get('[data-cy="email-field"]').clear().type('valid1@g.co') - cy.get('[data-cy="id-field"]').clear().type('1').type('{enter}') + cy.get('[data-cy="email-field"]').clear().type('valid1@g.co').type('{enter}') cy.get('[data-cy="send-button"]').click() cy.get('[data-cy="pending-list"]', { timeout: 10000 }).should('contain.text', 'valid1@g.co') }) diff --git a/application/backend/src/routes.ts b/application/backend/src/routes.ts index cb195cb03..9df61cedb 100644 --- a/application/backend/src/routes.ts +++ b/application/backend/src/routes.ts @@ -667,7 +667,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ExternalId": { "dataType": "refAlias", - "type": {"dataType":"string","validators":{"minLength":{"value":1},"maxLength":{"value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=.:]+$"}}}, + "type": {"dataType":"string","validators":{"maxLength":{"value":128},"pattern":{"value":"^[a-zA-Z0-9\\-_=.:]*$"}}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UpdateProfileRequest": { diff --git a/application/backend/swagger.json b/application/backend/swagger.json index 4e8787a61..a654a9e2f 100644 --- a/application/backend/swagger.json +++ b/application/backend/swagger.json @@ -1571,9 +1571,8 @@ "ExternalId": { "type": "string", "example": "123e4567-e89b-12d3-a456-426614174000", - "minLength": 1, "maxLength": 128, - "pattern": "^[a-zA-Z0-9\\-_=.:]+$" + "pattern": "^[a-zA-Z0-9\\-_=.:]*$" }, "UpdateProfileRequest": { "properties": { diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 02b1d2560..451a8861b 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -89,9 +89,8 @@ export type Postcode = string export type DoB = string /** - * @minLength 1 * @maxLength 128 - * @pattern ^[a-zA-Z0-9\-_=.:]+$ + * @pattern ^[a-zA-Z0-9\-_=.:]*$ * @example "123e4567-e89b-12d3-a456-426614174000" */ export type ExternalId = string From 2344f287f3453d83eedf4fa10030c9a391680889 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 13:34:15 +1200 Subject: [PATCH 22/36] Fix failing Auth tests with country code --- application/backend/src/controllers/AuthController.test.ts | 2 +- application/backend/tests/integration/Auth.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/backend/src/controllers/AuthController.test.ts b/application/backend/src/controllers/AuthController.test.ts index 0511837d6..0f662b374 100644 --- a/application/backend/src/controllers/AuthController.test.ts +++ b/application/backend/src/controllers/AuthController.test.ts @@ -280,7 +280,7 @@ describe('AuthController', () => { lastName: testLastName, email: TestInvites.INVITE_PENDING.email, password: testPassword, - mobile: '+61477777777', + mobile: '0477777777', addressLine: '123 Some Street', suburb: 'Sydney', postcode: '2000', diff --git a/application/backend/tests/integration/Auth.test.ts b/application/backend/tests/integration/Auth.test.ts index 73e7ab2ab..33d304dda 100644 --- a/application/backend/tests/integration/Auth.test.ts +++ b/application/backend/tests/integration/Auth.test.ts @@ -124,7 +124,7 @@ describe('Auth', () => { lastName: 'Doe', email: TestInvites.INVITE_2_PENDING.email, password: 'johnDoesP@ssword123', - mobile: '+61477777777', + mobile: '0477777777', addressLine: '123 Some Street', suburb: 'Sydney', postcode: '2000', From ac1244380fef65277ac4dad225902ddb18d27047 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 14:09:29 +1200 Subject: [PATCH 23/36] Add backend tests for invalid participant and admin details --- .../src/controllers/AuthController.test.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/application/backend/src/controllers/AuthController.test.ts b/application/backend/src/controllers/AuthController.test.ts index 0f662b374..286b196c2 100644 --- a/application/backend/src/controllers/AuthController.test.ts +++ b/application/backend/src/controllers/AuthController.test.ts @@ -135,6 +135,54 @@ describe('AuthController', () => { expect(body.message).toBe('Validation Failed') }) + it('should return 422 if validation fails on invalid user input', async () => { + const registerRequest = { + firstName: "{{7*7}}", + lastName: "{{7*7}}", + email: "{{7*7}}@email.com", + password: testPassword, + role: Role.Participant, + } + + const response = await request(app) + .post('/auth/register') + .set({ Authorization: `Bearer ${orgAdminToken}` }) + .send(registerRequest) + expect(response.status).toEqual(422) + + const body = response.body + expect(body.message).toBe('Validation Failed') + expect(body.details).toEqual({ + 'bodyRequest.email': { + message: 'Invalid value provided', + }, + 'bodyRequest.firstName': { + message: 'Invalid value provided', + }, + 'bodyRequest.lastName': { + message: 'Invalid value provided', + }, + }) + }) + + it('should return 422 if validation fails on invalid user first name input', async () => { + const registerRequest = { + firstName: "{{7*7}}", + lastName: testLastName, + password: 'password123', //Does not meet minimum password requirements + role: Role.Participant, + } + + const response = await request(app) + .post('/auth/register') + .set({ Authorization: `Bearer ${orgAdminToken}` }) + .send(registerRequest) + expect(response.status).toEqual(422) + + const body = response.body + expect(body.message).toBe('Validation Failed') + }) + it('should return an error if the user is already registered', async () => { const registerRequest: RegisterRequest = { firstName: testFirstName, @@ -369,6 +417,60 @@ describe('AuthController', () => { }) }) + it('should fail validation if provided with illegal values', async () => { + const registerParticipantRequest: RegisterParticipantRequest = { + ...registerParticipantRequestBase, + firstName: "{{7*7}}${{7*7}}#{7*7}<%= 7*7 %>", + lastName: '", + suburb: "", + nextOfKin: { + firstName: 'John{7*7}', + lastName: '', + email: '@smith.com', + }, + } + + const participantInviteId = await prisma.invite.findFirstOrThrow({ + where: { + email: registerParticipantRequestBase.email, + studyId: 1, + }, + }) + + const registerParticipantResponse = await request(app) + .post(`/auth/register/participants/${participantInviteId.id}`) + .send(registerParticipantRequest) + expect(registerParticipantResponse.status).toEqual(422) + + const registerParticipantBody = registerParticipantResponse.body + expect(registerParticipantBody.message).toEqual('Validation Failed') + expect(registerParticipantBody.token).toBe(undefined) + expect(registerParticipantBody.details).toEqual({ + 'bodyRequest.firstName': { + message: 'Invalid value provided', + }, + 'bodyRequest.lastName': { + message: 'Invalid value provided', + }, + 'bodyRequest.addressLine': { + message: 'Invalid value provided', + }, + 'bodyRequest.suburb': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.firstName': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.lastName': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.email': { + message: 'Invalid value provided', + }, + }) + }) + it('Should add dependent profiles if provided, should have same family id', async () => { const registerParticipantRequest: RegisterParticipantRequest = { ...registerParticipantRequestBase, From f3c77adfb7c37381e656c36ed1a2fd82bcc3d87f Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 16:29:32 +1200 Subject: [PATCH 24/36] Add user-client cypress test for xss in registration --- application/common/src/validation.ts | 27 +++++++-- .../cypress/e2e/registration.cy.js | 54 +++++++++++++++++- .../user-client/src/pages/Register.tsx | 56 +++++++------------ 3 files changed, 94 insertions(+), 43 deletions(-) diff --git a/application/common/src/validation.ts b/application/common/src/validation.ts index 94d8772e1..43958e120 100644 --- a/application/common/src/validation.ts +++ b/application/common/src/validation.ts @@ -1,10 +1,18 @@ -import { REGEX } from 'types/commonTypes' +import { REGEX } from '../types/commonTypes' export const nameRules = (required = true) => ({ required: required ? 'This field is required' : false, pattern: { value: REGEX.NAME, - message: 'Contains invalid characters (only letters, spaces, hyphens allowed)', + message: 'Name contains invalid characters (only letters, spaces, hyphens allowed)', + }, +}) + +export const emailRules = (required = true) => ({ + required: required ? 'This field is required' : false, + pattern: { + value: REGEX.EMAIL, + message: 'Enter a valid email', }, }) @@ -12,7 +20,18 @@ export const addressRules = (required = true) => ({ required: required ? 'This field is required' : false, pattern: { value: REGEX.ADDRESS, - message: 'Contains invalid characters', + message: 'Address contains invalid characters', + }, +}) + +// DOB is handled by date picker +// State is handled by drop down + +export const postcodeRules = (required = true) => ({ + required: required ? 'This field is required' : false, + pattern: { + value: REGEX.POSTCODE, + message: 'Invalid postcode', }, }) @@ -20,6 +39,6 @@ export const mobileRules = (required = true) => ({ required: required ? 'This field is required' : false, pattern: { value: REGEX.MOBILE, - message: 'Contains invalid characters', + message: 'Mobile number contains invalid characters (no spaces or country code allowed)', }, }) diff --git a/application/user-client/cypress/e2e/registration.cy.js b/application/user-client/cypress/e2e/registration.cy.js index 579d03a0e..276005df1 100644 --- a/application/user-client/cypress/e2e/registration.cy.js +++ b/application/user-client/cypress/e2e/registration.cy.js @@ -72,12 +72,12 @@ describe('registration', () => { cy.get('[data-cy="reg-button"]').click() cy.contains('Invalid password').should('exist') cy.contains('Invalid postcode').should('exist') - cy.contains('Invalid mobile number').should('exist') + cy.contains('Mobile number contains invalid characters').should('exist') cy.contains('at least 14 characters').should('exist') cy.contains('passwords do not match').should('exist') }) - it('Input some invalid data and get correct error messages', () => { + it('Input an invalid password and get correct error messages', () => { cy.task('getInviteIdtask', { email: TestInvites.INVITE_PENDING.email, studyId: TestStudies.TEST_STUDY.id, @@ -93,6 +93,56 @@ describe('registration', () => { cy.contains('must not contain easily guessable words').should('exist') }) + it('Input xss-themed invalid data and get correct error messages', () => { + cy.task('getInviteIdtask', { + email: TestInvites.INVITE_PENDING.email, + studyId: TestStudies.TEST_STUDY.id, + }) + .as('inviteId') + .then((inviteId) => { + cy.visit(`/register/${inviteId}`) + }) + cy.get('[data-cy="reg-first"]').type("\{\{7*7}}$#", { + parseSpecialCharSequences: false, + }) + cy.get('[data-cy="reg-last"]').type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + cy.get('[data-cy="reg-email"]').type(TestInvites.INVITE_PENDING.email) + cy.get('[data-cy="reg-password"]').type('Aadsfoswefw1515fd@!') + cy.get('[data-cy="reg-confirm-password"]').type('Aadsfoswefw1515fd@!') + cy.get('[data-cy="reg-dob"]').type('1990-01-01') + cy.get('[data-cy="reg-address-line"]').type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + cy.get('[data-cy="reg-suburb"]').type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + cy.get('[data-cy="reg-state"]').click() + cy.contains('VIC').click() + cy.get('[data-cy="reg-postcode"]').type("", { + parseSpecialCharSequences: false, + }) + cy.get('[data-cy="reg-mobile"]').type('') + cy.get('[data-cy="reg-contact-method"]').click() + cy.get('[data-value="EMAIL"]').click() + cy.get('[data-cy="nok-first"]').type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + // hit enter after the last form field + // NOK email seems to have it's own xss protection in electron + cy.get('[data-cy="nok-surname"]') + .type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + .type('{enter}') + + cy.contains('Name contains invalid characters').should('exist') + cy.contains('Address contains invalid characters').should('exist') + cy.contains('Mobile number contains invalid characters').should('exist') + cy.contains('Invalid postcode').should('exist') + }) + it('Attempt to register existing email (i.e. no invite) and get correct error message', () => { cy.visit('/register/not-a-real-inviteId') fillValid() diff --git a/application/user-client/src/pages/Register.tsx b/application/user-client/src/pages/Register.tsx index 8e186c155..523c07f56 100644 --- a/application/user-client/src/pages/Register.tsx +++ b/application/user-client/src/pages/Register.tsx @@ -29,7 +29,13 @@ import { } from '@common/types/api/users/ParticipantProfile' import { AddCircle, Close } from '@mui/icons-material' import { useEffect, useState } from 'react' -import { REGEX } from '@common/types/commonTypes' +import { + addressRules, + emailRules, + mobileRules, + nameRules, + postcodeRules, +} from '@common/src/validation' interface FormValues { firstName: string @@ -199,7 +205,7 @@ export default function Register() { helperText={errors.firstName?.message} data-cy="reg-first" slotProps={{ inputLabel: { shrink: Boolean(watch('firstName')) } }} - {...register('firstName', { required: 'This field is required' })} + {...register('firstName', nameRules())} /> State @@ -329,13 +329,7 @@ export default function Register() { helperText={errors.postcode?.message} data-cy="reg-postcode" slotProps={{ inputLabel: { shrink: Boolean(watch('postcode')) } }} - {...register('postcode', { - required: 'This field is required', - pattern: { - value: /^\d{4}$/, - message: 'Invalid postcode', - }, - })} + {...register('postcode', postcodeRules())} /> Preferred Contact Method @@ -395,7 +383,7 @@ export default function Register() { helperText={errors.nok_first?.message} data-cy="nok-first" slotProps={{ inputLabel: { shrink: Boolean(watch('nok_first')) } }} - {...register('nok_first', { required: 'This field is required' })} + {...register('nok_first', nameRules())} /> <> @@ -458,9 +444,7 @@ export default function Register() { slotProps={{ inputLabel: { shrink: Boolean(watch(`dependents.${idx}.firstName`)) }, }} - {...register(`dependents.${idx}.firstName`, { - required: 'This field is required', - })} + {...register(`dependents.${idx}.firstName`, nameRules())} /> Date: Wed, 26 Aug 2026 17:42:28 +1200 Subject: [PATCH 25/36] WIP Adds xss tests to user-client profileEdit pages --- .../user-client/cypress/e2e/profileEdit.cy.js | 57 +++++++++++++++++++ .../user-client/src/pages/ProfileEdit.tsx | 32 +++++++---- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/application/user-client/cypress/e2e/profileEdit.cy.js b/application/user-client/cypress/e2e/profileEdit.cy.js index b13c2bf52..40d955463 100644 --- a/application/user-client/cypress/e2e/profileEdit.cy.js +++ b/application/user-client/cypress/e2e/profileEdit.cy.js @@ -67,4 +67,61 @@ describe('Profile Edit', () => { expect($el.val()).to.not.include(TestUsers.PARTICIPANT_UNANSWERED.email) }) }) + + it('Invalid xss firstName input gets correct error message', () => { + cy.login(TestUsers.PARTICIPANT_UNANSWERED.email) + cy.visit('/profile/update') + + cy.get('[data-cy="update-first"] input').clear() + cy.get('[data-cy="update-first"]') + .type("\{\{7*7}}$#", { + parseSpecialCharSequences: false, + }) + .type('{enter}') + + cy.get('[data-cy="update-button"]').click() + cy.contains('Name contains invalid characters').should('exist') + }) + + it('Invalid xss lastName input gets correct error message', () => { + cy.login(TestUsers.PARTICIPANT_UNANSWERED.email) + cy.visit('/profile/update') + + cy.get('[data-cy="update-last"] input').clear() + cy.get('[data-cy="update-last"]').type("\{\{7*7}}$#", { + parseSpecialCharSequences: false, + }) + + cy.get('[data-cy="update-button"]').click() + cy.contains('Name contains invalid characters').should('exist') + }) + + it('Invalid xss address input gets correct error message', () => { + cy.login(TestUsers.PARTICIPANT_UNANSWERED.email) + cy.visit('/profile/update') + + cy.get('[data-cy="update-address"] input').clear() + cy.get('[data-cy="update-address"]').type("\{\{7*7}}$#", { + parseSpecialCharSequences: false, + }) + + cy.get('[data-cy="update-button"]').click() + cy.contains('Address contains invalid characters').should('exist') + }) + + it('Invalid xss postcode input gets correct error message', () => { + cy.login(TestUsers.PARTICIPANT_UNANSWERED.email) + cy.visit('/profile/update') + + cy.get('[data-cy="update-postcode"] input').clear() + cy.get('[data-cy="update-postcode"]').type( + "\{\{7*7}}$#", + { + parseSpecialCharSequences: false, + }, + ) + + cy.get('[data-cy="update-button"]').click() + cy.contains('Invalid postcode').should('exist') + }) }) diff --git a/application/user-client/src/pages/ProfileEdit.tsx b/application/user-client/src/pages/ProfileEdit.tsx index 85546f3c9..58f2d22eb 100644 --- a/application/user-client/src/pages/ProfileEdit.tsx +++ b/application/user-client/src/pages/ProfileEdit.tsx @@ -21,6 +21,7 @@ import { GetParticipantProfileResponse, UpdateProfileRequest } from '@common/typ import NavBar from '../components/NavBar' import { apiClient } from '../apiClient' import { ContactMethod, StateTerritory } from '@common/types/api/users/ParticipantProfile' +import { addressRules, nameRules, postcodeRules } from '@common/src/validation' interface FormValues { firstName: string @@ -126,7 +127,7 @@ export default function ProfileEdit() { helperText={errors.firstName?.message} data-cy="update-first" disabled={isPending} - {...register('firstName', { required: true, value: data?.firstName })} + {...register('firstName', { ...nameRules(), value: data?.firstName })} /> State @@ -190,14 +197,9 @@ export default function ProfileEdit() { defaultValue="." error={Boolean(errors.postcode)} helperText={errors.postcode?.message} - {...register('postcode', { - required: true, - value: data?.postcode, - pattern: { - value: /^\d{4}$/, - message: 'Invalid postcode', - }, - })} + data-cy="update-postcode" + disabled={isPending} + {...register('postcode', { ...postcodeRules(), value: data?.postcode })} /> Date: Wed, 26 Aug 2026 18:36:23 +1200 Subject: [PATCH 26/36] Adds xss tests to user-client profileEdit pages --- .../user-client/cypress/e2e/profileEdit.cy.js | 48 ++++++++++++++++++- .../user-client/src/pages/ProfileEdit.tsx | 23 +++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/application/user-client/cypress/e2e/profileEdit.cy.js b/application/user-client/cypress/e2e/profileEdit.cy.js index 40d955463..efd19a2cf 100644 --- a/application/user-client/cypress/e2e/profileEdit.cy.js +++ b/application/user-client/cypress/e2e/profileEdit.cy.js @@ -44,7 +44,7 @@ describe('Profile Edit', () => { cy.get('[data-cy="update-mobile"] input').clear() cy.get('[data-cy="update-mobile"]').type('0487654a') cy.get('[data-cy="update-button"]').click() - cy.contains('Invalid mobile').should('exist') + cy.contains('Mobile number contains invalid characters').should('exist') }) it('Shows family members correctly', () => { @@ -124,4 +124,50 @@ describe('Profile Edit', () => { cy.get('[data-cy="update-button"]').click() cy.contains('Invalid postcode').should('exist') }) + + it('Invalid xss mobile input gets correct error message', () => { + cy.login(TestUsers.PARTICIPANT_UNANSWERED.email) + cy.visit('/profile/update') + + cy.get('[data-cy="update-mobile"] input').clear() + cy.get('[data-cy="update-mobile"]').type("\{\{7*7}}$#", { + parseSpecialCharSequences: false, + }) + + cy.get('[data-cy="update-button"]').click() + cy.contains('Mobile number contains invalid characters').should('exist') + }) + + it('Invalid xss next-of-kin firstname input gets correct error message', () => { + cy.login(TestUsers.PARTICIPANT_UNANSWERED.email) + cy.visit('/profile/update') + + cy.get('[data-cy="update-nok-first"] input').clear() + cy.get('[data-cy="update-nok-first"]').type( + "\{\{7*7}}$#", + { + parseSpecialCharSequences: false, + }, + ) + + cy.get('[data-cy="update-button"]').click() + cy.contains('Name contains invalid characters').should('exist') + }) + + it('Invalid xss next-of-kin lastname input gets correct error message', () => { + cy.login(TestUsers.PARTICIPANT_UNANSWERED.email) + cy.visit('/profile/update') + + cy.get('[data-cy="update-nok-last"] input').clear() + cy.get('[data-cy="update-nok-last"]').type( + "\{\{7*7}}$#", + { + parseSpecialCharSequences: false, + }, + ) + + cy.get('[data-cy="update-button"]').click() + cy.contains('Name contains invalid characters').should('exist') + }) + // nok email validation is handled by electron }) diff --git a/application/user-client/src/pages/ProfileEdit.tsx b/application/user-client/src/pages/ProfileEdit.tsx index 58f2d22eb..8f57d2cf0 100644 --- a/application/user-client/src/pages/ProfileEdit.tsx +++ b/application/user-client/src/pages/ProfileEdit.tsx @@ -21,7 +21,13 @@ import { GetParticipantProfileResponse, UpdateProfileRequest } from '@common/typ import NavBar from '../components/NavBar' import { apiClient } from '../apiClient' import { ContactMethod, StateTerritory } from '@common/types/api/users/ParticipantProfile' -import { addressRules, nameRules, postcodeRules } from '@common/src/validation' +import { + addressRules, + emailRules, + mobileRules, + nameRules, + postcodeRules, +} from '@common/src/validation' interface FormValues { firstName: string @@ -209,14 +215,7 @@ export default function ProfileEdit() { helperText={errors.mobile?.message} data-cy="update-mobile" disabled={isPending} - {...register('mobile', { - required: true, - value: data?.mobile, - pattern: { - value: /04\d{8}$/, - message: 'Invalid mobile number', - }, - })} + {...register('mobile', { ...mobileRules(), value: data?.mobile })} /> Preferred Contact Method @@ -253,7 +252,7 @@ export default function ProfileEdit() { data-cy="update-nok-first" disabled={isPending} {...register('nok_first', { - required: true, + ...nameRules(), value: data?.nextOfKin?.firstName, })} /> @@ -267,7 +266,7 @@ export default function ProfileEdit() { disabled={isPending} key="nok_surname" {...register('nok_surname', { - required: true, + ...nameRules(), value: data?.nextOfKin?.lastName, })} /> @@ -283,7 +282,7 @@ export default function ProfileEdit() { disabled={isPending} key="nok_email" {...register('nok_email', { - required: true, + ...emailRules(), value: data?.nextOfKin?.email, })} /> From 23898c99dcc6172413acc79fe4c36aad9cb538ee Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 18:55:41 +1200 Subject: [PATCH 27/36] Adds xss tests to backend profiles controller --- .../controllers/ProfilesController.test.ts | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/application/backend/src/controllers/ProfilesController.test.ts b/application/backend/src/controllers/ProfilesController.test.ts index 33bfd6a54..efd8fed5e 100644 --- a/application/backend/src/controllers/ProfilesController.test.ts +++ b/application/backend/src/controllers/ProfilesController.test.ts @@ -166,5 +166,101 @@ describe('ProfilesController', () => { expect(response.body.message).toBe('Validation Failed') } }) + + it('should fail validation if provided with illegal xss values', async () => { + const updateProfileRequest: UpdateProfileRequest = { + firstName: "{{7*7}}${{7*7}}#{7*7}<%= 7*7 %>", + lastName: '", + suburb: "", + nextOfKin: { + firstName: 'John{7*7}', + lastName: '', + email: '@smith.com', + }, + } + + const updateProfileResponse = await request(app) + .patch('/profiles/current') + .set({ authorization: `Bearer ${registeredParticipantToken}` }) + .send(updateProfileRequest) + expect(updateProfileResponse.status).toEqual(422) + + const updateProfileBody = updateProfileResponse.body + expect(updateProfileBody.message).toEqual('Validation Failed') + expect(updateProfileBody.token).toBe(undefined) + expect(updateProfileBody.details).toEqual({ + 'bodyRequest.firstName': { + message: 'Invalid value provided', + }, + 'bodyRequest.lastName': { + message: 'Invalid value provided', + }, + 'bodyRequest.addressLine': { + message: 'Invalid value provided', + }, + 'bodyRequest.suburb': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.firstName': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.lastName': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.email': { + message: 'Invalid value provided', + }, + }) + }) + }) + + describe('PATCH /profiles/:userId', () => { + it('should fail validation if provided with illegal xss values', async () => { + const updateProfileRequest: UpdateProfileRequest = { + firstName: "{{7*7}}${{7*7}}#{7*7}<%= 7*7 %>", + lastName: '", + suburb: "", + nextOfKin: { + firstName: 'John{7*7}', + lastName: '', + email: '@smith.com', + }, + } + + const updateProfileResponse = await request(app) + .patch(`/profiles/${TestUsers.PARTICIPANT_COMPLETED.id}`) + .set({ Authorization: `Bearer ${orgAdminToken}` }) + .send(updateProfileRequest) + expect(updateProfileResponse.status).toEqual(422) + + const updateProfileBody = updateProfileResponse.body + expect(updateProfileBody.message).toEqual('Validation Failed') + expect(updateProfileBody.token).toBe(undefined) + expect(updateProfileBody.details).toEqual({ + 'bodyRequest.firstName': { + message: 'Invalid value provided', + }, + 'bodyRequest.lastName': { + message: 'Invalid value provided', + }, + 'bodyRequest.addressLine': { + message: 'Invalid value provided', + }, + 'bodyRequest.suburb': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.firstName': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.lastName': { + message: 'Invalid value provided', + }, + 'bodyRequest.nextOfKin.email': { + message: 'Invalid value provided', + }, + }) + }) }) }) From a7ce5f73f062f952f5cb38c1902f54dade34a87b Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 19:21:04 +1200 Subject: [PATCH 28/36] Tidies up auth controller test --- .../src/controllers/AuthController.test.ts | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/application/backend/src/controllers/AuthController.test.ts b/application/backend/src/controllers/AuthController.test.ts index 286b196c2..498cd1f09 100644 --- a/application/backend/src/controllers/AuthController.test.ts +++ b/application/backend/src/controllers/AuthController.test.ts @@ -32,7 +32,7 @@ describe('AuthController', () => { const testFirstName = 'John' const testLastName = 'Doe' const testEmail = 'johndoe@example.com' - const testPassword = 'Loginfortests123' + const testPassword = 'Lolliesfortests123' const testGuardianFirstName = 'Jenny' beforeAll(async () => { @@ -141,7 +141,7 @@ describe('AuthController', () => { lastName: "{{7*7}}", email: "{{7*7}}@email.com", password: testPassword, - role: Role.Participant, + role: Role.OrganisationAdmin, } const response = await request(app) @@ -165,24 +165,6 @@ describe('AuthController', () => { }) }) - it('should return 422 if validation fails on invalid user first name input', async () => { - const registerRequest = { - firstName: "{{7*7}}", - lastName: testLastName, - password: 'password123', //Does not meet minimum password requirements - role: Role.Participant, - } - - const response = await request(app) - .post('/auth/register') - .set({ Authorization: `Bearer ${orgAdminToken}` }) - .send(registerRequest) - expect(response.status).toEqual(422) - - const body = response.body - expect(body.message).toBe('Validation Failed') - }) - it('should return an error if the user is already registered', async () => { const registerRequest: RegisterRequest = { firstName: testFirstName, From 34955eb91062cf862ff3d66df7aa6e42eafc89fd Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 19:45:06 +1200 Subject: [PATCH 29/36] Add backend and user-client tests for dependents xss --- .../src/controllers/AuthController.test.ts | 14 +++++++++++ .../cypress/e2e/registration.cy.js | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/application/backend/src/controllers/AuthController.test.ts b/application/backend/src/controllers/AuthController.test.ts index 498cd1f09..59d022950 100644 --- a/application/backend/src/controllers/AuthController.test.ts +++ b/application/backend/src/controllers/AuthController.test.ts @@ -411,6 +411,14 @@ describe('AuthController', () => { lastName: '', email: '@smith.com', }, + dependents: [ + { + firstName: 'John{7*7}', + lastName: '', + dob: '2020-01-01', + permanent: false, + }, + ], } const participantInviteId = await prisma.invite.findFirstOrThrow({ @@ -450,6 +458,12 @@ describe('AuthController', () => { 'bodyRequest.nextOfKin.email': { message: 'Invalid value provided', }, + 'dependents.$0.firstName': { + message: 'Invalid value provided', + }, + 'dependents.$0.lastName': { + message: 'Invalid value provided', + }, }) }) diff --git a/application/user-client/cypress/e2e/registration.cy.js b/application/user-client/cypress/e2e/registration.cy.js index 276005df1..a50c51676 100644 --- a/application/user-client/cypress/e2e/registration.cy.js +++ b/application/user-client/cypress/e2e/registration.cy.js @@ -175,4 +175,27 @@ describe('registration', () => { cy.contains('Welcome FIRST').should('exist') cy.contains('Step 2').should('exist') }) + + it('Add dependents with xss, check errors', () => { + cy.task('getInviteIdtask', { + email: TestInvites.INVITE_PENDING.email, + studyId: TestStudies.TEST_STUDY.id, + }) + .as('inviteId') + .then((inviteId) => { + cy.visit(`/register/${inviteId}`) + }) + fillValid() + cy.get('[data-cy="add-dependent"]').click() + cy.get('[data-cy=dep-first]').type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + cy.get('[data-cy="dep-dob"]').type('2020-01-01') + cy.get('[data-cy=dep-surname]') + .type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + .type('{enter}') + cy.contains('Name contains invalid characters').should('exist') + }) }) From 91410c744a33b19dc19d196b4630575065df5b94 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Wed, 26 Aug 2026 20:04:33 +1200 Subject: [PATCH 30/36] Add admin-client xss test for family edit and add warnings --- .../admin-client/cypress/e2e/families.cy.js | 16 +++++++++++++++ .../admin-client/src/pages/family/edit.tsx | 20 ++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/application/admin-client/cypress/e2e/families.cy.js b/application/admin-client/cypress/e2e/families.cy.js index adc204ff6..4934631e0 100644 --- a/application/admin-client/cypress/e2e/families.cy.js +++ b/application/admin-client/cypress/e2e/families.cy.js @@ -41,6 +41,22 @@ describe('Family Editing', () => { cy.get('[data-cy="in-study-checkbox"] input').last().should('be.checked') }) + it('Cannot use xss when adding new dependent to family', () => { + cy.visit('/participants/family/edit/100') + cy.get('[data-cy="add-member-button"]').click() + cy.get('[data-cy="registered-no"]').click() + cy.get('[data-cy="new-dependent"]').click() + cy.get('[data-cy=dep-first]').type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + cy.get('[data-cy=dep-surname]').type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + cy.get('[data-cy="dep-dob"]').type('2020-01-01') + cy.get('[data-cy="add-dep-button"]').click() + cy.contains('Name contains invalid characters').should('exist') + }) + it('Remove member from family', () => { cy.visit('/participants/family/edit/100') cy.get('[data-cy="remove-member-button"]').click() diff --git a/application/admin-client/src/pages/family/edit.tsx b/application/admin-client/src/pages/family/edit.tsx index ea6b0c765..96e2fe6f3 100644 --- a/application/admin-client/src/pages/family/edit.tsx +++ b/application/admin-client/src/pages/family/edit.tsx @@ -35,6 +35,7 @@ import { GetFamilyResponse } from '@common/types/api/families' import { axiosInstance } from '../../providers/dataProvider' import { useCurrentStudyId } from '../../studyStore' import { useQueryClient } from '@tanstack/react-query' +import { nameRules } from '@common/src/validation' export const FamilyEdit = () => { const studyId = useCurrentStudyId() @@ -68,7 +69,12 @@ export const FamilyEdit = () => { }, [data, id, nav, queryClient]) //Dependents add form - const { register, handleSubmit, reset } = useForm() + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm() const resetForm = () => { reset() @@ -333,18 +339,18 @@ export const FamilyEdit = () => { label="First Name" data-cy="dep-first" required - {...register(`firstName`, { - required: 'This field is required', - })} + error={!!errors.firstName} + helperText={errors.firstName?.message as string} + {...register(`firstName`, nameRules())} /> Date: Wed, 26 Aug 2026 20:11:53 +1200 Subject: [PATCH 31/36] Add admin-client xss test for setup page --- .../admin-client/cypress/e2e/setup.cy.js | 25 +++++++++++++++++++ .../admin-client/src/pages/setup/index.tsx | 10 ++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/application/admin-client/cypress/e2e/setup.cy.js b/application/admin-client/cypress/e2e/setup.cy.js index 5927133b1..5eeb499d5 100644 --- a/application/admin-client/cypress/e2e/setup.cy.js +++ b/application/admin-client/cypress/e2e/setup.cy.js @@ -13,4 +13,29 @@ describe('Setup', () => { cy.visit('/surveys') cy.contains('Current Draft').should('exist') }) + + it('Cannot register with xss', () => { + cy.task('wipe') + cy.visit('/') + cy.url().should('contain', '/setup') + cy.get('[data-cy="setup-email"]').type( + "{{7*7}}@gmail.com", + { + parseSpecialCharSequences: false, + }, + ) + cy.get('[data-cy="setup-password"]').type(TestUsers.ORG_ADMIN.password) // Using test data to conform to pr requirements + cy.get('[data-cy="setup-submit"]').click() + cy.contains('Enter a valid email').should('exist') + }) + + it('Cannot register with weak password', () => { + cy.task('wipe') + cy.visit('/') + cy.url().should('contain', '/setup') + cy.get('[data-cy="setup-email"]').type('abc@d.com') + cy.get('[data-cy="setup-password"]').type('password') + cy.get('[data-cy="setup-submit"]').click() + cy.contains('Invalid password').should('exist') + }) }) diff --git a/application/admin-client/src/pages/setup/index.tsx b/application/admin-client/src/pages/setup/index.tsx index be43d7cd0..a7678b8d1 100644 --- a/application/admin-client/src/pages/setup/index.tsx +++ b/application/admin-client/src/pages/setup/index.tsx @@ -3,7 +3,7 @@ import { Alert, Box, Button, Card, Container, TextField, Typography } from '@mui import { useLogin } from '../../hooks/useLogin' import { useForm } from 'react-hook-form' import { checkPasswordStrength } from '@common/src/PasswordStrength' -import { REGEX } from '@common/types/commonTypes' +import { emailRules } from '@common/src/validation' export const SetupPage = () => { const { @@ -69,13 +69,7 @@ export const SetupPage = () => { data-cy="setup-email" error={Boolean(errors.email)} helperText={errors.email?.message as any} - {...register('email', { - required: true, - pattern: { - value: REGEX.EMAIL, - message: 'Enter a valid email', - }, - })} + {...register('email', emailRules())} /> Date: Wed, 26 Aug 2026 21:34:37 +1200 Subject: [PATCH 32/36] Add admin-client xss test for participant edit page --- .../cypress/e2e/participants.cy.js | 158 +++++++++++++++++- .../admin-client/cypress/e2e/users.cy.js | 68 +++++++- .../src/pages/participants/edit.tsx | 74 +++----- .../admin-client/src/pages/users/create.tsx | 15 +- .../admin-client/src/pages/users/edit.tsx | 16 +- application/common/src/validation.ts | 9 + application/common/types/commonTypes.ts | 1 + 7 files changed, 261 insertions(+), 80 deletions(-) diff --git a/application/admin-client/cypress/e2e/participants.cy.js b/application/admin-client/cypress/e2e/participants.cy.js index 5bc4def1c..e970bb596 100644 --- a/application/admin-client/cypress/e2e/participants.cy.js +++ b/application/admin-client/cypress/e2e/participants.cy.js @@ -38,7 +38,7 @@ describe('Participants', () => { cy.get('input[name="profile.postcode"]').clear().type('2222') cy.get('input[name="profile.nextOfKin.email"]').clear().type('invalid') cy.contains('Save').click() - cy.contains('Invalid email').should('exist') + cy.contains('Enter a valid email').should('exist') cy.get('input[name="profile.nextOfKin.email"]').clear().type('valid@email.com') cy.contains('Save').click() cy.url().should('contain', `participants/${TestUsers.PARTICIPANT_UNANSWERED.id}`) @@ -68,4 +68,160 @@ describe('Participants', () => { cy.get('[data-rowindex="2"]').contains('V1').trigger('mouseover', { force: true }) cy.contains('Incomplete').should('be.visible') }) + + it('Edit participant, validate xss first name', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.firstName"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Name contains invalid characters').should('exist') + }) + + it('Edit participant, validate xss last name', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.lastName"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Name contains invalid characters').should('exist') + }) + + it('Edit participant, validate xss email', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.email"]') + .clear() + .type("{{7*7}}@gmail.com", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Enter a valid email').should('exist') + }) + + it('Edit participant, validate xss externalId', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="externalId"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('External ID can only consist of').should('exist') + }) + + it('Edit participant, validate xss address', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.addressLine"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Address contains invalid characters').should('exist') + }) + + it('Edit participant, validate xss suburb', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.suburb"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Address contains invalid characters').should('exist') + }) + + it('Edit participant, validate xss postcode', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.postcode"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Invalid postcode').should('exist') + }) + + it('Edit participant, validate xss mobile', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.mobile"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Mobile number contains invalid characters').should('exist') + }) + + it('Edit participant, validate xss nok first name', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.nextOfKin.firstName"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Name contains invalid characters').should('exist') + }) + + it('Edit participant, validate xss nok last name', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.nextOfKin.lastName"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Name contains invalid characters').should('exist') + }) + + it('Edit participant, validate xss nok email', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.nextOfKin.email"]') + .clear() + .type("{{7*7}}@gmail.com", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Enter a valid email').should('exist') + }) + + it('Edit participant, validate xss nok mobile', () => { + cy.login(TestUsers.ORG_ADMIN.email) + cy.visit(`/participants/edit/${TestUsers.PARTICIPANT_UNANSWERED.id}`) + cy.contains('Edit Participant').should('exist') + cy.get('input[name="profile.nextOfKin.mobile"]') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Mobile number contains invalid characters').should('exist') + }) }) diff --git a/application/admin-client/cypress/e2e/users.cy.js b/application/admin-client/cypress/e2e/users.cy.js index cf3f87cfb..146cea9da 100644 --- a/application/admin-client/cypress/e2e/users.cy.js +++ b/application/admin-client/cypress/e2e/users.cy.js @@ -26,7 +26,7 @@ describe('Users', () => { cy.get('input').eq(1).type('Presley') cy.get('input').eq(2).type('elvisexample.com') cy.contains('Save').click() - cy.contains('Invalid email').should('exist') + cy.contains('Enter a valid email').should('exist') cy.get('input').eq(2).clear().type('elvis@example.com') cy.contains('Save').click() cy.contains('Created at').should('exist') @@ -38,11 +38,75 @@ describe('Users', () => { cy.get('input').eq(0).type('A') cy.get('input').eq(2).clear().type('elvisexample.com') cy.contains('Save').click() - cy.contains('Invalid email').should('exist') + cy.contains('Enter a valid email').should('exist') cy.get('input').eq(2).clear().type('elvis@example.com') cy.contains('Save').click() cy.contains('Success').should('exist') cy.visit('/users') cy.contains('OrganisationA').should('exist') }) + it('Create user, check validation of xss firstname input', () => { + cy.visit('/users') + cy.contains('Create').click() + cy.url().should('contain', '/users/create') + cy.get('[data-cy="create-first"]').type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.get('input').eq(1).type('Presley') + cy.get('input').eq(2).type('elvis@example.com') + cy.contains('Save').click() + cy.contains('Name contains invalid characters').should('exist') + }) + it('Create user, check validation of xss lastname input', () => { + cy.visit('/users') + cy.contains('Create').click() + cy.url().should('contain', '/users/create') + cy.get('[data-cy="create-first"]').type('Elvis') + cy.get('input').eq(1).type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.get('input').eq(2).type('elvis@example.com') + cy.contains('Save').click() + cy.contains('Name contains invalid characters').should('exist') + }) + it('Create user, check validation of xss email input', () => { + cy.visit('/users') + cy.contains('Create').click() + cy.url().should('contain', '/users/create') + cy.get('[data-cy="create-first"]').type('Elvis') + cy.get('input').eq(1).type('Presley') + cy.get('input').eq(2).type("{{7*7}}@gmail.com", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Enter a valid email').should('exist') + }) + it('Edit user, check validation of xss firstname input', () => { + cy.visit('/users') + cy.get('[data-cy="edit-button"]').eq(1).click() + cy.get('input').eq(0).type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Name contains invalid characters').should('exist') + }) + it('Edit user, check validation of xss lastname input', () => { + cy.visit('/users') + cy.get('[data-cy="edit-button"]').eq(1).click() + cy.get('input').eq(1).type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Name contains invalid characters').should('exist') + }) + it('Edit user, check validation of xss email input', () => { + cy.visit('/users') + cy.get('[data-cy="edit-button"]').eq(1).click() + cy.get('input').eq(0).type('A') + cy.get('input').eq(2).clear().type("{{7*7}}@gmail.com", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains('Enter a valid email').should('exist') + }) }) diff --git a/application/admin-client/src/pages/participants/edit.tsx b/application/admin-client/src/pages/participants/edit.tsx index e02195481..3a817eda2 100644 --- a/application/admin-client/src/pages/participants/edit.tsx +++ b/application/admin-client/src/pages/participants/edit.tsx @@ -13,7 +13,14 @@ import { useForm } from '@refinedev/react-hook-form' import { Controller } from 'react-hook-form' import { ContactMethod, StateTerritory } from '@common/types/api/users/ParticipantProfile' import { GetParticipantResponse, UpdateParticipantRequest } from '@common/types/api/participants' -import { REGEX } from '@common/types/commonTypes' +import { + addressRules, + emailRules, + externalIdRules, + mobileRules, + nameRules, + postcodeRules, +} from '@common/src/validation' export const ParticipantEdit = () => { const { id } = useParsed() @@ -52,9 +59,7 @@ export const ParticipantEdit = () => { > { label={'First Name'} /> { label={'Last Name'} /> { label={'Email'} /> { )} /> { label={'Address Line'} /> { )} /> { label={'Postcode'} /> { Alternative Contact { label={'First Name'} /> { label={'Last Name'} /> { label={'Email'} /> { const { @@ -20,9 +20,7 @@ export const UserCreate = () => { { name="firstName" /> { name="lastName" /> REGEX.EMAIL.test(email) || 'Invalid email address', - })} + {...register('email', emailRules())} error={!!(errors as any)?.email} helperText={(errors as any)?.email?.message} margin="normal" diff --git a/application/admin-client/src/pages/users/edit.tsx b/application/admin-client/src/pages/users/edit.tsx index 9c1a12d8f..11c63e197 100644 --- a/application/admin-client/src/pages/users/edit.tsx +++ b/application/admin-client/src/pages/users/edit.tsx @@ -14,7 +14,7 @@ import { useStudyStore } from '../../studyStore' import { useGetIdentity, useInvalidate, useNotification, useParsed, useShow } from '@refinedev/core' import { Controller } from 'react-hook-form' import { axiosInstance } from '../../providers/dataProvider' -import { REGEX } from '@common/types/commonTypes' +import { emailRules, nameRules } from '@common/src/validation' export const UserEdit = () => { type FieldValues = UpdateUserRequest @@ -84,9 +84,7 @@ export const UserEdit = () => { autoComplete="off" > { data-cy="first" /> { disabled={editingDisabled} /> - REGEX.EMAIL.test(email || '') || 'Invalid email address', - })} + {...register('email', emailRules())} error={!!(errors as any)?.email} helperText={(errors as any)?.email?.message} margin="normal" diff --git a/application/common/src/validation.ts b/application/common/src/validation.ts index 43958e120..aac81ddb4 100644 --- a/application/common/src/validation.ts +++ b/application/common/src/validation.ts @@ -42,3 +42,12 @@ export const mobileRules = (required = true) => ({ message: 'Mobile number contains invalid characters (no spaces or country code allowed)', }, }) + +// Default is not required +export const externalIdRules = (required = false) => ({ + required: required ? 'This field is required' : false, + pattern: { + value: REGEX.EXTERNALID, + message: 'External ID can only consist of alpha numeric characters and -_=.:', + }, +}) diff --git a/application/common/types/commonTypes.ts b/application/common/types/commonTypes.ts index 451a8861b..2c019e986 100644 --- a/application/common/types/commonTypes.ts +++ b/application/common/types/commonTypes.ts @@ -12,6 +12,7 @@ export const REGEX = { MOBILE: /^04\d{8}$/, POSTCODE: /^\d{4}$/, EMAIL: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + EXTERNALID: /^[a-zA-Z0-9\-_=.:]*$/, URL: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/, } From 8f097e284286404ae3b6f7f281579df0362fb10e Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Thu, 27 Aug 2026 10:23:27 +1200 Subject: [PATCH 33/36] WIP start moving to validation message constant --- application/common/src/validation.ts | 33 ++++++++++++------- .../user-client/cypress/e2e/profileEdit.cy.js | 19 ++++++----- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/application/common/src/validation.ts b/application/common/src/validation.ts index aac81ddb4..b85309a21 100644 --- a/application/common/src/validation.ts +++ b/application/common/src/validation.ts @@ -1,26 +1,35 @@ import { REGEX } from '../types/commonTypes' +export const VALIDATION_MESSAGES = { + REQUIRED: 'This field is required', + NAME_INVALID: 'Name contains invalid characters (only letters, spaces, hyphens allowed)', + EMAIL_INVALID: 'Enter a valid email', + ADDRESS_INVALID: 'Address contains invalid characters', + POSTCODE_INVALID: 'Invalid postcode', + MOBILE_INVALID: 'Mobile number contains invalid characters (no spaces or country code allowed)', + EXTERNALID_INVALID: 'External ID can only consist of alpha numeric characters and -_=.:', +} export const nameRules = (required = true) => ({ - required: required ? 'This field is required' : false, + required: required ? VALIDATION_MESSAGES.REQUIRED : false, pattern: { value: REGEX.NAME, - message: 'Name contains invalid characters (only letters, spaces, hyphens allowed)', + message: VALIDATION_MESSAGES.NAME_INVALID, }, }) export const emailRules = (required = true) => ({ - required: required ? 'This field is required' : false, + required: required ? VALIDATION_MESSAGES.REQUIRED : false, pattern: { value: REGEX.EMAIL, - message: 'Enter a valid email', + message: VALIDATION_MESSAGES.EMAIL_INVALID, }, }) export const addressRules = (required = true) => ({ - required: required ? 'This field is required' : false, + required: required ? VALIDATION_MESSAGES.REQUIRED : false, pattern: { value: REGEX.ADDRESS, - message: 'Address contains invalid characters', + message: VALIDATION_MESSAGES.ADDRESS_INVALID, }, }) @@ -28,26 +37,26 @@ export const addressRules = (required = true) => ({ // State is handled by drop down export const postcodeRules = (required = true) => ({ - required: required ? 'This field is required' : false, + required: required ? VALIDATION_MESSAGES.REQUIRED : false, pattern: { value: REGEX.POSTCODE, - message: 'Invalid postcode', + message: VALIDATION_MESSAGES.POSTCODE_INVALID, }, }) export const mobileRules = (required = true) => ({ - required: required ? 'This field is required' : false, + required: required ? VALIDATION_MESSAGES.REQUIRED : false, pattern: { value: REGEX.MOBILE, - message: 'Mobile number contains invalid characters (no spaces or country code allowed)', + message: VALIDATION_MESSAGES.MOBILE_INVALID, }, }) // Default is not required export const externalIdRules = (required = false) => ({ - required: required ? 'This field is required' : false, + required: required ? VALIDATION_MESSAGES.REQUIRED : false, pattern: { value: REGEX.EXTERNALID, - message: 'External ID can only consist of alpha numeric characters and -_=.:', + message: VALIDATION_MESSAGES.EXTERNALID_INVALID, }, }) diff --git a/application/user-client/cypress/e2e/profileEdit.cy.js b/application/user-client/cypress/e2e/profileEdit.cy.js index efd19a2cf..20cf05000 100644 --- a/application/user-client/cypress/e2e/profileEdit.cy.js +++ b/application/user-client/cypress/e2e/profileEdit.cy.js @@ -1,6 +1,7 @@ /// const { TestUsers } = require('../../../common/testing/constants') +const { VALIDATION_MESSAGES } = require('../../../common/src/validation') beforeEach(() => { cy.task('reset') @@ -44,7 +45,7 @@ describe('Profile Edit', () => { cy.get('[data-cy="update-mobile"] input').clear() cy.get('[data-cy="update-mobile"]').type('0487654a') cy.get('[data-cy="update-button"]').click() - cy.contains('Mobile number contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.MOBILE_INVALID).should('exist') }) it('Shows family members correctly', () => { @@ -80,7 +81,7 @@ describe('Profile Edit', () => { .type('{enter}') cy.get('[data-cy="update-button"]').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Invalid xss lastName input gets correct error message', () => { @@ -93,7 +94,7 @@ describe('Profile Edit', () => { }) cy.get('[data-cy="update-button"]').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Invalid xss address input gets correct error message', () => { @@ -106,7 +107,7 @@ describe('Profile Edit', () => { }) cy.get('[data-cy="update-button"]').click() - cy.contains('Address contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.ADDRESS_INVALID).should('exist') }) it('Invalid xss postcode input gets correct error message', () => { @@ -122,7 +123,7 @@ describe('Profile Edit', () => { ) cy.get('[data-cy="update-button"]').click() - cy.contains('Invalid postcode').should('exist') + cy.contains(VALIDATION_MESSAGES.POSTCODE_INVALID).should('exist') }) it('Invalid xss mobile input gets correct error message', () => { @@ -135,7 +136,7 @@ describe('Profile Edit', () => { }) cy.get('[data-cy="update-button"]').click() - cy.contains('Mobile number contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.MOBILE_INVALID).should('exist') }) it('Invalid xss next-of-kin firstname input gets correct error message', () => { @@ -151,7 +152,7 @@ describe('Profile Edit', () => { ) cy.get('[data-cy="update-button"]').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Invalid xss next-of-kin lastname input gets correct error message', () => { @@ -167,7 +168,7 @@ describe('Profile Edit', () => { ) cy.get('[data-cy="update-button"]').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) - // nok email validation is handled by electron + // nok email validation is handled by electron/browser }) From d5590460f7c88c50e0e80fa514295484a1396237 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Thu, 27 Aug 2026 10:29:43 +1200 Subject: [PATCH 34/36] Move user-client cypress tests to use validation message constants --- .../cypress/e2e/registration.cy.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/application/user-client/cypress/e2e/registration.cy.js b/application/user-client/cypress/e2e/registration.cy.js index a50c51676..b135f65b1 100644 --- a/application/user-client/cypress/e2e/registration.cy.js +++ b/application/user-client/cypress/e2e/registration.cy.js @@ -1,5 +1,6 @@ /// const { TestUsers, TestInvites, TestStudies } = require('../../../common/testing/constants') +const { VALIDATION_MESSAGES } = require('../../../common/src/validation') beforeEach(() => { cy.task('reset') @@ -71,8 +72,8 @@ describe('registration', () => { cy.get('[data-cy="reg-mobile"]').type('04123') cy.get('[data-cy="reg-button"]').click() cy.contains('Invalid password').should('exist') - cy.contains('Invalid postcode').should('exist') - cy.contains('Mobile number contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.POSTCODE_INVALID).should('exist') + cy.contains(VALIDATION_MESSAGES.MOBILE_INVALID).should('exist') cy.contains('at least 14 characters').should('exist') cy.contains('passwords do not match').should('exist') }) @@ -130,17 +131,17 @@ describe('registration', () => { parseSpecialCharSequences: false, }) // hit enter after the last form field - // NOK email seems to have it's own xss protection in electron + // NOK email seems to have it's own xss protection in electron/browser cy.get('[data-cy="nok-surname"]') .type("{{7*7}}$#", { parseSpecialCharSequences: false, }) .type('{enter}') - cy.contains('Name contains invalid characters').should('exist') - cy.contains('Address contains invalid characters').should('exist') - cy.contains('Mobile number contains invalid characters').should('exist') - cy.contains('Invalid postcode').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') + cy.contains(VALIDATION_MESSAGES.ADDRESS_INVALID).should('exist') + cy.contains(VALIDATION_MESSAGES.MOBILE_INVALID).should('exist') + cy.contains(VALIDATION_MESSAGES.POSTCODE_INVALID).should('exist') }) it('Attempt to register existing email (i.e. no invite) and get correct error message', () => { @@ -165,7 +166,7 @@ describe('registration', () => { fillValid() cy.get('[data-cy="add-dependent"]').click() cy.get('[data-cy="reg-button"]').click() - cy.contains('This field is required').should('exist') + cy.contains(VALIDATION_MESSAGES.REQUIRED).should('exist') cy.get('[data-cy="add-dependent"]').click() cy.get('[data-cy=dep-first]').eq(1).type('JNR') cy.get('[data-cy=dep-surname]').eq(1).type('LAST') @@ -196,6 +197,6 @@ describe('registration', () => { parseSpecialCharSequences: false, }) .type('{enter}') - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) }) From 852e6dc8ba7b35cb6a8a8c7d27cd071eb3b61be8 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Thu, 27 Aug 2026 10:46:33 +1200 Subject: [PATCH 35/36] Move admin-client cypress tests to use validation message constants --- .../admin-client/cypress/e2e/families.cy.js | 17 +++++++++-- .../cypress/e2e/participants.cy.js | 29 ++++++++++--------- .../admin-client/cypress/e2e/setup.cy.js | 3 +- .../admin-client/cypress/e2e/users.cy.js | 21 +++++++------- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/application/admin-client/cypress/e2e/families.cy.js b/application/admin-client/cypress/e2e/families.cy.js index 4934631e0..b7f1e56bd 100644 --- a/application/admin-client/cypress/e2e/families.cy.js +++ b/application/admin-client/cypress/e2e/families.cy.js @@ -1,6 +1,7 @@ /// const { TestUsers } = require('../../../common/testing/constants') +const { VALIDATION_MESSAGES } = require('../../../common/src/validation') beforeEach(() => { cy.task('reset') @@ -41,7 +42,7 @@ describe('Family Editing', () => { cy.get('[data-cy="in-study-checkbox"] input').last().should('be.checked') }) - it('Cannot use xss when adding new dependent to family', () => { + it('Cannot use xss first name when adding new dependent to family', () => { cy.visit('/participants/family/edit/100') cy.get('[data-cy="add-member-button"]').click() cy.get('[data-cy="registered-no"]').click() @@ -49,12 +50,24 @@ describe('Family Editing', () => { cy.get('[data-cy=dep-first]').type("{{7*7}}$#", { parseSpecialCharSequences: false, }) + cy.get('[data-cy=dep-surname]').type('Smith') + cy.get('[data-cy="dep-dob"]').type('2020-01-01') + cy.get('[data-cy="add-dep-button"]').click() + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') + }) + + it('Cannot use xss surname when adding new dependent to family', () => { + cy.visit('/participants/family/edit/100') + cy.get('[data-cy="add-member-button"]').click() + cy.get('[data-cy="registered-no"]').click() + cy.get('[data-cy="new-dependent"]').click() + cy.get('[data-cy=dep-first]').type('Alfred') cy.get('[data-cy=dep-surname]').type("{{7*7}}$#", { parseSpecialCharSequences: false, }) cy.get('[data-cy="dep-dob"]').type('2020-01-01') cy.get('[data-cy="add-dep-button"]').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Remove member from family', () => { diff --git a/application/admin-client/cypress/e2e/participants.cy.js b/application/admin-client/cypress/e2e/participants.cy.js index e970bb596..f3eb7835c 100644 --- a/application/admin-client/cypress/e2e/participants.cy.js +++ b/application/admin-client/cypress/e2e/participants.cy.js @@ -1,6 +1,7 @@ /// const { TestUsers } = require('../../../common/testing/constants') +const { VALIDATION_MESSAGES } = require('../../../common/src/validation') beforeEach(() => { cy.task('reset') @@ -34,11 +35,11 @@ describe('Participants', () => { cy.get('input[name="profile.postcode"]').clear().type('222a') cy.get('input[name="externalId"]').clear().type('extID') cy.contains('Save').click() - cy.contains('Invalid postcode').should('exist') + cy.contains(VALIDATION_MESSAGES.POSTCODE_INVALID).should('exist') cy.get('input[name="profile.postcode"]').clear().type('2222') cy.get('input[name="profile.nextOfKin.email"]').clear().type('invalid') cy.contains('Save').click() - cy.contains('Enter a valid email').should('exist') + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') cy.get('input[name="profile.nextOfKin.email"]').clear().type('valid@email.com') cy.contains('Save').click() cy.url().should('contain', `participants/${TestUsers.PARTICIPANT_UNANSWERED.id}`) @@ -79,7 +80,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Edit participant, validate xss last name', () => { @@ -92,7 +93,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Edit participant, validate xss email', () => { @@ -105,7 +106,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Enter a valid email').should('exist') + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') }) it('Edit participant, validate xss externalId', () => { @@ -118,7 +119,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('External ID can only consist of').should('exist') + cy.contains(VALIDATION_MESSAGES.EXTERNALID_INVALID).should('exist') }) it('Edit participant, validate xss address', () => { @@ -131,7 +132,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Address contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.ADDRESS_INVALID).should('exist') }) it('Edit participant, validate xss suburb', () => { @@ -144,7 +145,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Address contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.ADDRESS_INVALID).should('exist') }) it('Edit participant, validate xss postcode', () => { @@ -157,7 +158,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Invalid postcode').should('exist') + cy.contains(VALIDATION_MESSAGES.POSTCODE_INVALID).should('exist') }) it('Edit participant, validate xss mobile', () => { @@ -170,7 +171,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Mobile number contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.MOBILE_INVALID).should('exist') }) it('Edit participant, validate xss nok first name', () => { @@ -183,7 +184,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Edit participant, validate xss nok last name', () => { @@ -196,7 +197,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Edit participant, validate xss nok email', () => { @@ -209,7 +210,7 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Enter a valid email').should('exist') + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') }) it('Edit participant, validate xss nok mobile', () => { @@ -222,6 +223,6 @@ describe('Participants', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Mobile number contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.MOBILE_INVALID).should('exist') }) }) diff --git a/application/admin-client/cypress/e2e/setup.cy.js b/application/admin-client/cypress/e2e/setup.cy.js index 5eeb499d5..5fa855317 100644 --- a/application/admin-client/cypress/e2e/setup.cy.js +++ b/application/admin-client/cypress/e2e/setup.cy.js @@ -1,5 +1,6 @@ /// const { TestUsers } = require('../../../common/testing/constants') +const { VALIDATION_MESSAGES } = require('../../../common/src/validation') describe('Setup', () => { it('Redirects to setup page if database empty, can register', () => { @@ -26,7 +27,7 @@ describe('Setup', () => { ) cy.get('[data-cy="setup-password"]').type(TestUsers.ORG_ADMIN.password) // Using test data to conform to pr requirements cy.get('[data-cy="setup-submit"]').click() - cy.contains('Enter a valid email').should('exist') + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') }) it('Cannot register with weak password', () => { diff --git a/application/admin-client/cypress/e2e/users.cy.js b/application/admin-client/cypress/e2e/users.cy.js index 146cea9da..b590dfff3 100644 --- a/application/admin-client/cypress/e2e/users.cy.js +++ b/application/admin-client/cypress/e2e/users.cy.js @@ -1,6 +1,7 @@ /// const { TestUsers } = require('../../../common/testing/constants') +const { VALIDATION_MESSAGES } = require('../../../common/src/validation') beforeEach(() => { cy.task('reset') @@ -26,7 +27,7 @@ describe('Users', () => { cy.get('input').eq(1).type('Presley') cy.get('input').eq(2).type('elvisexample.com') cy.contains('Save').click() - cy.contains('Enter a valid email').should('exist') + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') cy.get('input').eq(2).clear().type('elvis@example.com') cy.contains('Save').click() cy.contains('Created at').should('exist') @@ -38,7 +39,7 @@ describe('Users', () => { cy.get('input').eq(0).type('A') cy.get('input').eq(2).clear().type('elvisexample.com') cy.contains('Save').click() - cy.contains('Enter a valid email').should('exist') + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') cy.get('input').eq(2).clear().type('elvis@example.com') cy.contains('Save').click() cy.contains('Success').should('exist') @@ -55,7 +56,7 @@ describe('Users', () => { cy.get('input').eq(1).type('Presley') cy.get('input').eq(2).type('elvis@example.com') cy.contains('Save').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Create user, check validation of xss lastname input', () => { cy.visit('/users') @@ -67,7 +68,7 @@ describe('Users', () => { }) cy.get('input').eq(2).type('elvis@example.com') cy.contains('Save').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Create user, check validation of xss email input', () => { cy.visit('/users') @@ -75,11 +76,11 @@ describe('Users', () => { cy.url().should('contain', '/users/create') cy.get('[data-cy="create-first"]').type('Elvis') cy.get('input').eq(1).type('Presley') - cy.get('input').eq(2).type("{{7*7}}@gmail.com", { + cy.get('input').eq(2).type("{{7*7}}@gmail.com", { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Enter a valid email').should('exist') + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') }) it('Edit user, check validation of xss firstname input', () => { cy.visit('/users') @@ -88,7 +89,7 @@ describe('Users', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Edit user, check validation of xss lastname input', () => { cy.visit('/users') @@ -97,16 +98,16 @@ describe('Users', () => { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Name contains invalid characters').should('exist') + cy.contains(VALIDATION_MESSAGES.NAME_INVALID).should('exist') }) it('Edit user, check validation of xss email input', () => { cy.visit('/users') cy.get('[data-cy="edit-button"]').eq(1).click() cy.get('input').eq(0).type('A') - cy.get('input').eq(2).clear().type("{{7*7}}@gmail.com", { + cy.get('input').eq(2).clear().type("{{7*7}}@gmail.com", { parseSpecialCharSequences: false, }) cy.contains('Save').click() - cy.contains('Enter a valid email').should('exist') + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') }) }) From 9fc974e454402fc82fd8cfdb860c6d45f6ee08a7 Mon Sep 17 00:00:00 2001 From: ignatiusm Date: Thu, 27 Aug 2026 13:17:17 +1200 Subject: [PATCH 36/36] Move admin-client settings tests to use validation message constants --- .../admin-client/cypress/e2e/settings.cy.js | 107 ++++++++++++++---- .../admin-client/src/pages/settings/index.tsx | 16 +-- application/common/src/validation.ts | 10 ++ 3 files changed, 95 insertions(+), 38 deletions(-) diff --git a/application/admin-client/cypress/e2e/settings.cy.js b/application/admin-client/cypress/e2e/settings.cy.js index 50eb41f30..119ab5626 100644 --- a/application/admin-client/cypress/e2e/settings.cy.js +++ b/application/admin-client/cypress/e2e/settings.cy.js @@ -1,6 +1,7 @@ /// const { TestUsers } = require('../../../common/testing/constants') +const { VALIDATION_MESSAGES } = require('../../../common/src/validation') beforeEach(() => { cy.task('reset') @@ -52,31 +53,6 @@ describe('Settings page', () => { ) }) - it('Invalid values prevent saving and show appropriate error messages', () => { - const values = { ...fieldMap, primaryColour: 'abc' } - - cy.visit('/settings') - - cy.get('[data-cy="tcLink"] input').should( - 'have.value', - 'https://garvan-data-science-platform.github.io/ctrl-docs/docs/terms-and-conditions', - ) - - for (const [key, value] of Object.entries(values)) { - cy.get(`[data-cy="${key}"] input`).clear().type(value) - } - cy.get('[data-cy="save-button"]').click() - - cy.contains('Invalid colour').should('exist') - - for (const [key, value] of Object.entries(fieldMap)) { - cy.get(`[data-cy="${key}"] input`).clear().type(value) - } - - cy.contains('Invalid colour').should('not.exist') - cy.contains('Invalid url').should('not.exist') - }) - it('Can upload a logo', () => { cy.visit('/settings') cy.uploadCommonFile('[data-cy="logo-upload"]', 'valid_logo.png') @@ -160,4 +136,85 @@ describe('Settings page', () => { cy.contains('Deleted logo').should('exist') cy.get('[data-cy="logo-preview"]').should('not.exist') }) + + it('Invalid tcLink url prevent saving and show appropriate error messages', () => { + cy.visit('/settings') + + cy.get('[data-cy="tcLink"] input').clear().type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + + cy.get('[data-cy="save-button"]').click() + + cy.contains(VALIDATION_MESSAGES.URL_INVALID).should('exist') + }) + + it('Invalid newsLink url prevent saving and show appropriate error messages', () => { + cy.visit('/settings') + + cy.get('[data-cy="newsLink"] input') + .clear() + .type("{{7*7}}", { + parseSpecialCharSequences: false, + }) + + cy.get('[data-cy="save-button"]').click() + + cy.contains(VALIDATION_MESSAGES.URL_INVALID).should('exist') + }) + + it('Invalid primary colour prevent saving and show appropriate error messages', () => { + const values = { ...fieldMap, primaryColour: 'abc' } + + cy.visit('/settings') + + for (const [key, value] of Object.entries(values)) { + cy.get(`[data-cy="${key}"] input`).clear().type(value) + } + cy.get('[data-cy="save-button"]').click() + + cy.contains('Invalid colour').should('exist') + + for (const [key, value] of Object.entries(fieldMap)) { + cy.get(`[data-cy="${key}"] input`).clear().type(value) + } + + cy.contains('Invalid colour').should('not.exist') + }) + + it('Invalid xss primary colour prevent saving and show appropriate error messages', () => { + const values = { + ...fieldMap, + primaryColour: "{{7*7}}", + } + + cy.visit('/settings') + + for (const [key, value] of Object.entries(values)) { + cy.get(`[data-cy="${key}"] input`).clear().type(value, { + parseSpecialCharSequences: false, + }) + } + cy.get('[data-cy="save-button"]').click() + + cy.contains('Invalid colour').should('exist') //TODO: move to common? + }) + + it('Invalid xss secondary colour prevent saving and show appropriate error messages', () => { + const values = { + ...fieldMap, + secondaryColour: "{{7*7}}", + } + + cy.visit('/settings') + + for (const [key, value] of Object.entries(values)) { + cy.get(`[data-cy="${key}"] input`).clear().type(value, { + parseSpecialCharSequences: false, + }) + } + cy.get('[data-cy="save-button"]').click() + + cy.contains('Invalid colour').should('exist') //TODO: move to common? + }) }) diff --git a/application/admin-client/src/pages/settings/index.tsx b/application/admin-client/src/pages/settings/index.tsx index 1cd59e4c5..338d24a73 100644 --- a/application/admin-client/src/pages/settings/index.tsx +++ b/application/admin-client/src/pages/settings/index.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router' import { axiosInstance } from '../../providers/dataProvider' import { Info } from '@mui/icons-material' import { LogoUploader } from '../../components/LogoUploader' -import { REGEX } from '@common/types/commonTypes' +import { urlRules } from '@common/src/validation' import { RESOURCES } from '../../constants' const SettingsPage = () => { @@ -87,12 +87,7 @@ const SettingsPage = () => { { data-cy="tcLink" /> ({ required: required ? VALIDATION_MESSAGES.REQUIRED : false, @@ -60,3 +61,12 @@ export const externalIdRules = (required = false) => ({ message: VALIDATION_MESSAGES.EXTERNALID_INVALID, }, }) + +// Default is not required +export const urlRules = (required = false) => ({ + required: required ? VALIDATION_MESSAGES.REQUIRED : false, + pattern: { + value: REGEX.URL, + message: VALIDATION_MESSAGES.URL_INVALID, + }, +})