Skip to content

Commit ea84e8b

Browse files
Merge pull request #23 from StreetSupport/feature/implement-removing-organisations
Implement removing organisations
2 parents 532b2d0 + 7b313b9 commit ea84e8b

File tree

30 files changed

+244
-50
lines changed

30 files changed

+244
-50
lines changed

docs/PERMISSIONS.md

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,18 @@ Both Admin (NextAuth) and API (Express middleware) validate these claims for acc
3434
| Role | Description | Access Level |
3535
|------|-------------|--------------|
3636
| `SuperAdmin` | Full platform access | All pages, all API endpoints |
37+
| `SuperAdminPlus` | SuperAdmin with extended privileges | All SuperAdmin access + organisation deletion |
3738
| `CityAdmin` | Location-specific administrator | Pages/APIs for assigned locations |
3839
| `VolunteerAdmin` | Volunteer management | Organisation management, content creation |
3940
| `OrgAdmin` | Organisation-specific administrator | Own organisation only |
4041
| `SwepAdmin` | SWEP banner management | SWEP banners for assigned locations |
4142

43+
### Special Notes on SuperAdminPlus
44+
45+
- **Cannot be created through UI**: This role must be manually assigned in MongoDB and Auth0
46+
- **Organisation Deletion**: Only SuperAdminPlus can delete organisations and their related data
47+
- **Cannot be removed through UI**: The role removal is disabled in the Edit User modal
48+
4249
### Role Prefixes (Specific Claims)
4350

4451
| Prefix | Format | Example |
@@ -50,6 +57,8 @@ Both Admin (NextAuth) and API (Express middleware) validate these claims for acc
5057
### Role Hierarchy
5158

