Skip to content

Commit fe44554

Browse files
committed
Implement removing organisations
1 parent f31803a commit fe44554

File tree

5 files changed

+134
-22
lines changed

5 files changed

+134
-22
lines changed

src/constants/roles.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515
export const ROLES = {
1616
SUPER_ADMIN: 'SuperAdmin',
17+
SUPER_ADMIN_PLUS: 'SuperAdminPlus',
1718
CITY_ADMIN: 'CityAdmin',
1819
VOLUNTEER_ADMIN: 'VolunteerAdmin',
1920
ORG_ADMIN: 'OrgAdmin',
@@ -49,6 +50,7 @@ export type Role = BaseRole | `${typeof ROLE_PREFIXES.CITY_ADMIN_FOR}${string}`
4950
*/
5051
export const BASE_ROLES_ARRAY: readonly BaseRole[] = [
5152
ROLES.SUPER_ADMIN,
53+
ROLES.SUPER_ADMIN_PLUS,
5254
ROLES.CITY_ADMIN,
5355
ROLES.VOLUNTEER_ADMIN,
5456
ROLES.ORG_ADMIN,
@@ -126,7 +128,7 @@ export function hasRole(authClaims: string[], role: BaseRole): boolean {
126128
* Check if user has SuperAdmin role
127129
*/
128130
export function isSuperAdmin(authClaims: string[]): boolean {
129-
return hasRole(authClaims, ROLES.SUPER_ADMIN);
131+
return hasRole(authClaims, ROLES.SUPER_ADMIN) || hasRole(authClaims, ROLES.SUPER_ADMIN_PLUS);
130132
}
131133

132134
/**
@@ -158,7 +160,7 @@ export function hasLocationAccess(authClaims: string[], locationSlug: string): b
158160
* Regular expression pattern for validating role formats
159161
* Matches: SuperAdmin, CityAdmin, CityAdminFor:*, AdminFor:*, SwepAdmin, SwepAdminFor:*, OrgAdmin, VolunteerAdmin
160162
*/
161-
export const ROLE_VALIDATION_PATTERN = /^(SuperAdmin|CityAdmin|CityAdminFor:.+|VolunteerAdmin|SwepAdmin|SwepAdminFor:.+|OrgAdmin|AdminFor:.+)$/;
163+
export const ROLE_VALIDATION_PATTERN = /^(SuperAdmin|SuperAdminPlus|CityAdmin|CityAdminFor:.+|VolunteerAdmin|SwepAdmin|SwepAdminFor:.+|OrgAdmin|AdminFor:.+)$/;
162164

163165
/**
164166
* Validate an array of roles

src/controllers/organisationController.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Service from '../models/serviceModel.js';
66
import Accommodation from '../models/accommodationModel.js';
77
import User from '../models/userModel.js';
88
import { asyncHandler } from '../utils/asyncHandler.js';
9-
import { sendSuccess, sendCreated, sendNotFound, sendBadRequest, sendPaginatedSuccess } from '../utils/apiResponses.js';
9+
import { sendSuccess, sendCreated, sendNotFound, sendBadRequest, sendPaginatedSuccess, sendInternalError } from '../utils/apiResponses.js';
1010
import { ROLES, ROLE_PREFIXES } from '../constants/roles.js';
1111
import { validateOrganisation } from '../schemas/organisationSchema.js';
1212
import { processAddressesWithCoordinates, updateLocationIfPostcodeChanged } from '../utils/postcodeValidation.js';
@@ -37,7 +37,7 @@ export const getOrganisations = asyncHandler(async (req: Request, res: Response)
3737
// const userId = req.user?._id;
3838

3939
// Role-based filtering
40-
const isSuperAdmin = requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN);
40+
const isSuperAdmin = requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN) || requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS);
4141
const isVolunteerAdmin = requestingUserAuthClaims.includes(ROLES.VOLUNTEER_ADMIN);
4242
const isCityAdmin = requestingUserAuthClaims.includes(ROLES.CITY_ADMIN);
4343

@@ -651,4 +651,77 @@ export const updateRelatedServices = async (
651651
return groupedServicesResult.modifiedCount +
652652
servicesResult.modifiedCount +
653653
accommodationsResult.modifiedCount;
654-
};
654+
};
655+
656+
// @desc Delete organisation and all related data
657+
// @route DELETE /api/organisations/:id
658+
// @access Private (SuperAdminPlus only)
659+
export const deleteOrganisation = asyncHandler(async (req: Request, res: Response) => {
660+
// Start a MongoDB session for transaction
661+
const session = await mongoose.startSession();
662+
663+
try {
664+
// Start transaction
665+
await session.startTransaction();
666+
667+
// Find the organisation first
668+
const organisation = await Organisation.findById(req.params.id).session(session);
669+
670+
if (!organisation) {
671+
await session.abortTransaction();
672+
return sendNotFound(res, 'Organisation not found');
673+
}
674+
675+
const organisationKey = organisation.Key;
676+
const organisationName = organisation.Name;
677+
678+
// Delete all related grouped services
679+
const groupedServicesResult = await GroupedService.deleteMany(
680+
{ ProviderId: organisationKey },
681+
{ session }
682+
);
683+
684+
// Delete all related individual services
685+
const servicesResult = await Service.deleteMany(
686+
{ ServiceProviderKey: organisationKey },
687+
{ session }
688+
);
689+
690+
// Delete all related accommodations
691+
const accommodationsResult = await Accommodation.deleteMany(
692+
{ 'GeneralInfo.ServiceProviderId': organisationKey },
693+
{ session }
694+
);
695+
696+
// Delete the organisation itself
697+
await Organisation.findByIdAndDelete(req.params.id, { session });
698+
699+
// Commit the transaction
700+
await session.commitTransaction();
701+
702+
const deletionSummary = {
703+
organisationName,
704+
organisationKey,
705+
deletedGroupedServices: groupedServicesResult.deletedCount,
706+
deletedIndividualServices: servicesResult.deletedCount,
707+
deletedAccommodations: accommodationsResult.deletedCount
708+
};
709+
710+
return sendSuccess(
711+
res,
712+
deletionSummary,
713+
`Organisation "${organisationName}" and all related data deleted successfully. ` +
714+
`Deleted: ${groupedServicesResult.deletedCount} grouped services, ` +
715+
`${servicesResult.deletedCount} individual services, ` +
716+
`${accommodationsResult.deletedCount} accommodations.`
717+
);
718+
} catch (error) {
719+
// Abort transaction on error
720+
await session.abortTransaction();
721+
console.error('Error deleting organisation:', error);
722+
return sendInternalError(res, 'Failed to delete organisation');
723+
} finally {
724+
// End session
725+
session.endSession();
726+
}
727+
});

src/controllers/userController.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,13 @@ const getUsers = asyncHandler(async (req: Request, res: Response) => {
4242

4343
// Exclude SuperAdmin users from results if requesting user is not a SuperAdmin
4444
const requestingUserAuthClaims = req.user?.AuthClaims || [];
45-
if (!requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN)) {
45+
if (!requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN) && !requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS)) {
4646
conditions.push({ AuthClaims: { $ne: ROLES.SUPER_ADMIN } });
47+
conditions.push({ AuthClaims: { $ne: ROLES.SUPER_ADMIN_PLUS } });
4748
}
4849

49-
// Exclude VolunteerAdmin users from results if requesting user is not a SuperAdmin or VolunteerAdmin
50-
if (!requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN)) {
50+
// Exclude VolunteerAdmin users from results if requesting user is not a SuperAdmin
51+
if (!requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN) && !requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS)) {
5152
conditions.push({ AuthClaims: { $ne: ROLES.VOLUNTEER_ADMIN } });
5253
}
5354

@@ -133,8 +134,8 @@ const getUserById = asyncHandler(async (req: Request, res: Response) => {
133134

134135
// Exclude SuperAdmin users from results if requesting user is not a SuperAdmin
135136
const requestingUserAuthClaims = req.user?.AuthClaims || [];
136-
if (!requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN)) {
137-
if(user.AuthClaims.some((claim: string) => claim === ROLES.SUPER_ADMIN)){
137+
if (!requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN) && !requestingUserAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS)) {
138+
if(user.AuthClaims.some((claim: string) => claim === ROLES.SUPER_ADMIN || claim === ROLES.SUPER_ADMIN_PLUS)){
138139
return sendForbidden(res);
139140
};
140141
}
@@ -332,6 +333,7 @@ const deleteUser = asyncHandler(async (req: Request, res: Response) => {
332333
// Remove user from organisation administrators if they have org-related roles
333334
const hasOrgRoles = user.AuthClaims?.some((claim: string) =>
334335
claim === ROLES.SUPER_ADMIN ||
336+
claim === ROLES.SUPER_ADMIN_PLUS ||
335337
claim === ROLES.VOLUNTEER_ADMIN ||
336338
claim.startsWith(ROLE_PREFIXES.ADMIN_FOR) ||
337339
claim.startsWith(ROLE_PREFIXES.CITY_ADMIN_FOR)

src/middleware/authMiddleware.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ interface JwtPayload {
4646
*/
4747
const handleSuperAdminAccess = (
4848
userAuthClaims: string[]
49-
): boolean => userAuthClaims.includes(ROLES.SUPER_ADMIN);
49+
): boolean => userAuthClaims.includes(ROLES.SUPER_ADMIN) || userAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS);
5050

5151
/**
5252
* Helper: handles global privileged access rules for VolunteerAdmin.
@@ -802,7 +802,7 @@ export const requireFaqLocationAccess = (req: Request, res: Response, next: Next
802802

803803
// If LocationKey is 'general', only SuperAdmin and VolunteerAdmin can access
804804
if (locationId === 'general' || locations.includes('general')) {
805-
if (userAuthClaims.includes(ROLES.SUPER_ADMIN) || userAuthClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
805+
if (userAuthClaims.includes(ROLES.SUPER_ADMIN) || userAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS) || userAuthClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
806806
return next();
807807
}
808808
return sendForbidden(res, 'Access to general advice is restricted to SuperAdmin and VolunteerAdmin');
@@ -846,7 +846,7 @@ export const requireUserCreationAccess = asyncHandler(async (req: Request, res:
846846
}
847847

848848
// SuperAdmin has access to everything.
849-
if (userAuthClaims.includes(ROLES.SUPER_ADMIN)) {
849+
if (userAuthClaims.includes(ROLES.SUPER_ADMIN) || userAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS)) {
850850
return next();
851851
}
852852

@@ -878,7 +878,7 @@ export const requireUserCreationAccess = asyncHandler(async (req: Request, res:
878878
// Validate that they're not trying to assign SuperAdmin role or VolunteerAdmin roles or CityAdmin roles
879879
const requestBody = req.body;
880880
if (requestBody.AuthClaims && Array.isArray(requestBody.AuthClaims)) {
881-
if (requestBody.AuthClaims.includes(ROLES.SUPER_ADMIN) || requestBody.AuthClaims.includes(ROLES.VOLUNTEER_ADMIN) || requestBody.AuthClaims.includes(ROLES.CITY_ADMIN)) {
881+
if (requestBody.AuthClaims.includes(ROLES.SUPER_ADMIN) || requestBody.AuthClaims.includes(ROLES.SUPER_ADMIN_PLUS) || requestBody.AuthClaims.includes(ROLES.VOLUNTEER_ADMIN) || requestBody.AuthClaims.includes(ROLES.CITY_ADMIN)) {
882882
return sendForbidden(res, 'OrgAdmin cannot assign SuperAdmin, VolunteerAdmin or CityAdmin roles');
883883
}
884884
}
@@ -889,7 +889,7 @@ export const requireUserCreationAccess = asyncHandler(async (req: Request, res:
889889
// CityAdmin can create CityAdmin, SwepAdmin, and OrgAdmin users
890890
if (userAuthClaims.includes(ROLES.CITY_ADMIN)) {
891891
// Validate that they're not trying to assign SuperAdmin or VolunteerAdmin role
892-
if (newUserClaims.includes(ROLES.SUPER_ADMIN) || newUserClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
892+
if (newUserClaims.includes(ROLES.SUPER_ADMIN) || newUserClaims.includes(ROLES.SUPER_ADMIN_PLUS) || newUserClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
893893
return sendForbidden(res, 'CityAdmin cannot assign SuperAdmin or VolunteerAdmin roles');
894894
}
895895

@@ -978,7 +978,7 @@ export const requireDeletionUserAccess = asyncHandler(async (req: Request, res:
978978
}
979979

980980
// 1. SuperAdmin can delete everything
981-
if (userAuthClaims.includes(ROLES.SUPER_ADMIN)) {
981+
if (userAuthClaims.includes(ROLES.SUPER_ADMIN) || userAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS)) {
982982
return next();
983983
}
984984

@@ -994,7 +994,7 @@ export const requireDeletionUserAccess = asyncHandler(async (req: Request, res:
994994
// 2. CityAdmin can delete specific roles within their city
995995
if (userAuthClaims.includes(ROLES.CITY_ADMIN)) {
996996
// Cannot delete SuperAdmin or VolunteerAdmin
997-
if (targetUserClaims.includes(ROLES.SUPER_ADMIN) || targetUserClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
997+
if (targetUserClaims.includes(ROLES.SUPER_ADMIN) || targetUserClaims.includes(ROLES.SUPER_ADMIN_PLUS) || targetUserClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
998998
return sendForbidden(res, 'CityAdmin cannot delete/deactivate/activate SuperAdmin or VolunteerAdmin users');
999999
}
10001000

@@ -1105,7 +1105,7 @@ export const requireUserAccess = asyncHandler(async (req: Request, res: Response
11051105
const userId = req.params.id;
11061106

11071107
// 1. SuperAdmin can do everything
1108-
if (userAuthClaims.includes(ROLES.SUPER_ADMIN)) {
1108+
if (userAuthClaims.includes(ROLES.SUPER_ADMIN) || userAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS)) {
11091109
// For updates, validate role structure
11101110
if (method === HTTP_METHODS.PUT || method === HTTP_METHODS.PATCH) {
11111111
const requestBody = req.body;
@@ -1197,14 +1197,14 @@ export const requireUserAccess = asyncHandler(async (req: Request, res: Response
11971197

11981198
if (method === HTTP_METHODS.PUT || method === HTTP_METHODS.PATCH) {
11991199
// CityAdmin cannot update SuperAdmin or VolunteerAdmin users
1200-
if (targetUserClaims.includes(ROLES.SUPER_ADMIN) || targetUserClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
1200+
if (targetUserClaims.includes(ROLES.SUPER_ADMIN) || targetUserClaims.includes(ROLES.SUPER_ADMIN_PLUS) || targetUserClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
12011201
return sendForbidden(res, 'CityAdmin cannot update SuperAdmin or VolunteerAdmin users');
12021202
}
12031203

12041204
// Validate that they're not trying to assign SuperAdmin or VolunteerAdmin role
12051205
const requestBody = req.body;
12061206
if (requestBody.AuthClaims && Array.isArray(requestBody.AuthClaims)) {
1207-
if (requestBody.AuthClaims.includes(ROLES.SUPER_ADMIN) || requestBody.AuthClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
1207+
if (requestBody.AuthClaims.includes(ROLES.SUPER_ADMIN) || requestBody.AuthClaims.includes(ROLES.SUPER_ADMIN_PLUS) || requestBody.AuthClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
12081208
return sendForbidden(res, 'CityAdmin cannot assign SuperAdmin or VolunteerAdmin roles');
12091209
}
12101210

@@ -1688,3 +1688,36 @@ export const locationLogosGetAuth = [
16881688
authenticate,
16891689
requireLocationLogoByFiltersAccess
16901690
];
1691+
1692+
/**
1693+
* Helper: handles access rules for SuperAdminPlus.
1694+
* - SuperAdminPlus: access for organisation removal
1695+
* Returns true if user has SuperAdminPlus role, otherwise false.
1696+
*/
1697+
const handleSuperAdminPlusAccess = (
1698+
userAuthClaims: string[]
1699+
): boolean => userAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS);
1700+
1701+
/**
1702+
* Middleware to require SuperAdminPlus role for organisation deletion
1703+
*/
1704+
export const requireSuperAdminPlusAccess = (req: Request, res: Response, next: NextFunction) => {
1705+
if (ensureAuthenticated(req, res)) { return; }
1706+
1707+
const userAuthClaims = req.user?.AuthClaims || [];
1708+
1709+
// Only SuperAdminPlus can delete organisations
1710+
if (handleSuperAdminPlusAccess(userAuthClaims)) {
1711+
return next();
1712+
}
1713+
1714+
return sendForbidden(res, 'Only SuperAdminPlus role can perform this action');
1715+
};
1716+
1717+
/**
1718+
* Combined middleware for organisation deletion endpoint
1719+
*/
1720+
export const organisationDeleteAuth = [
1721+
authenticate,
1722+
requireSuperAdminPlusAccess
1723+
];

