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())}
/>