Skip to content

Commit b08f78e

Browse files
authored
Merge pull request #2607 from trycompai/lewis/comp-delete-org-trace
[dev] [carhartlewis] lewis/comp-delete-org-trace
2 parents 987ad1e + 68328ee commit b08f78e

13 files changed

Lines changed: 1304 additions & 4 deletions

apps/api/src/admin-organizations/admin-audit-log.interceptor.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ describe('AdminAuditLogInterceptor', () => {
8282
let interceptor: AdminAuditLogInterceptor;
8383

8484
beforeEach(() => {
85-
interceptor = new AdminAuditLogInterceptor();
85+
interceptor = new AdminAuditLogInterceptor({
86+
get: jest.fn().mockReturnValue(false),
87+
} as never);
8688
jest.clearAllMocks();
8789
mockPolicyFind.mockResolvedValue(null);
8890
mockTaskFind.mockResolvedValue(null);

apps/api/src/admin-organizations/admin-audit-log.interceptor.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
NestInterceptor,
77
} from '@nestjs/common';
88
import { AuditLogEntityType, db, Prisma } from '@db';
9+
import { Reflector } from '@nestjs/core';
910
import { Observable, tap } from 'rxjs';
1011
import { MUTATION_METHODS, SENSITIVE_KEYS } from '../audit/audit-log.constants';
12+
import { SKIP_ADMIN_AUDIT_LOG_KEY } from './skip-admin-audit-log.decorator';
1113

1214
const SEGMENT_TO_RESOURCE: Record<
1315
string,
@@ -41,7 +43,17 @@ interface ParsedPath {
4143
export class AdminAuditLogInterceptor implements NestInterceptor {
4244
private readonly logger = new Logger(AdminAuditLogInterceptor.name);
4345

46+
constructor(private readonly reflector: Reflector) {}
47+
4448
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
49+
const skip = this.reflector.get<boolean>(
50+
SKIP_ADMIN_AUDIT_LOG_KEY,
51+
context.getHandler(),
52+
);
53+
if (skip) {
54+
return next.handle();
55+
}
56+
4557
const request = context.switchToHttp().getRequest();
4658
const method: string = request.method;
4759

apps/api/src/admin-organizations/admin-organizations.controller.spec.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,28 @@ jest.mock('../auth/auth.server', () => ({
1414
auth: { api: {} },
1515
}));
1616

17-
jest.mock('@db', () => ({ db: {} }));
17+
jest.mock('@db', () => ({
18+
db: {},
19+
AuditLogEntityType: {
20+
organization: 'organization',
21+
people: 'people',
22+
control: 'control',
23+
policy: 'policy',
24+
task: 'task',
25+
vendor: 'vendor',
26+
risk: 'risk',
27+
finding: 'finding',
28+
framework: 'framework',
29+
integration: 'integration',
30+
trust: 'trust',
31+
},
32+
CommentEntityType: {
33+
task: 'task',
34+
vendor: 'vendor',
35+
risk: 'risk',
36+
policy: 'policy',
37+
},
38+
}));
1839

1940
describe('AdminOrganizationsController', () => {
2041
let controller: AdminOrganizationsController;
@@ -28,12 +49,20 @@ describe('AdminOrganizationsController', () => {
2849
revokeInvitation: jest.fn(),
2950
getAuditLogs: jest.fn(),
3051
};
52+
const mockPurgeService = {
53+
purgeOrganization: jest.fn(),
54+
};
3155

3256
beforeEach(async () => {
3357
const module: TestingModule = await Test.createTestingModule({
3458
controllers: [AdminOrganizationsController],
3559
providers: [
3660
{ provide: AdminOrganizationsService, useValue: mockService },
61+
{
62+
provide: require('./purge-organization.service')
63+
.PurgeOrganizationService,
64+
useValue: mockPurgeService,
65+
},
3766
],
3867
}).compile();
3968

@@ -160,6 +189,28 @@ describe('AdminOrganizationsController', () => {
160189
});
161190
});
162191

192+
describe('purge', () => {
193+
it('should call purge service with confirm, id, and acting user', async () => {
194+
mockPurgeService.purgeOrganization.mockResolvedValue({
195+
success: true,
196+
organizationId: 'org_1',
197+
});
198+
199+
const result = await controller.purge(
200+
'org_1',
201+
{ userId: 'usr_admin' } as { userId: string },
202+
{ confirm: 'acme' },
203+
);
204+
205+
expect(mockPurgeService.purgeOrganization).toHaveBeenCalledWith({
206+
organizationId: 'org_1',
207+
confirm: 'acme',
208+
adminUserId: 'usr_admin',
209+
});
210+
expect(result).toEqual({ success: true, organizationId: 'org_1' });
211+
});
212+
});
213+
163214
describe('revokeInvitation', () => {
164215
it('should call service with org id and invitation id', async () => {
165216
mockService.revokeInvitation.mockResolvedValue({ success: true });

apps/api/src/admin-organizations/admin-organizations.controller.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ import { ApiExcludeController, ApiOperation, ApiQuery, ApiTags } from '@nestjs/s
1717
import { Throttle } from '@nestjs/throttler';
1818
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
1919
import { AdminOrganizationsService } from './admin-organizations.service';
20+
import { PurgeOrganizationService } from './purge-organization.service';
2021
import { AdminAuditLogInterceptor } from './admin-audit-log.interceptor';
22+
import { SkipAdminAuditLog } from './skip-admin-audit-log.decorator';
2123
import { InviteMemberDto } from './dto/invite-member.dto';
24+
import { PurgeOrganizationDto } from './dto/purge-organization.dto';
2225

2326
@ApiExcludeController()
2427
@ApiTags('Admin - Organizations')
@@ -27,7 +30,10 @@ import { InviteMemberDto } from './dto/invite-member.dto';
2730
@UseInterceptors(AdminAuditLogInterceptor)
2831
@Throttle({ default: { ttl: 60000, limit: 30 } })
2932
export class AdminOrganizationsController {
30-
constructor(private readonly service: AdminOrganizationsService) {}
33+
constructor(
34+
private readonly service: AdminOrganizationsService,
35+
private readonly purgeService: PurgeOrganizationService,
36+
) {}
3137

3238
@Get()
3339
@ApiOperation({ summary: 'List all organizations (platform admin)' })
@@ -159,6 +165,32 @@ export class AdminOrganizationsController {
159165
return this.service.listInvitations(id);
160166
}
161167

168+
@Delete(':id')
169+
@SkipAdminAuditLog()
170+
@ApiOperation({
171+
summary:
172+
'Permanently delete organization and all associated data (platform admin)',
173+
})
174+
@Throttle({ default: { ttl: 60000, limit: 2 } })
175+
@UsePipes(
176+
new ValidationPipe({
177+
whitelist: true,
178+
forbidNonWhitelisted: true,
179+
transform: true,
180+
}),
181+
)
182+
async purge(
183+
@Param('id') id: string,
184+
@Req() req: { userId: string },
185+
@Body() body: PurgeOrganizationDto,
186+
) {
187+
return this.purgeService.purgeOrganization({
188+
organizationId: id,
189+
confirm: body.confirm,
190+
adminUserId: req.userId,
191+
});
192+
}
193+
162194
@Delete(':id/invitations/:invId')
163195
@ApiOperation({ summary: 'Revoke invitation (platform admin)' })
164196
@Throttle({ default: { ttl: 60000, limit: 10 } })

apps/api/src/admin-organizations/admin-organizations.module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { CommentsModule } from '../comments/comments.module';
99
import { AttachmentsModule } from '../attachments/attachments.module';
1010
import { AdminOrganizationsController } from './admin-organizations.controller';
1111
import { AdminOrganizationsService } from './admin-organizations.service';
12+
import { PurgeOrganizationService } from './purge-organization.service';
13+
import { PurgeOrganizationSnapshotService } from './purge-organization-snapshot.service';
14+
import { PurgeOrganizationExternalService } from './purge-organization-external.service';
1215
import { AdminFindingsController } from './admin-findings.controller';
1316
import { AdminPoliciesController } from './admin-policies.controller';
1417
import { AdminTasksController } from './admin-tasks.controller';
@@ -36,6 +39,11 @@ import { AdminEvidenceController } from './admin-evidence.controller';
3639
AdminContextController,
3740
AdminEvidenceController,
3841
],
39-
providers: [AdminOrganizationsService],
42+
providers: [
43+
AdminOrganizationsService,
44+
PurgeOrganizationService,
45+
PurgeOrganizationSnapshotService,
46+
PurgeOrganizationExternalService,
47+
],
4048
})
4149
export class AdminOrganizationsModule {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
3+
4+
export class PurgeOrganizationDto {
5+
@ApiProperty({
6+
description:
7+
'The target organization slug. Must match exactly to confirm deletion.',
8+
example: 'acme-corp',
9+
})
10+
@IsString()
11+
@IsNotEmpty()
12+
@MinLength(1)
13+
confirm: string;
14+
}

0 commit comments

Comments
 (0)