src/routes/organisationRoutes.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {
88
clearNotes,
99
getOrganisationByKey,
1010
confirmOrganisationInfo,
11-
updateAdministrator
11+
updateAdministrator,
12+
deleteOrganisation
1213
} from '../controllers/organisationController.js';
13-
import { organisationsAuth, organisationsByKeyAuth, organisationsByLocationAuth, verifyOrganisationsAuth } from '../middleware/authMiddleware.js';
14+
import { organisationsAuth, organisationsByKeyAuth, organisationsByLocationAuth, verifyOrganisationsAuth, organisationDeleteAuth } from '../middleware/authMiddleware.js';
1415

1516
const router = Router();
1617

@@ -21,6 +22,7 @@ router.put('/:id', organisationsAuth, updateOrganisation);
2122
router.patch('/:id/toggle-verified', verifyOrganisationsAuth, toggleVerified);
2223
router.patch('/:id/toggle-published', organisationsAuth, togglePublished);
2324
router.delete('/:id/notes', organisationsAuth, clearNotes);
25+
router.delete('/:id', organisationDeleteAuth, deleteOrganisation);
2426
router.post('/:id/confirm-info', organisationsAuth, confirmOrganisationInfo);
2527
router.put('/:id/administrator', organisationsAuth, updateAdministrator);
2628

0 commit comments

Comments
 (0)