Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions application/admin-client/src/pages/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'
import { axiosInstance } from '../../providers/dataProvider'
import { Info } from '@mui/icons-material'
import { LogoUploader } from '../../components/LogoUploader'
import { urlRegex } from '@common/src/regex'
import { RESOURCES } from '../../constants'

const SettingsPage = () => {
Expand Down Expand Up @@ -88,8 +89,7 @@ const SettingsPage = () => {
<TextField
{...register('tcLink', {
pattern: {
value:
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/, //eslint-disable-line
value: urlRegex, //eslint-disable-line
message: 'Invalid url, must include http(s)://...',
},
})}
Expand All @@ -105,8 +105,7 @@ const SettingsPage = () => {
<TextField
{...register('newsLink', {
pattern: {
value:
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/, //eslint-disable-line
value: urlRegex, //eslint-disable-line
message: 'Invalid url, must include http(s)://...',
},
})}
Expand Down
1 change: 1 addition & 0 deletions application/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"packageManager": "yarn@4.0.2",
"dependencies": {
"@braintree/sanitize-url": "^7.1.2",
"@prisma/client": "^5.20.0",
"@tsoa/runtime": "^6.6.0",
"ajv": "^8.18.0",
Expand Down
22 changes: 22 additions & 0 deletions application/backend/src/controllers/SettingsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ describe('SettingsController', () => {

expect(response.status).toBe(422)
})

it('should fail to update if newsLink does not match URL Regex', async () => {
const reqBody = { newsLink: 'string' }

const response = await request(app)
.patch('/settings')
.set({ Authorization: `Bearer ${orgAdminToken}` })
.send(reqBody)

expect(response.status).toBe(422)
})

it('should fail to update if tcLink does not match URL Regex', async () => {
const reqBody = { tcLink: 'string' }

const response = await request(app)
.patch('/settings')
.set({ Authorization: `Bearer ${orgAdminToken}` })
.send(reqBody)

expect(response.status).toBe(422)
})
})

describe('GET /settings/userportal', () => {
Expand Down
29 changes: 28 additions & 1 deletion application/backend/src/controllers/SettingsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
Post,
UploadedFile,
Security,
ValidateError,
} from 'tsoa'
import { Readable } from 'stream'
import logger from 'common/src/logger'
import type {
GetSettingsResponse,
GetUserPortalSettingsResponse,
Expand All @@ -29,6 +31,17 @@ 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 { sanitizeUrl } from '@braintree/sanitize-url'

function sanitiseAndValidateUrl(link: string) {
const validatedUrl = sanitizeUrl(link)
if (urlRegex.test(validatedUrl)) {
return validatedUrl
} else {
throw new Error('urlRegex failed')
}
}

@Route('settings')
@Tags('Settings')
Expand Down Expand Up @@ -71,7 +84,21 @@ export class SettingsController extends Controller {
@Security('jwt', ['OrganisationAdmin'])
@Response<ValidateErrorResponse>('422', 'Validation Failed')
public async updateSettings(@Body() bodyRequest: UpdateSettingsRequest) {
await prisma.organisation.update({ where: { id: 1 }, data: bodyRequest })
const data = bodyRequest
// There is already validation on Admin Portal UI but best to not store unsanitized data
Object.keys(data)
.filter((urlKey) => urlKey === 'newsLink' || urlKey === 'tcLink')
.forEach((key) => {
try {
data[key] = sanitiseAndValidateUrl(data[key] as string)
} catch (err) {
const errorMessage: string = `Link is not acceptable URL: ${data.newsLink}`
logger.error({ errorMessage, err })
throw new ValidateError({}, errorMessage)
}
})

await prisma.organisation.update({ where: { id: 1 }, data: data })
}

@Get('/userportal')
Expand Down
3 changes: 3 additions & 0 deletions application/common/src/regex.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
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()@:%_+.~#?&//=]*)$/
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,13 @@ __metadata:
languageName: node
linkType: hard

"@braintree/sanitize-url@npm:^7.1.2":
version: 7.1.2
resolution: "@braintree/sanitize-url@npm:7.1.2"
checksum: 10c0/62f2aa0cf58626e3880b2dc1025c42064b4639abd157ae4e1c35f4c2f5031e9273772046a423979845069c814e27ff818e8e669280dc53585e6f033d5b7a59cb
languageName: node
linkType: hard

"@chakra-ui/anatomy@npm:2.2.2":
version: 2.2.2
resolution: "@chakra-ui/anatomy@npm:2.2.2"
Expand Down Expand Up @@ -6517,6 +6524,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "backend@workspace:application/backend"
dependencies:
"@braintree/sanitize-url": "npm:^7.1.2"
"@jest/globals": "npm:^29.7.0"
"@prisma/client": "npm:^5.20.0"
"@tsoa/runtime": "npm:^6.6.0"
Expand Down
Loading