Add protected person deletion

This commit is contained in:
karilint
2026-05-25 14:04:06 +03:00
parent ee1421a5bd
commit 6f4428211c
6 changed files with 359 additions and 4 deletions
+100
View File
@@ -0,0 +1,100 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'
import { pool, nowDb } from '../../utils/db'
import { login, logout, noPermError, resetDatabase, resetDatabaseTimeout, send } from '../utils'
const createPerson = async (initials: string, userId?: number) => {
await nowDb.com_people.create({
data: {
initials,
first_name: 'Delete',
surname: 'Candidate',
full_name: 'Delete Candidate',
email: `${initials.toLowerCase()}@example.com`,
organization: 'Test organization',
country: 'Finland',
user_id: userId,
},
})
}
describe('Deleting person works', () => {
beforeAll(async () => {
await resetDatabase()
}, resetDatabaseTimeout)
beforeEach(async () => {
await login()
})
afterAll(async () => {
await pool.end()
})
it('deletes a person without user relation or protected activity', async () => {
await createPerson('DEL-NO')
const deleteResult = await send('person/DEL-NO', 'DELETE')
expect(deleteResult.status).toEqual(200)
const deletedPerson = await nowDb.com_people.findUnique({ where: { initials: 'DEL-NO' } })
expect(deletedPerson).toBeNull()
})
it('deletes a person and their user when the user has no protected activity', async () => {
const user = await nowDb.com_users.create({
data: {
user_name: 'delete-person-user',
now_user_group: 'eu',
},
})
await createPerson('DEL-USR', user.user_id)
const deleteResult = await send('person/DEL-USR', 'DELETE')
expect(deleteResult.status).toEqual(200)
const deletedPerson = await nowDb.com_people.findUnique({ where: { initials: 'DEL-USR' } })
const deletedUser = await nowDb.com_users.findUnique({ where: { user_id: user.user_id } })
expect(deletedPerson).toBeNull()
expect(deletedUser).toBeNull()
})
it('does not delete a person with update history or project and coordinator links', async () => {
const deleteResult = await send<{ blockers: string[]; message: string }>('person/AD', 'DELETE')
expect(deleteResult.status).toEqual(409)
expect(deleteResult.body.message).toEqual(
'Person cannot be deleted because they are still linked to database data.'
)
expect(deleteResult.body.blockers).toEqual([
'person has data update history',
'person is assigned to projects or coordinator groups',
])
const person = await nowDb.com_people.findUnique({ where: { initials: 'AD' } })
expect(person).not.toBeNull()
})
it('does not allow users to delete their own person record', async () => {
const deleteResult = await send<{ blockers: string[]; message: string }>('person/TEST-SU', 'DELETE')
expect(deleteResult.status).toEqual(409)
expect(deleteResult.body.message).toEqual('You cannot delete your own person record.')
expect(deleteResult.body.blockers).toEqual(['self'])
})
it('deleting fails without admin permissions', async () => {
await createPerson('DEL-PERM')
logout()
const deleteResultNoPerm = await send('person/DEL-PERM', 'DELETE')
expect(deleteResultNoPerm.status).toEqual(403)
await login('testEr')
const deleteResultEr = await send('person/DEL-PERM', 'DELETE')
expect(deleteResultEr.status).toEqual(403)
expect(deleteResultEr.body).toEqual(noPermError)
const person = await nowDb.com_people.findUnique({ where: { initials: 'DEL-PERM' } })
expect(person).not.toBeNull()
})
})
+11 -1
View File
@@ -1,5 +1,5 @@
import { Router, Request } from 'express'
import { getAllPersons, getPersonDetails, validateEntirePerson } from '../services/person'
import { deletePerson, getAllPersons, getPersonDetails, validateEntirePerson } from '../services/person'
import {
Role,
PersonDetailsType,
@@ -73,4 +73,14 @@ router.put(
}
)
router.delete('/:id', requireOneOf([Role.Admin]), async (req, res) => {
const result = await deletePerson(req.params.id, req.user!.initials)
if (!result.deleted) {
return res.status(result.status).send({ message: result.message, blockers: result.blockers })
}
return res.status(200).send()
})
export default router
+72
View File
@@ -4,6 +4,9 @@ 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 PERSON_DELETE_BLOCKED_MESSAGE = 'Person cannot be deleted because they are still linked to database data.'
export const PERSON_DELETE_SELF_MESSAGE = 'You cannot delete your own person record.'
export const getAllPersons = async () => {
const persons = await nowDb.com_people.findMany({})
const users = await nowDb.com_users.findMany({})
@@ -39,3 +42,72 @@ export const validateEntirePerson = (editedFields: EditDataType<Prisma.com_peopl
}
return errors
}
const hasAny = async (checks: Array<Promise<number>>): Promise<boolean> => {
const counts = await Promise.all(checks)
return counts.some(count => count > 0)
}
const hasUpdateActivity = async (initials: string): Promise<boolean> => {
return hasAny([
nowDb.now_lau.count({ where: { OR: [{ lau_coordinator: initials }, { lau_authorizer: initials }] } }),
nowDb.now_sau.count({ where: { OR: [{ sau_coordinator: initials }, { sau_authorizer: initials }] } }),
nowDb.now_tau.count({ where: { OR: [{ tau_coordinator: initials }, { tau_authorizer: initials }] } }),
nowDb.now_bau.count({ where: { OR: [{ bau_coordinator: initials }, { bau_authorizer: initials }] } }),
nowDb.now_time_update.count({ where: { OR: [{ coordinator: initials }, { authorizer: initials }] } }),
])
}
const hasCoordinationOrProjectLinks = async (initials: string): Promise<boolean> => {
return hasAny([
nowDb.now_proj.count({ where: { contact: initials } }),
nowDb.now_proj_people.count({ where: { initials } }),
nowDb.now_reg_coord_people.count({ where: { initials } }),
nowDb.now_sp_coord_people.count({ where: { initials } }),
nowDb.now_strat_coord_people.count({ where: { initials } }),
])
}
export const getPersonDeleteBlockers = async (initials: string): Promise<string[]> => {
const blockers: string[] = []
if (await hasUpdateActivity(initials)) {
blockers.push('person has data update history')
}
if (await hasCoordinationOrProjectLinks(initials)) {
blockers.push('person is assigned to projects or coordinator groups')
}
return blockers
}
export const deletePerson = async (initials: string, currentUserInitials: string) => {
if (initials === currentUserInitials) {
return { deleted: false, status: 409 as const, message: PERSON_DELETE_SELF_MESSAGE, blockers: ['self'] }
}
const person = await nowDb.com_people.findUnique({
where: { initials },
select: { initials: true, user_id: true },
})
if (!person) {
return { deleted: false, status: 404 as const, message: 'Person not found.', blockers: [] }
}
const blockers = await getPersonDeleteBlockers(initials)
if (blockers.length > 0) {
return { deleted: false, status: 409 as const, message: PERSON_DELETE_BLOCKED_MESSAGE, blockers }
}
await nowDb.$transaction(async prisma => {
await prisma.com_people.delete({ where: { initials } })
if (person.user_id) {
await prisma.com_users.deleteMany({ where: { user_id: person.user_id } })
}
})
return { deleted: true, status: 200 as const, message: null, blockers: [] }
}