Merge pull request #674 from nowcommunity/backend-functionality-for-editing-persons

Backend functionality for editing persons
This commit is contained in:
Kari Lintulaakso
2025-02-27 11:03:27 +02:00
committed by GitHub
6 changed files with 253 additions and 5 deletions
+26
View File
@@ -0,0 +1,26 @@
import { PersonDetailsType } from '../../../../frontend/src/shared/types'
export const existingPerson: Omit<PersonDetailsType, 'user'> = {
initials: 'AD',
first_name: 'adf',
surname: 'ads',
full_name: 'adf ads',
format: null,
email: 'email',
user_id: 156,
organization: 'organization',
country: 'Finland',
password_set: new Date('2024-05-22T00:00:00.000Z'),
used_morph: null,
used_now: null,
used_gene: null,
}
export const editedPerson = {
initials: existingPerson.initials,
first_name: 'Test first name',
surname: 'Test surname',
email: 'test.email@emailprovider.com',
organization: 'Test organization',
country: 'Test country',
}
+115
View File
@@ -0,0 +1,115 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'
import { PersonDetailsType } from '../../../../frontend/src/shared/types'
import { pool } from '../../utils/db'
import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils'
import { editedPerson, existingPerson } from './data'
let updatedPerson: PersonDetailsType | null = null
describe('Updating person works', () => {
beforeAll(async () => {
await resetDatabase()
}, resetDatabaseTimeout)
beforeEach(async () => {
await login()
})
afterAll(async () => {
await pool.end()
})
it('Request succeeds and returns valid initials', async () => {
const { body: resultBody, status: getReqStatus } = await send<{ initials: string }>('person/', 'PUT', {
person: editedPerson,
})
const { initials: resultId } = resultBody
expect(typeof resultId).toEqual('string')
expect(getReqStatus).toEqual(200)
const { body, status: getReqStat } = await send<PersonDetailsType>(`person/${resultId}`, 'GET')
expect(getReqStat).toEqual(200)
updatedPerson = body
})
it('Contains correct data and full name is automatically updated', () => {
const { initials, first_name, surname, full_name, email, organization, country } = updatedPerson!
expect(initials).toEqual(existingPerson.initials)
expect(first_name).toEqual(editedPerson.first_name)
expect(surname).toEqual(editedPerson.surname)
expect(email).toEqual(editedPerson.email)
expect(organization).toEqual(editedPerson.organization)
expect(country).toEqual(editedPerson.country)
expect(full_name).toEqual(`${editedPerson.first_name} ${editedPerson.surname}`)
})
it('Updating fails if any person field is empty', async () => {
const { body: resultBody1, status: getReqStatus1 } = await send('person/', 'PUT', {
person: { ...editedPerson, first_name: '' },
})
expect(getReqStatus1).toEqual(403)
expect(resultBody1.length).toEqual(1) //There should be 1 validation error
const { body: resultBody2, status: getReqStatus2 } = await send('person/', 'PUT', {
person: { ...editedPerson, surname: '' },
})
expect(getReqStatus2).toEqual(403)
expect(resultBody2.length).toEqual(1) //There should be 1 validation error
const { body: resultBody3, status: getReqStatus3 } = await send('person/', 'PUT', {
person: { ...editedPerson, email: '' },
})
expect(getReqStatus3).toEqual(403)
expect(resultBody3.length).toEqual(1) //There should be 1 validation error
const { body: resultBody4, status: getReqStatus4 } = await send('person/', 'PUT', {
person: { ...editedPerson, organization: '' },
})
expect(getReqStatus4).toEqual(403)
expect(resultBody4.length).toEqual(1) //There should be 1 validation error
const { body: resultBody5, status: getReqStatus5 } = await send('person/', 'PUT', {
person: { ...editedPerson, country: '' },
})
expect(getReqStatus5).toEqual(403)
expect(resultBody5.length).toEqual(1) //There should be 1 validation error
})
it('Updating other people fails without permissions', async () => {
logout()
const { body: resultBodyNoPerm, status: resultStatusNoPerm } = await send('person/', 'PUT', {
person: { initials: 'AD', first_name: 'Test first name 2' },
})
expect(resultBodyNoPerm).toEqual(noPermError)
expect(resultStatusNoPerm).toEqual(401)
await login('testEr')
const { body: resultBodyEr, status: resultStatusEr } = await send('person/', 'PUT', {
person: { initials: 'AD', first_name: 'Test first name 2' },
})
expect(resultBodyEr).toEqual(noPermError)
expect(resultStatusEr).toEqual(401)
await login('testEu')
const { body: resultBodyEu, status: resultStatusEu } = await send('person/', 'PUT', {
person: { initials: 'AD', first_name: 'Test first name 2' },
})
expect(resultBodyEu).toEqual(noPermError)
expect(resultStatusEu).toEqual(401)
})
it('Non-admin user updating their own information succeeds', async () => {
await login('testEr')
const { body: resultBodyEr, status: resultStatusEr } = await send<{ initials: string }>('person/', 'PUT', {
person: { initials: 'TEST-ER', first_name: 'Test first name 3' },
})
const { initials: resultId } = resultBodyEr
expect(typeof resultId).toEqual('string')
expect(resultStatusEr).toEqual(200)
const { body, status: getReqStat } = await send<PersonDetailsType>(`person/${resultId}`, 'GET')
expect(getReqStat).toEqual(200)
expect(body.first_name).toEqual('Test first name 3')
})
})
+40 -5
View File
@@ -1,7 +1,8 @@
import { Router } from 'express'
import { getAllPersons, getPersonDetails } from '../services/person'
import { Role } from '../../../frontend/src/shared/types'
import { Router, Request } from 'express'
import { getAllPersons, getPersonDetails, validateEntirePerson } from '../services/person'
import { Role, PersonDetailsType, EditDataType, EditMetaData } from '../../../frontend/src/shared/types'
import { requireOneOf } from '../middlewares/authorizer'
import { writePerson } from '../services/write/person'
const router = Router()
@@ -14,12 +15,46 @@ router.get('/:id', async (req, res) => {
const id = req.params.id
/* Access checking happens differently for this route, since we want to allow the route to return the user's own data for any user */
if (!req.user) return res.status(401).send()
if (req.user.role !== Role.Admin && req.user.initials !== id) return res.status(401).send()
if (!req.user)
return res.status(401).send({
message: 'User is not logged in',
})
if (req.user.role !== Role.Admin && req.user.initials !== id)
return res.status(401).send({
message: 'User not authorized for the requested resource or action',
})
const person = await getPersonDetails(id)
if (!person) return res.status(404).send()
return res.status(200).send(person)
})
router.put(
'/',
async (req: Request<object, object, { person: EditDataType<PersonDetailsType> & EditMetaData }>, res) => {
const { ...editedPerson } = req.body.person
if (!editedPerson.initials) {
return res.status(403).send({ error: 'Missing initials, creating new persons is not yet implemented' })
}
/* Access checking happens differently for this route, since we want to allow users to modify their own data */
if (!req.user)
return res.status(401).send({
message: 'User not authorized for the requested resource or action',
})
if (req.user.role !== Role.Admin && req.user.initials !== editedPerson.initials)
return res.status(401).send({
message: 'User not authorized for the requested resource or action',
})
const validationErrors = validateEntirePerson({ ...editedPerson })
if (validationErrors.length > 0) {
return res.status(403).send(validationErrors)
}
const initials = await writePerson(editedPerson)
return res.status(200).send({ initials })
}
)
export default router
+14
View File
@@ -1,4 +1,8 @@
import { nowDb } from '../utils/db'
import { EditDataType, PersonDetailsType } from '../../../frontend/src/shared/types'
import Prisma from '../../prisma/generated/now_test_client'
import { ValidationObject } from '../../../frontend/src/shared/validators/validator'
import { validatePerson } from '../../../frontend/src/shared/validators/person'
export const getAllPersons = async () => {
const persons = await nowDb.com_people.findMany({})
@@ -25,3 +29,13 @@ export const getPersonDetails = async (id: string) => {
return { ...person, user }
}
export const validateEntirePerson = (editedFields: EditDataType<Prisma.com_people>) => {
const keys = Object.keys(editedFields)
const errors: ValidationObject[] = []
for (const key of keys) {
const error = validatePerson(editedFields, key as keyof PersonDetailsType)
if (error.error) errors.push(error)
}
return errors
}
+22
View File
@@ -0,0 +1,22 @@
import { EditDataType, PersonDetailsType } from '../../../../frontend/src/shared/types'
import Prisma from '../../../prisma/generated/now_test_client'
import { nowDb, getFieldsOfTables } from '../../utils/db'
import { filterAllowedKeys } from './writeOperations/utils'
export const writePerson = async (person: EditDataType<PersonDetailsType>) => {
const allowedColumns = getFieldsOfTables(['com_people'])
const filteredPerson = filterAllowedKeys(person, allowedColumns) as Prisma.com_people
let personId: string
if (!filteredPerson.initials) {
throw new Error('Missing initials, creating new persons is not yet implemented')
} else {
await nowDb.com_people.update({
where: { initials: person.initials },
data: { ...filteredPerson, full_name: `${filteredPerson.first_name} ${filteredPerson.surname}` },
})
personId = filteredPerson.initials
}
return personId
}
+36
View File
@@ -0,0 +1,36 @@
import { EditDataType, PersonDetailsType } from '../types'
import { Validators, validator } from './validator'
export const validatePerson = (
editData: EditDataType<PersonDetailsType>,
fieldName: keyof EditDataType<PersonDetailsType>
) => {
const validators: Validators<Partial<EditDataType<PersonDetailsType>>> = {
initials: {
name: 'initials',
required: true,
},
first_name: {
name: 'First Name',
required: true,
},
surname: {
name: 'Surname',
required: true,
},
email: {
name: 'Email',
required: true,
},
organization: {
name: 'Organization',
required: true,
},
country: {
name: 'Country',
required: true,
},
}
return validator<EditDataType<PersonDetailsType>>(validators, editData, fieldName)
}