5259
```
60+
SuperAdminPlus (SuperAdmin + organisation deletion)
61+
5362
SuperAdmin
5463
↓ (Full access)
5564
VolunteerAdmin
@@ -64,16 +73,27 @@ OrgAdmin + AdminFor:*
6473

6574
### Page Access by Role
6675

67-
| Page | SuperAdmin | CityAdmin | VolunteerAdmin | OrgAdmin | SwepAdmin |
68-
|------|------------|-----------|----------------|----------|-----------|
69-
| `/cities` ||||||
70-
| `/organisations` ||||||
71-
| `/users` ||||||
72-
| `/banners` ||||||
73-
| `/swep-banners` ||||||
74-
| `/advice` ||||||
75-
| `/location-logos` ||||||
76-
| `/resources` ||||||
76+
| Page | SuperAdmin | SuperAdminPlus | CityAdmin | VolunteerAdmin | OrgAdmin | SwepAdmin |
77+
|------|------------|----------------|-----------|----------------|----------|-----------|
78+
| `/cities` |||||||
79+
| `/organisations` |||||||
80+
| `/users` |||||||
81+
| `/banners` |||||||
82+
| `/swep-banners` |||||||
83+
| `/advice` |||||||
84+
| `/location-logos` |||||||
85+
| `/resources` |||||||
86+
87+
### Organisation Actions by Role
88+
89+
| Action | SuperAdmin | SuperAdminPlus | CityAdmin | VolunteerAdmin | OrgAdmin |
90+
|--------|------------|----------------|-----------|----------------|----------|
91+
| View ||||||
92+
| Create ||||||
93+
| Edit ||||||
94+
| Publish/Disable ||||||
95+
| Verify ||||||
96+
| **Delete** ||||||
7797

7898
---
7999

@@ -156,14 +176,15 @@ The API validates role assignments based on the creator's permissions:
156176
```typescript
157177
// authMiddleware.ts - requireUserCreationAccess
158178
// SuperAdmin can assign any role
159-
if (userAuthClaims.includes(ROLES.SUPER_ADMIN)) {
179+
if (userAuthClaims.includes(ROLES.SUPER_ADMIN) || userAuthClaims.includes(ROLES.SUPER_ADMIN_PLUS)) {
160180
return next();
161181
}
162182

163183
// CityAdmin cannot assign SuperAdmin or VolunteerAdmin
164184
if (userAuthClaims.includes(ROLES.CITY_ADMIN)) {
165185
if (newUserClaims.includes(ROLES.SUPER_ADMIN) ||
166-
newUserClaims.includes(ROLES.VOLUNTEER_ADMIN)) {
186+
newUserClaims.includes(ROLES.VOLUNTEER_ADMIN) ||
187+
newUserClaims.includes(ROLES.VOLUNTEER_ADMIN_PLUS)) {
167188
return sendForbidden(res, 'CityAdmin cannot assign SuperAdmin or VolunteerAdmin roles');
168189
}
169190
}
@@ -292,7 +313,7 @@ Using the `useAuthorization` hook:
292313
// Example: Banners page
293314
export default function BannersPage() {
294315
const { isChecking, isAuthorized } = useAuthorization({
295-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
316+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
296317
requiredPage: '/banners',
297318
autoRedirect: true
298319
});
@@ -321,7 +342,7 @@ export function hasApiAccess(
321342
method: HttpMethod
322343
): boolean {
323344
// SuperAdmin has access to everything
324-
if (userAuthClaims.roles.includes(ROLES.SUPER_ADMIN)) {
345+
if (userAuthClaims.roles.includes(ROLES.SUPER_ADMIN) || userAuthClaims.roles.includes(ROLES.SUPER_ADMIN_PLUS)) {
325346
return true;
326347
}
327348

@@ -346,10 +367,26 @@ export function hasApiAccess(
346367
```typescript
347368
// src/types/auth.ts
348369
export const ROLE_PERMISSIONS: Record<UserRole, RolePermissions> = {
349-
[ROLES.SUPER_ADMIN]: {
370+
[ROLES.SUPER_ADMIN_PLUS]: {
350371
pages: ['*'],
351372
apiEndpoints: [{ path: '*', methods: ['*'] }]
352373
},
374+
[ROLES.SUPER_ADMIN]: {
375+
pages: ['*'],
376+
apiEndpoints: [
377+
{ path: '/api/cities', methods: ['*'] },
378+
{ path: '/api/organisations', methods: [HTTP_METHODS.GET, HTTP_METHODS.POST, HTTP_METHODS.PUT, HTTP_METHODS.PATCH] },
379+
{ path: '/api/services', methods: ['*'] },
380+
{ path: '/api/accommodations', methods: ['*'] },
381+
{ path: '/api/faqs', methods: ['*'] },
382+
{ path: '/api/banners', methods: ['*'] },
383+
{ path: '/api/location-logos', methods: ['*'] },
384+
{ path: '/api/swep-banners', methods: ['*'] },
385+
{ path: '/api/resources', methods: ['*'] },
386+
{ path: '/api/users', methods: ['*'] },
387+
{ path: '/api/service-categories', methods: ['*'] },
388+
]
389+
},
353390
[ROLES.CITY_ADMIN]: {
354391
pages: ['/cities', '/organisations', '/advice', '/banners', '/location-logos', '/swep-banners', '/users'],
355392
apiEndpoints: [

src/app/advice/[id]/edit/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ export default function AdviceEditPage() {
3333

3434
// Check authorization FIRST
3535
const { isChecking, isAuthorized } = useAuthorization({
36-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
36+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
3737
requiredPage: '/advice',
3838
autoRedirect: true
3939
});
4040

4141
const { data: session } = useSession();
4242
const userRoles = session?.user?.authClaims?.roles || [];
43-
const canAccessGeneralAdvice = userRoles.includes(ROLES.SUPER_ADMIN) || userRoles.includes(ROLES.VOLUNTEER_ADMIN);
43+
const canAccessGeneralAdvice = userRoles.includes(ROLES.SUPER_ADMIN) || userRoles.includes(ROLES.SUPER_ADMIN_PLUS) || userRoles.includes(ROLES.VOLUNTEER_ADMIN);
4444

4545
const [loading, setLoading] = useState(true);
4646
const [error, setError] = useState<string | null>(null);

src/app/advice/[id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function AdviceViewPage() {
2424

2525
// Check authorization FIRST
2626
const { isChecking, isAuthorized } = useAuthorization({
27-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
27+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
2828
requiredPage: '/advice',
2929
autoRedirect: true
3030
});

src/app/advice/new/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ export default function NewAdvicePage() {
2828

2929
// Check authorization FIRST
3030
const { isChecking, isAuthorized } = useAuthorization({
31-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN, ROLES.SWEP_ADMIN],
31+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN, ROLES.SWEP_ADMIN],
3232
requiredPage: '/advice',
3333
autoRedirect: true
3434
});
3535

3636
const { data: session } = useSession();
3737
const userRoles = session?.user?.authClaims?.roles || [];
38-
const canAccessGeneralAdvice = userRoles.includes(ROLES.SUPER_ADMIN) || userRoles.includes(ROLES.VOLUNTEER_ADMIN);
38+
const canAccessGeneralAdvice = userRoles.includes(ROLES.SUPER_ADMIN) || userRoles.includes(ROLES.SUPER_ADMIN_PLUS) || userRoles.includes(ROLES.VOLUNTEER_ADMIN);
3939

4040
const [saving, setSaving] = useState(false);
4141
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);

src/app/advice/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import AdviceManagement from '@/components/advice/AdviceManagement';
1111
export default function AdvicePage() {
1212
// Check authorization FIRST before any other logic
1313
const { isChecking, isAuthorized } = useAuthorization({
14-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
14+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
1515
requiredPage: '/advice',
1616
autoRedirect: true
1717
});

src/app/api/organisations/[id]/route.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NextRequest } from 'next/server';
33
import { withAuth, AuthenticatedApiHandler } from '@/lib/withAuth';
44
import { hasApiAccess } from '@/lib/userService';
55
import { sendForbidden, sendInternalError, proxyResponse, sendError } from '@/utils/apiResponses';
6+
import { ROLES } from '@/constants/roles';
67

78
const API_BASE_URL = process.env.API_BASE_URL;
89

@@ -96,6 +97,38 @@ const patchHandler: AuthenticatedApiHandler = async (req: NextRequest, context,
9697
}
9798
};
9899

100+
const deleteHandler: AuthenticatedApiHandler = async (req: NextRequest, context, auth) => {
101+
try {
102+
// Only SuperAdminPlus can delete organisations
103+
const userRoles = auth.session.user.authClaims.roles;
104+
if (!userRoles.includes(ROLES.SUPER_ADMIN_PLUS)) {
105+
return sendForbidden('Only SuperAdminPlus role can delete organisations');
106+
}
107+
108+
const { id } = context.params;
109+
110+
const response = await fetch(`${API_BASE_URL}/api/organisations/${id}`, {
111+
method: HTTP_METHODS.DELETE,
112+
headers: {
113+
'Content-Type': 'application/json',
114+
'Authorization': `Bearer ${auth.accessToken}`,
115+
},
116+
});
117+
118+
const data = await response.json();
119+
120+
if (!response.ok) {
121+
return sendError(response.status, data.error || 'Failed to delete organisation');
122+
}
123+
124+
return proxyResponse(data);
125+
} catch (error) {
126+
console.error('Error deleting organisation:', error);
127+
return sendInternalError();
128+
}
129+
};
130+
99131
export const GET = withAuth(getHandler);
100132
export const PUT = withAuth(putHandler);
101133
export const PATCH = withAuth(patchHandler);
134+
export const DELETE = withAuth(deleteHandler);

src/app/banners/[id]/edit/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function transformBannerToFormData(banner: IBanner): IBannerFormData {
7070
export default function EditBannerPage() {
7171
// Check authorization FIRST
7272
const { isChecking, isAuthorized } = useAuthorization({
73-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
73+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
7474
requiredPage: '/banners',
7575
autoRedirect: true
7676
});

src/app/banners/[id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
2020
export default function BannerViewPage() {
2121
// Check authorization FIRST
2222
const { isChecking, isAuthorized } = useAuthorization({
23-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
23+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
2424
requiredPage: '/banners',
2525
autoRedirect: true
2626
});

src/app/banners/new/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { PageHeader } from '@/components/ui/PageHeader';
1717
export default function NewBannerPage() {
1818
// Check authorization FIRST before any other logic
1919
const { isChecking, isAuthorized } = useAuthorization({
20-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
20+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
2121
requiredPage: '/banners',
2222
autoRedirect: true
2323
});

src/app/banners/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { ResultsSummary } from '@/components/ui/ResultsSummary';
2525
export default function BannersListPage() {
2626
// Check authorization FIRST
2727
const { isChecking, isAuthorized } = useAuthorization({
28-
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
28+
allowedRoles: [ROLES.SUPER_ADMIN, ROLES.SUPER_ADMIN_PLUS, ROLES.CITY_ADMIN, ROLES.VOLUNTEER_ADMIN],
2929
requiredPage: '/banners',
3030
autoRedirect: true
3131
});

0 commit comments

Comments
 (0)