diff --git a/application/admin-client/cypress/e2e/families.cy.js b/application/admin-client/cypress/e2e/families.cy.js index adc204ff6..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,6 +42,34 @@ describe('Family Editing', () => { cy.get('[data-cy="in-study-checkbox"] input').last().should('be.checked') }) + 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() + 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('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(VALIDATION_MESSAGES.NAME_INVALID).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/cypress/e2e/participants.cy.js b/application/admin-client/cypress/e2e/participants.cy.js index 5bc4def1c..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('Invalid 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}`) @@ -68,4 +69,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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.EMAIL_INVALID).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(VALIDATION_MESSAGES.EXTERNALID_INVALID).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(VALIDATION_MESSAGES.ADDRESS_INVALID).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(VALIDATION_MESSAGES.ADDRESS_INVALID).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(VALIDATION_MESSAGES.POSTCODE_INVALID).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(VALIDATION_MESSAGES.MOBILE_INVALID).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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.EMAIL_INVALID).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(VALIDATION_MESSAGES.MOBILE_INVALID).should('exist') + }) }) 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/cypress/e2e/setup.cy.js b/application/admin-client/cypress/e2e/setup.cy.js index 5927133b1..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', () => { @@ -13,4 +14,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(VALIDATION_MESSAGES.EMAIL_INVALID).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/cypress/e2e/users.cy.js b/application/admin-client/cypress/e2e/users.cy.js index cf3f87cfb..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('Invalid 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,11 +39,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(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') 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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.EMAIL_INVALID).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(VALIDATION_MESSAGES.NAME_INVALID).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(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", { + parseSpecialCharSequences: false, + }) + cy.contains('Save').click() + cy.contains(VALIDATION_MESSAGES.EMAIL_INVALID).should('exist') + }) }) 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/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())} /> { 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'} /> { @@ -87,12 +87,7 @@ const SettingsPage = () => { { data-cy="tcLink" /> { 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: emailRegex, - message: 'Enter a valid email', - }, - })} + {...register('email', emailRules())} /> { - 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..3dd3289e4 100644 --- a/application/admin-client/src/pages/users/create.tsx +++ b/application/admin-client/src/pages/users/create.tsx @@ -1,9 +1,9 @@ -import { emailRegex } from '@common/src/regex' import { Box, MenuItem, TextField } from '@mui/material' import { useGetIdentity } from '@refinedev/core' import { Create } from '@refinedev/mui' import { useForm } from '@refinedev/react-hook-form' import { Controller } from 'react-hook-form' +import { emailRules, nameRules } from '@common/src/validation' export const UserCreate = () => { const { @@ -20,9 +20,7 @@ export const UserCreate = () => { { name="firstName" /> { name="lastName" /> emailRegex.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 3eb6cd6fb..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 { emailRegex } from '@common/src/regex' +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} /> - emailRegex.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/backend/src/controllers/AuthController.test.ts b/application/backend/src/controllers/AuthController.test.ts index 009f1152f..59d022950 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 () => { @@ -135,6 +135,36 @@ 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.OrganisationAdmin, + } + + 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 an error if the user is already registered', async () => { const registerRequest: RegisterRequest = { firstName: testFirstName, @@ -280,7 +310,7 @@ describe('AuthController', () => { lastName: testLastName, email: TestInvites.INVITE_PENDING.email, password: testPassword, - mobile: '+61477777777', + mobile: '0477777777', addressLine: '123 Some Street', suburb: 'Sydney', postcode: '2000', @@ -369,6 +399,74 @@ 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', + }, + dependents: [ + { + firstName: 'John{7*7}', + lastName: '', + dob: '2020-01-01', + permanent: false, + }, + ], + } + + 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', + }, + 'dependents.$0.firstName': { + message: 'Invalid value provided', + }, + 'dependents.$0.lastName': { + message: 'Invalid value provided', + }, + }) + }) + it('Should add dependent profiles if provided, should have same family id', async () => { const registerParticipantRequest: RegisterParticipantRequest = { ...registerParticipantRequestBase, @@ -562,6 +660,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 +697,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 +714,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/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', + }, + }) + }) }) }) 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/backend/src/routes.ts b/application/backend/src/routes.ts index d55fc09c3..9df61cedb 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":"^[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":"^[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": { + "dataType": "refAlias", + "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": { "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, @@ -220,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":{"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":{"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": { @@ -232,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"}}, @@ -244,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":"^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": { "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, }, @@ -281,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}, }, @@ -347,6 +402,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":{"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": { "dataType": "refAlias", "type": {"dataType":"array","array":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"boolean"},{"dataType":"enum","enums":[null]}]},"validators":{}}, @@ -365,7 +425,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 +520,37 @@ 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":{"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":{"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\\-_=.:]+$"}}}, + }, + // 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, }, @@ -483,7 +558,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, }, @@ -501,16 +576,41 @@ 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, }, // 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":"^[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": { + "dataType": "refAlias", + "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": { + "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 "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 + "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 "ContactMethod": { "dataType": "refEnum", "enums": ["EMAIL","MOBILE","MAIL"], @@ -524,11 +624,11 @@ const models: TsoaRoute.Models = { "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, }, @@ -536,10 +636,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}, }, @@ -549,7 +649,7 @@ 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, }, @@ -557,22 +657,40 @@ const models: TsoaRoute.Models = { "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, }, // 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"}],"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":"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": { - "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": { @@ -591,9 +709,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"}, @@ -613,14 +731,14 @@ 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, }, // 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": { @@ -639,7 +757,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, }, @@ -652,7 +770,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, }, @@ -670,11 +788,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 - "Email": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{"pattern":{"errorMsg":"Please provide valid email","value":"^(.+)@(.+)$"}}}, - }, - // 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_": { "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":{}}, @@ -684,7 +797,7 @@ const models: TsoaRoute.Models = { "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, }, @@ -698,12 +811,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, }, @@ -711,8 +834,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, }, @@ -751,10 +874,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, }, @@ -762,33 +890,28 @@ 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, }, // 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, @@ -812,10 +935,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, }, @@ -861,12 +989,12 @@ const models: TsoaRoute.Models = { "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, }, @@ -884,8 +1012,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, }, @@ -895,7 +1023,7 @@ 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, }, @@ -903,22 +1031,22 @@ const models: TsoaRoute.Models = { "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 +1056,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 +1087,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..a654a9e2f 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,27 @@ "type": "object", "additionalProperties": false }, + "FirstName": { + "type": "string", + "example": "John", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$" + }, + "LastName": { + "type": "string", + "example": "Doe", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$" + }, + "Email": { + "type": "string", + "example": "john.doe@email.com", + "minLength": 1, + "maxLength": 254, + "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" + }, "Role": { "type": "string", "enum": [ @@ -253,22 +274,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 +300,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 +364,7 @@ "token" ], "type": "object", - "additionalProperties": false, - "example": { - "newPassword": "Newsupersecret123", - "token": "1063e00e4a273e698577" - } + "additionalProperties": false }, "SurveyVersionStatus": { "type": "string", @@ -419,6 +420,32 @@ "type": "object", "additionalProperties": false }, + "SurveyStepTitle": { + "type": "string", + "example": "Introduction to CTRL", + "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.", + "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 +472,10 @@ "SurveyQuestionChoices": { "properties": { "text": { - "type": "string" + "$ref": "#/components/schemas/SurveyQuestionText" }, "tooltip": { - "type": "string" + "$ref": "#/components/schemas/SurveyQuestionTooltip" }, "required": { "type": "boolean" @@ -477,10 +504,10 @@ "SurveyQuestionCheckbox": { "properties": { "text": { - "type": "string" + "$ref": "#/components/schemas/SurveyQuestionText" }, "tooltip": { - "type": "string" + "$ref": "#/components/schemas/SurveyQuestionTooltip" }, "required": { "type": "boolean" @@ -499,10 +526,17 @@ "type": "object", "additionalProperties": false }, + "Url": { + "type": "string", + "example": "https://redcap.orgname.com/api/", + "minLength": 1, + "maxLength": 128, + "pattern": "^https?:\\/\\/[a-zA-Z0-9\\-._~:/?#[\\]@!$&'()*+,;=%]+$" + }, "SurveyVideo": { "properties": { "link": { - "type": "string" + "$ref": "#/components/schemas/Url" } }, "required": [ @@ -511,10 +545,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 +669,10 @@ "SurveyStep": { "properties": { "title": { - "type": "string" + "$ref": "#/components/schemas/SurveyStepTitle" }, "text": { - "type": "string" + "$ref": "#/components/schemas/SurveyStepDescription" }, "last_updated": { "type": "string" @@ -798,6 +839,11 @@ "type": "object", "additionalProperties": false }, + "DoB": { + "type": "string", + "example": "2000-05-21", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, "SurveyStepAnswerArray": { "items": { "anyOf": [ @@ -840,13 +886,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 +1188,16 @@ "type": "object", "additionalProperties": false }, + "StudyName": { + "type": "string", + "example": "Acme Genomics Study", + "maxLength": 128, + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" + }, "CreateStudyRequest": { "properties": { "name": { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/StudyName" } }, "required": [ @@ -1155,23 +1206,34 @@ "type": "object", "additionalProperties": false }, + "StudyDescription": { + "type": "string", + "example": "This is a short description of the study", + "maxLength": 900, + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\\s.,!?:;()'\"\\-/#@&%$£€+]+$" + }, + "RedcapToken": { + "type": "string", + "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", @@ -1182,11 +1244,15 @@ "data": { "properties": { "newsLink": { - "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/Url" + } + ], "nullable": true }, "tcLink": { - "type": "string" + "$ref": "#/components/schemas/Url" }, "secondaryColour": { "type": "string", @@ -1246,7 +1312,11 @@ "data": { "properties": { "newsLink": { - "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/Url" + } + ], "nullable": true }, "secondaryColour": { @@ -1272,6 +1342,32 @@ "type": "object", "additionalProperties": false }, + "MiddleName": { + "type": "string", + "example": "William", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-ZÀ-ÖØ-öø-ɏ\\s\\-'.]+$" + }, + "Mobile": { + "type": "string", + "example": "0412341432", + "pattern": "^04\\d{8}$" + }, + "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 +1381,11 @@ ], "type": "string" }, + "Postcode": { + "type": "string", + "example": "1234", + "pattern": "^\\d{4}$" + }, "ContactMethod": { "enum": [ "EMAIL", @@ -1305,20 +1406,24 @@ "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": [ @@ -1332,16 +1437,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", @@ -1385,34 +1490,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", @@ -1442,13 +1547,13 @@ "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" @@ -1463,52 +1568,49 @@ "type": "object", "additionalProperties": false }, - "Partial_RegisterParticipantRequest_": { + "ExternalId": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000", + "maxLength": 128, + "pattern": "^[a-zA-Z0-9\\-_=.:]*$" + }, + "UpdateProfileRequest": { "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" @@ -1520,39 +1622,15 @@ "items": { "$ref": "#/components/schemas/OnBehalf" }, - "type": "array" + "type": "array", + "maxItems": 15 }, "externalId": { - "type": "string" + "$ref": "#/components/schemas/ExternalId" } }, "type": "object", - "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" - } - } + "additionalProperties": false }, "ParticipantAnswerStatus": { "properties": { @@ -1594,13 +1672,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", @@ -1657,16 +1735,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", @@ -1725,34 +1803,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", @@ -1815,10 +1893,10 @@ "items": { "properties": { "description": { - "type": "string" + "$ref": "#/components/schemas/StudyDescription" }, "studyName": { - "type": "string" + "$ref": "#/components/schemas/StudyName" }, "sentAt": { "type": "string" @@ -1834,7 +1912,7 @@ "format": "double" }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" }, "id": { "type": "string" @@ -1897,7 +1975,7 @@ "format": "double" }, "email": { - "type": "string" + "$ref": "#/components/schemas/Email" }, "id": { "type": "string" @@ -1962,10 +2040,6 @@ "type": "object", "additionalProperties": false }, - "Email": { - "type": "string", - "pattern": "^(.+)@(.+)$" - }, "Partial_GetParticipantProfileResponse-at-data_": { "properties": { "id": { @@ -2033,7 +2107,7 @@ "studyParticipant": { "properties": { "externalId": { - "type": "string" + "$ref": "#/components/schemas/ExternalId" } }, "type": "object" @@ -2058,6 +2132,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": { @@ -2067,10 +2155,10 @@ "type": "array" }, "subjectText": { - "type": "string" + "$ref": "#/components/schemas/InviteEmailSubject" }, "explanatoryText": { - "type": "string" + "$ref": "#/components/schemas/InviteEmailText" } }, "required": [ @@ -2079,23 +2167,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": [ @@ -2202,11 +2282,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": [ @@ -2218,92 +2304,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" } @@ -2314,10 +2325,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", @@ -2371,12 +2389,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": [ @@ -2483,27 +2506,22 @@ "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 +2532,7 @@ "role" ], "type": "object", - "additionalProperties": false, - "example": { - "firstName": "John", - "lastName": "Doe", - "email": "john.doe@email.com", - "password": "Supersecret123", - "role": "OrganisationAdmin" - } + "additionalProperties": false }, "SetupResponse": { "properties": { @@ -2581,12 +2592,10 @@ "RegisterSetupRequest": { "properties": { "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "password": { - "type": "string", - "minLength": 14 + "$ref": "#/components/schemas/Password" } }, "required": [ @@ -2594,11 +2603,7 @@ "password" ], "type": "object", - "additionalProperties": false, - "example": { - "email": "john.doe@email.com", - "password": "Supersecret123" - } + "additionalProperties": false }, "RegisterParticipantResponse": { "properties": { @@ -2610,7 +2615,7 @@ "type": "string" }, "role": { - "type": "string" + "$ref": "#/components/schemas/RoleT" } }, "required": [ @@ -2624,49 +2629,40 @@ "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 +2674,11 @@ "items": { "$ref": "#/components/schemas/OnBehalf" }, - "type": "array" + "type": "array", + "maxItems": 15 }, "externalId": { - "type": "string" + "$ref": "#/components/schemas/ExternalId" } }, "required": [ @@ -2701,30 +2698,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 +2710,7 @@ "format": "double" }, "role": { - "type": "string" + "$ref": "#/components/schemas/RoleT" } }, "required": [ @@ -2792,11 +2766,10 @@ "LoginRequest": { "properties": { "email": { - "type": "string", - "pattern": "^(.+)@(.+)$" + "$ref": "#/components/schemas/Email" }, "password": { - "type": "string" + "$ref": "#/components/schemas/Password" } }, "required": [ @@ -2804,11 +2777,7 @@ "password" ], "type": "object", - "additionalProperties": false, - "example": { - "email": "john.doe@email.com", - "password": "Supersecret123" - } + "additionalProperties": false }, "OTPLoginRequest": { "properties": { 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', 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({ 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 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..3ce451d1c --- /dev/null +++ b/application/common/src/validation.ts @@ -0,0 +1,72 @@ +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 -_=.:', + URL_INVALID: 'Invalid URL, must include http(s)://...', +} +export const nameRules = (required = true) => ({ + required: required ? VALIDATION_MESSAGES.REQUIRED : false, + pattern: { + value: REGEX.NAME, + message: VALIDATION_MESSAGES.NAME_INVALID, + }, +}) + +export const emailRules = (required = true) => ({ + required: required ? VALIDATION_MESSAGES.REQUIRED : false, + pattern: { + value: REGEX.EMAIL, + message: VALIDATION_MESSAGES.EMAIL_INVALID, + }, +}) + +export const addressRules = (required = true) => ({ + required: required ? VALIDATION_MESSAGES.REQUIRED : false, + pattern: { + value: REGEX.ADDRESS, + message: VALIDATION_MESSAGES.ADDRESS_INVALID, + }, +}) + +// DOB is handled by date picker +// State is handled by drop down + +export const postcodeRules = (required = true) => ({ + required: required ? VALIDATION_MESSAGES.REQUIRED : false, + pattern: { + value: REGEX.POSTCODE, + message: VALIDATION_MESSAGES.POSTCODE_INVALID, + }, +}) + +export const mobileRules = (required = true) => ({ + required: required ? VALIDATION_MESSAGES.REQUIRED : false, + pattern: { + value: REGEX.MOBILE, + message: VALIDATION_MESSAGES.MOBILE_INVALID, + }, +}) + +// Default is not required +export const externalIdRules = (required = false) => ({ + required: required ? VALIDATION_MESSAGES.REQUIRED : false, + pattern: { + value: REGEX.EXTERNALID, + 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, + }, +}) 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..e02f853bf 100644 --- a/application/common/types/api/auth/registerParticipant.ts +++ b/application/common/types/api/auth/registerParticipant.ts @@ -1,3 +1,17 @@ +import { + AddressLine, + DoB, + Email, + ExternalId, + FirstName, + LastName, + MiddleName, + Mobile, + Password, + Postcode, + RoleT, + Suburb, +} from '../../commonTypes' import type { AlternativeContact, ContactMethod, @@ -6,85 +20,33 @@ 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 { id: number token: string - role: string + role: RoleT } 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 } 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/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/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/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/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 } } 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/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/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..54cceb890 100644 --- a/application/common/types/api/users/updateProfile.ts +++ b/application/common/types/api/users/updateProfile.ts @@ -1,27 +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' -/** - * @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 +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/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 new file mode 100644 index 000000000..2c019e986 --- /dev/null +++ b/application/common/types/commonTypes.ts @@ -0,0 +1,223 @@ +// 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, 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: /^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()@:%_+.~#?&//=]*)$/, +} + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ\s\-'.]+$ + * @example "John" + */ +export type FirstName = string + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ\s\-'.]+$ + * @example "William" + */ +export type MiddleName = string + +/** + * @minLength 1 + * @maxLength 100 + * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ\s\-'.]+$ + * @example "Doe" + */ +export type LastName = string + +/** + * @minLength 1 + * @maxLength 254 + * @pattern ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ + * @example "john.doe@email.com" + */ +export type Email = string + +/** + * @minLength 14 + * @maxLength 128 + * @example "Supersecret123" + */ +export type Password = string + +/** + * @pattern ^04\d{8}$ + * @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 + +/** + * @pattern ^\d{4}-\d{2}-\d{2}$ + * @example "2000-05-21" + */ +export type DoB = string + +/** + * @maxLength 128 + * @pattern ^[a-zA-Z0-9\-_=.:]*$ + * @example "123e4567-e89b-12d3-a456-426614174000" + */ +export type ExternalId = string + +/** + * @example "OrganisationAdmin" + */ +export type RoleT = Role + +// TODO: add annotations? +export type UserT = User + +// * @minLength 1 //TODO: check if we want minLength for study name +/** + * @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 +/** + * @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 + +/** + * @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 + +// 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 + * @pattern ^https?:\/\/[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]+$ + * @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 + +/** + * @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 + +/** + * @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 + +// * @minLength 1 TODO: confirm that an empty title is okay +// * @pattern ^[a-zA-ZÀ-ÖØ-öø-ɏ0-9\s.,!?:;()'"\-/#@&%$£€+]+$ +/** + * @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 TODO: confirm that an empty description is okay +// * @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 "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/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 } } diff --git a/application/common/types/survey.ts b/application/common/types/survey.ts index c3855794e..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 + 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' 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') }) diff --git a/application/user-client/cypress/e2e/profileEdit.cy.js b/application/user-client/cypress/e2e/profileEdit.cy.js index b13c2bf52..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('Invalid mobile').should('exist') + cy.contains(VALIDATION_MESSAGES.MOBILE_INVALID).should('exist') }) it('Shows family members correctly', () => { @@ -67,4 +68,107 @@ 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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.ADDRESS_INVALID).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(VALIDATION_MESSAGES.POSTCODE_INVALID).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(VALIDATION_MESSAGES.MOBILE_INVALID).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(VALIDATION_MESSAGES.NAME_INVALID).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(VALIDATION_MESSAGES.NAME_INVALID).should('exist') + }) + // nok email validation is handled by electron/browser }) diff --git a/application/user-client/cypress/e2e/registration.cy.js b/application/user-client/cypress/e2e/registration.cy.js index 579d03a0e..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,13 +72,13 @@ 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('Invalid mobile number').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') }) - 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 +94,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/browser + cy.get('[data-cy="nok-surname"]') + .type("{{7*7}}$#", { + parseSpecialCharSequences: false, + }) + .type('{enter}') + + 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', () => { cy.visit('/register/not-a-real-inviteId') fillValid() @@ -115,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') @@ -125,4 +176,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(VALIDATION_MESSAGES.NAME_INVALID).should('exist') + }) }) diff --git a/application/user-client/src/pages/ProfileEdit.tsx b/application/user-client/src/pages/ProfileEdit.tsx index 85546f3c9..8f57d2cf0 100644 --- a/application/user-client/src/pages/ProfileEdit.tsx +++ b/application/user-client/src/pages/ProfileEdit.tsx @@ -21,6 +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, + emailRules, + mobileRules, + nameRules, + postcodeRules, +} from '@common/src/validation' interface FormValues { firstName: string @@ -126,7 +133,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 +203,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 })} /> Preferred Contact Method @@ -248,8 +250,9 @@ export default function ProfileEdit() { error={Boolean(errors.nok_first)} helperText={errors.nok_first?.message} data-cy="update-nok-first" + disabled={isPending} {...register('nok_first', { - required: true, + ...nameRules(), value: data?.nextOfKin?.firstName, })} /> @@ -259,9 +262,11 @@ export default function ProfileEdit() { defaultValue="." error={Boolean(errors.nok_surname)} helperText={errors.nok_surname?.message} + data-cy="update-nok-last" + disabled={isPending} key="nok_surname" {...register('nok_surname', { - required: true, + ...nameRules(), value: data?.nextOfKin?.lastName, })} /> @@ -273,9 +278,11 @@ export default function ProfileEdit() { defaultValue="." error={Boolean(errors.nok_email)} helperText={errors.nok_email?.message} + data-cy="update-nok-email" + disabled={isPending} key="nok_email" {...register('nok_email', { - required: true, + ...emailRules(), value: data?.nextOfKin?.email, })} /> diff --git a/application/user-client/src/pages/Register.tsx b/application/user-client/src/pages/Register.tsx index 787529e6c..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 { emailRegex } from '@common/src/regex' +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())} />