Skip to content

Commit 4a006b3

Browse files
update in codebase
1 parent d9085ce commit 4a006b3

7 files changed

Lines changed: 868 additions & 8 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"specId": "c632ff22-f149-4b1d-9c86-4416012aa0ed", "workflowType": "requirements-first", "specType": "feature"}
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# Design Document: Admin Delete API
2+
3+
## Overview
4+
5+
This feature adds four new admin-only DELETE endpoints to the existing admin API surface. The endpoints allow platform administrators to perform privileged delete operations that bypass normal org-scoped ownership checks.
6+
7+
The design follows the existing admin controller patterns exactly: named exports, `asyncWrapper`, `sendSuccess`/`sendError`, and `auditService.log` after every successful mutation. No new middleware, no new services, no new utilities are introduced — only new handler functions and route registrations.
8+
9+
### Endpoints Summary
10+
11+
| Method | Path | Delete Type | Side Effects |
12+
|--------|------|-------------|--------------|
13+
| DELETE | `/api/v1/admin/contracts/:id` | Hard delete | Decrement org `contractCount` |
14+
| DELETE | `/api/v1/admin/organizations/:id` | Hard delete | Soft-delete contracts, hard-delete analyses, clear user org membership |
15+
| DELETE | `/api/v1/admin/analyses/:id` | Hard delete | None |
16+
| DELETE | `/api/v1/admin/templates/:id` | Soft delete (`isActive: false`) | None |
17+
18+
---
19+
20+
## Architecture
21+
22+
All four endpoints slot into the existing admin module without structural changes. The flow is identical to the existing `deactivateUser` handler:
23+
24+
```
25+
HTTP Request
26+
→ authenticate (JWT validation)
27+
→ authorize('admin') (role check)
28+
→ rateLimiter('strict') (5 req / 15 min)
29+
→ asyncWrapper(handler)
30+
→ find resource by _id (no org-scope filter)
31+
→ 404 if not found
32+
→ perform delete operation(s)
33+
→ auditService.log(...)
34+
→ sendSuccess(res, ...)
35+
```
36+
37+
The organization delete is the only handler with a cascade — it runs multiple write operations. These are executed with `Promise.all` where order doesn't matter (parallel), and sequentially where order matters (org deleted last, after cascade is complete).
38+
39+
```mermaid
40+
flowchart TD
41+
A[DELETE /admin/organizations/:id] --> B[Find org by _id]
42+
B -->|Not found| C[404 NOT_FOUND]
43+
B -->|Found| D[Promise.all: soft-delete contracts + hard-delete analyses + clear user memberships]
44+
D --> E[Hard-delete organization]
45+
E --> F[auditService.log]
46+
F --> G[200 OK]
47+
```
48+
49+
---
50+
51+
## Components and Interfaces
52+
53+
### Admin Controller — New Handlers
54+
55+
Four new named exports added to `src/controllers/admin.controller.js`:
56+
57+
```js
58+
export async function deleteContract(req, res)
59+
export async function deleteOrganization(req, res)
60+
export async function deleteAnalysis(req, res)
61+
export async function deleteTemplate(req, res)
62+
```
63+
64+
Each handler follows this interface contract:
65+
66+
- **Input**: `req.params.id` (resource ObjectId), `req.user.userId` (acting admin), `req.ip`, `req.headers['user-agent']`
67+
- **Output**: `sendSuccess` or `sendError` via `apiResponse.js`
68+
- **Error propagation**: thrown errors bubble up through `asyncWrapper` to the global error handler
69+
70+
### Admin Router — New Route Registrations
71+
72+
Four new lines added to `src/routes/admin.routes.js` (all covered by the existing router-level middleware):
73+
74+
```js
75+
router.delete('/contracts/:id', asyncWrapper(adminController.deleteContract));
76+
router.delete('/organizations/:id', asyncWrapper(adminController.deleteOrganization));
77+
router.delete('/analyses/:id', asyncWrapper(adminController.deleteAnalysis));
78+
router.delete('/templates/:id', asyncWrapper(adminController.deleteTemplate));
79+
```
80+
81+
### Models Used
82+
83+
| Model | Operation | Fields Written |
84+
|-------|-----------|----------------|
85+
| `Contract` | `findByIdAndDelete` | — |
86+
| `Contract` | `updateMany` | `isDeleted`, `deletedAt` |
87+
| `Organization` | `findByIdAndDelete` | — |
88+
| `Organization` | `findByIdAndUpdate` | `contractCount` (`$inc: -1`) |
89+
| `Analysis` | `findByIdAndDelete` | — |
90+
| `Analysis` | `deleteMany` | — |
91+
| `Template` | `findByIdAndUpdate` | `isActive` |
92+
| `User` | `updateMany` | `organization`, `role` |
93+
94+
---
95+
96+
## Data Models
97+
98+
No schema changes are required. All fields used by the new endpoints already exist:
99+
100+
**Contract** — `isDeleted: Boolean`, `deletedAt: Date`, `orgId: ObjectId` ✓
101+
**Organization** — `contractCount: Number`, `members: [{ userId }]`
102+
**Analysis** — `orgId: ObjectId`
103+
**Template** — `isActive: Boolean`
104+
**User** — `organization: ObjectId`, `role: String`
105+
106+
### Key Query Patterns
107+
108+
```js
109+
// deleteContract: unscoped lookup + hard delete
110+
const contract = await Contract.findById(id);
111+
await Contract.findByIdAndDelete(id);
112+
await Organization.findByIdAndUpdate(contract.orgId, { $inc: { contractCount: -1 } });
113+
114+
// deleteOrganization: cascade
115+
const org = await Organization.findById(id);
116+
await Promise.all([
117+
Contract.updateMany({ orgId: id }, { isDeleted: true, deletedAt: new Date() }),
118+
Analysis.deleteMany({ orgId: id }),
119+
User.updateMany({ organization: id }, { $unset: { organization: '' }, $set: { role: 'viewer' } }),
120+
]);
121+
await Organization.findByIdAndDelete(id);
122+
123+
// deleteAnalysis: unscoped hard delete
124+
await Analysis.findByIdAndDelete(id);
125+
126+
// deleteTemplate: soft delete — only active templates
127+
const template = await Template.findOne({ _id: id, isActive: true });
128+
await Template.findByIdAndUpdate(id, { isActive: false });
129+
```
130+
131+
---
132+
133+
## Correctness Properties
134+
135+
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
136+
137+
### Property 1: Contract hard-delete removes the document
138+
139+
*For any* existing Contract document, after `deleteContract` executes successfully, `Contract.findById(id)` SHALL return `null`.
140+
141+
**Validates: Requirements 2.3**
142+
143+
---
144+
145+
### Property 2: Contract deletion decrements org contractCount
146+
147+
*For any* Organization with `contractCount` N and any Contract belonging to that org, after `deleteContract` executes successfully, the Organization's `contractCount` SHALL equal N − 1.
148+
149+
**Validates: Requirements 2.4**
150+
151+
---
152+
153+
### Property 3: Org deletion soft-deletes all member contracts
154+
155+
*For any* Organization with any number of Contract documents, after `deleteOrganization` executes successfully, every Contract that had `orgId` equal to that org's `_id` SHALL have `isDeleted: true` and a non-null `deletedAt` timestamp.
156+
157+
**Validates: Requirements 3.3**
158+
159+
---
160+
161+
### Property 4: Org deletion hard-deletes all member analyses
162+
163+
*For any* Organization with any number of Analysis documents, after `deleteOrganization` executes successfully, `Analysis.find({ orgId })` SHALL return an empty array.
164+
165+
**Validates: Requirements 3.4**
166+
167+
---
168+
169+
### Property 5: Org hard-delete removes the organization document
170+
171+
*For any* existing Organization document, after `deleteOrganization` executes successfully, `Organization.findById(id)` SHALL return `null`.
172+
173+
**Validates: Requirements 3.5**
174+
175+
---
176+
177+
### Property 6: Org deletion clears all member user associations
178+
179+
*For any* Organization with any number of member Users, after `deleteOrganization` executes successfully, every User whose `organization` field referenced that org's `_id` SHALL have `organization: null` (or unset) and `role: 'viewer'`.
180+
181+
**Validates: Requirements 3.6**
182+
183+
---
184+
185+
### Property 7: Analysis hard-delete removes the document
186+
187+
*For any* existing Analysis document, after `deleteAnalysis` executes successfully, `Analysis.findById(id)` SHALL return `null`.
188+
189+
**Validates: Requirements 4.3**
190+
191+
---
192+
193+
### Property 8: Template soft-delete sets isActive to false
194+
195+
*For any* active Template document (`isActive: true`), after `deleteTemplate` executes successfully, the Template document SHALL have `isActive: false`.
196+
197+
**Validates: Requirements 5.3**
198+
199+
---
200+
201+
### Property 9: Every successful admin delete operation produces an audit log entry
202+
203+
*For any* successful invocation of any of the four delete handlers, the AuditLog collection SHALL contain exactly one new entry with the correct `action`, `resourceType`, and `resourceId` matching the deleted resource.
204+
205+
**Validates: Requirements 2.5, 3.7, 4.4, 5.4**
206+
207+
---
208+
209+
## Error Handling
210+
211+
All handlers use `asyncWrapper` — no try/catch blocks in handler code. Errors thrown by Mongoose or application logic propagate to the global error handler in `src/middleware/errorHandler.middleware.js`.
212+
213+
### Per-handler error cases
214+
215+
| Condition | Response |
216+
|-----------|----------|
217+
| Resource not found | `sendError(res, { statusCode: 404, code: 'NOT_FOUND', message: '...' })` |
218+
| Template already inactive | `sendError(res, { statusCode: 404, code: 'NOT_FOUND', message: 'Template not found.' })` |
219+
| Invalid ObjectId format | Mongoose `CastError` → global handler → 400 |
220+
| Unexpected DB error | Mongoose error → global handler → 500 `INTERNAL_ERROR` |
221+
| No JWT | `authenticate` middleware → 401 `UNAUTHORIZED` |
222+
| Non-admin role | `authorize('admin')` middleware → 403 `FORBIDDEN` |
223+
| Rate limit exceeded | `rateLimiter('strict')` middleware → 429 |
224+
225+
### Audit log failures
226+
227+
`auditService.log` swallows its own errors internally (existing behavior). A failed audit write will never cause the delete operation to fail or return an error response.
228+
229+
### Organization cascade partial failure
230+
231+
If one of the cascade operations in `deleteOrganization` throws (e.g., a DB timeout during `Analysis.deleteMany`), the error propagates through `asyncWrapper` and the entire request fails with 500. The cascade is not atomic — some writes may have already completed. This is an acceptable trade-off given the admin-only, low-frequency nature of this operation. A future improvement could wrap the cascade in a MongoDB transaction.
232+
233+
---
234+
235+
## Testing Strategy
236+
237+
### Unit Tests (example-based)
238+
239+
Each handler should have unit tests covering:
240+
241+
- **Happy path**: resource exists → correct delete operation → correct audit log call → correct response shape
242+
- **Not found**: non-existent ID → 404 + `NOT_FOUND` code
243+
- **Template already inactive**: `isActive: false` → 404 (same as not found)
244+
- **Auth/authz**: no token → 401, non-admin token → 403 (middleware tests, shared across all endpoints)
245+
246+
Use `jest` with `mongodb-memory-server` for in-memory MongoDB, consistent with the existing test setup (`jest.config.cjs`).
247+
248+
### Property-Based Tests
249+
250+
Use `fast-check` for property-based testing. Each property test runs a minimum of 100 iterations.
251+
252+
**Property 1 — Contract hard-delete removes the document**
253+
```
254+
// Feature: admin-delete-api, Property 1: contract hard-delete removes the document
255+
fc.assert(fc.asyncProperty(fc.record({ title: fc.string(), ... }), async (contractData) => {
256+
const contract = await Contract.create({ ...contractData, orgId, uploadedBy });
257+
await deleteContract({ params: { id: contract._id }, user: { userId: adminId }, ip, headers });
258+
expect(await Contract.findById(contract._id)).toBeNull();
259+
}), { numRuns: 100 });
260+
```
261+
262+
**Property 2 — contractCount decrements by 1**
263+
```
264+
// Feature: admin-delete-api, Property 2: contract deletion decrements org contractCount
265+
// Generate org with random contractCount N, create a contract, delete it, verify N-1
266+
```
267+
268+
**Property 3 — Org deletion soft-deletes all contracts**
269+
```
270+
// Feature: admin-delete-api, Property 3: org deletion soft-deletes all member contracts
271+
// Generate org with 1..20 contracts, delete org, verify all contracts have isDeleted: true
272+
```
273+
274+
**Property 4 — Org deletion hard-deletes all analyses**
275+
```
276+
// Feature: admin-delete-api, Property 4: org deletion hard-deletes all member analyses
277+
// Generate org with 0..10 analyses, delete org, verify Analysis.find({orgId}) is empty
278+
```
279+
280+
**Property 5 — Org hard-delete removes the org document**
281+
```
282+
// Feature: admin-delete-api, Property 5: org hard-delete removes the organization document
283+
```
284+
285+
**Property 6 — Org deletion clears user associations**
286+
```
287+
// Feature: admin-delete-api, Property 6: org deletion clears all member user associations
288+
// Generate org with 1..10 member users, delete org, verify all users have organization: null, role: 'viewer'
289+
```
290+
291+
**Property 7 — Analysis hard-delete removes the document**
292+
```
293+
// Feature: admin-delete-api, Property 7: analysis hard-delete removes the document
294+
```
295+
296+
**Property 8 — Template soft-delete sets isActive: false**
297+
```
298+
// Feature: admin-delete-api, Property 8: template soft-delete sets isActive to false
299+
// Generate active templates with varying fields, soft-delete, verify isActive: false
300+
```
301+
302+
**Property 9 — Audit log entry created for every delete**
303+
```
304+
// Feature: admin-delete-api, Property 9: every successful admin delete produces an audit log entry
305+
// For each of the 4 handlers, generate a resource, delete it, verify AuditLog contains correct entry
306+
```
307+
308+
### Integration Tests
309+
310+
- Verify the four routes are registered and reachable (smoke test against a running server or supertest)
311+
- Verify the middleware chain (auth, authz, rate limiter) is applied — call each endpoint without a token and with a non-admin token

0 commit comments

Comments
 (0)