Skip to content

Commit 615f09d

Browse files
authored
Merge pull request #2356 from trycompai/main
[comp] Production Deploy
2 parents e03db05 + eac8a20 commit 615f09d

File tree

121 files changed

+12491
-170
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+12491
-170
lines changed

apps/api/src/app.module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import { EvidenceExportModule } from './tasks/evidence-export/evidence-export.mo
2121
import { VendorsModule } from './vendors/vendors.module';
2222
import { ContextModule } from './context/context.module';
2323
import { TrustPortalModule } from './trust-portal/trust-portal.module';
24+
import { ControlTemplateModule } from './framework-editor/control-template/control-template.module';
25+
import { FrameworkEditorFrameworkModule } from './framework-editor/framework/framework.module';
26+
import { PolicyTemplateModule } from './framework-editor/policy-template/policy-template.module';
27+
import { RequirementModule } from './framework-editor/requirement/requirement.module';
2428
import { TaskTemplateModule } from './framework-editor/task-template/task-template.module';
2529
import { FindingTemplateModule } from './finding-template/finding-template.module';
2630
import { FindingsModule } from './findings/findings.module';
@@ -78,6 +82,10 @@ import { AdminOrganizationsModule } from './admin-organizations/admin-organizati
7882
CommentsModule,
7983
HealthModule,
8084
TrustPortalModule,
85+
ControlTemplateModule,
86+
FrameworkEditorFrameworkModule,
87+
PolicyTemplateModule,
88+
RequirementModule,
8189
TaskTemplateModule,
8290
FindingTemplateModule,
8391
FindingsModule,

apps/api/src/auth/auth.server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { ac, allRoles } from '@trycompai/auth';
1717
import { createAuthMiddleware } from 'better-auth/api';
1818
import { Redis } from '@upstash/redis';
19+
import type { AccessControl } from 'better-auth/plugins/access';
1920

2021
const MAGIC_LINK_EXPIRES_IN_SECONDS = 60 * 60; // 1 hour
2122

@@ -47,13 +48,15 @@ export function getTrustedOrigins(): string[] {
4748
'http://localhost:3000',
4849
'http://localhost:3002',
4950
'http://localhost:3333',
51+
'http://localhost:3004',
5052
'https://app.trycomp.ai',
5153
'https://portal.trycomp.ai',
5254
'https://api.trycomp.ai',
5355
'https://app.staging.trycomp.ai',
5456
'https://portal.staging.trycomp.ai',
5557
'https://api.staging.trycomp.ai',
5658
'https://dev.trycomp.ai',
59+
'https://framework-editor.trycomp.ai',
5760
];
5861
}
5962

@@ -414,7 +417,7 @@ export const auth = betterAuth({
414417
}),
415418
});
416419
},
417-
ac,
420+
ac: ac as AccessControl,
418421
roles: allRoles,
419422
// Enable dynamic access control for custom roles
420423
// This allows organizations to create custom roles at runtime
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Patch,
6+
Delete,
7+
Body,
8+
Param,
9+
Query,
10+
UseGuards,
11+
UsePipes,
12+
ValidationPipe,
13+
} from '@nestjs/common';
14+
import { ApiTags } from '@nestjs/swagger';
15+
import { PlatformAdminGuard } from '../../auth/platform-admin.guard';
16+
import { CreateControlTemplateDto } from './dto/create-control-template.dto';
17+
import { UpdateControlTemplateDto } from './dto/update-control-template.dto';
18+
import { ControlTemplateService } from './control-template.service';
19+
20+
@ApiTags('Framework Editor Control Templates')
21+
@Controller({ path: 'framework-editor/control-template', version: '1' })
22+
@UseGuards(PlatformAdminGuard)
23+
export class ControlTemplateController {
24+
constructor(private readonly service: ControlTemplateService) {}
25+
26+
@Get()
27+
async findAll(
28+
@Query('take') take?: string,
29+
@Query('skip') skip?: string,
30+
@Query('frameworkId') frameworkId?: string,
31+
) {
32+
const limit = Math.min(Number(take) || 500, 500);
33+
const offset = Number(skip) || 0;
34+
return this.service.findAll(limit, offset, frameworkId);
35+
}
36+
37+
@Get(':id')
38+
async findOne(@Param('id') id: string) {
39+
return this.service.findById(id);
40+
}
41+
42+
@Post()
43+
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
44+
async create(
45+
@Body() dto: CreateControlTemplateDto,
46+
@Query('frameworkId') frameworkId?: string,
47+
) {
48+
return this.service.create(dto, frameworkId);
49+
}
50+
51+
@Patch(':id')
52+
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
53+
async update(
54+
@Param('id') id: string,
55+
@Body() dto: UpdateControlTemplateDto,
56+
) {
57+
return this.service.update(id, dto);
58+
}
59+
60+
@Delete(':id')
61+
async delete(@Param('id') id: string) {
62+
return this.service.delete(id);
63+
}
64+
65+
@Post(':id/requirements/:reqId')
66+
async linkRequirement(
67+
@Param('id') id: string,
68+
@Param('reqId') reqId: string,
69+
) {
70+
return this.service.linkRequirement(id, reqId);
71+
}
72+
73+
@Delete(':id/requirements/:reqId')
74+
async unlinkRequirement(
75+
@Param('id') id: string,
76+
@Param('reqId') reqId: string,
77+
) {
78+
return this.service.unlinkRequirement(id, reqId);
79+
}
80+
81+
@Post(':id/policy-templates/:ptId')
82+
async linkPolicyTemplate(
83+
@Param('id') id: string,
84+
@Param('ptId') ptId: string,
85+
) {
86+
return this.service.linkPolicyTemplate(id, ptId);
87+
}
88+
89+
@Delete(':id/policy-templates/:ptId')
90+
async unlinkPolicyTemplate(
91+
@Param('id') id: string,
92+
@Param('ptId') ptId: string,
93+
) {
94+
return this.service.unlinkPolicyTemplate(id, ptId);
95+
}
96+
97+
@Post(':id/task-templates/:ttId')
98+
async linkTaskTemplate(
99+
@Param('id') id: string,
100+
@Param('ttId') ttId: string,
101+
) {
102+
return this.service.linkTaskTemplate(id, ttId);
103+
}
104+
105+
@Delete(':id/task-templates/:ttId')
106+
async unlinkTaskTemplate(
107+
@Param('id') id: string,
108+
@Param('ttId') ttId: string,
109+
) {
110+
return this.service.unlinkTaskTemplate(id, ttId);
111+
}
112+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { AuthModule } from '../../auth/auth.module';
3+
import { ControlTemplateController } from './control-template.controller';
4+
import { ControlTemplateService } from './control-template.service';
5+
6+
@Module({
7+
imports: [AuthModule],
8+
controllers: [ControlTemplateController],
9+
providers: [ControlTemplateService],
10+
exports: [ControlTemplateService],
11+
})
12+
export class ControlTemplateModule {}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { Injectable, NotFoundException, ConflictException, Logger } from '@nestjs/common';
2+
import { db, Prisma } from '@trycompai/db';
3+
import type { EvidenceFormType } from '@trycompai/db';
4+
import { CreateControlTemplateDto } from './dto/create-control-template.dto';
5+
import { UpdateControlTemplateDto } from './dto/update-control-template.dto';
6+
7+
@Injectable()
8+
export class ControlTemplateService {
9+
private readonly logger = new Logger(ControlTemplateService.name);
10+
11+
async findAll(take = 500, skip = 0, frameworkId?: string) {
12+
return db.frameworkEditorControlTemplate.findMany({
13+
take,
14+
skip,
15+
orderBy: { createdAt: 'asc' },
16+
where: frameworkId
17+
? { requirements: { some: { frameworkId } } }
18+
: undefined,
19+
include: {
20+
policyTemplates: { select: { id: true, name: true } },
21+
requirements: {
22+
select: {
23+
id: true,
24+
name: true,
25+
framework: { select: { name: true } },
26+
},
27+
},
28+
taskTemplates: { select: { id: true, name: true } },
29+
},
30+
});
31+
}
32+
33+
async findById(id: string) {
34+
const ct = await db.frameworkEditorControlTemplate.findUnique({
35+
where: { id },
36+
include: {
37+
policyTemplates: { select: { id: true, name: true } },
38+
requirements: {
39+
select: {
40+
id: true,
41+
name: true,
42+
framework: { select: { name: true } },
43+
},
44+
},
45+
taskTemplates: { select: { id: true, name: true } },
46+
},
47+
});
48+
if (!ct) throw new NotFoundException(`Control template ${id} not found`);
49+
return ct;
50+
}
51+
52+
async create(dto: CreateControlTemplateDto, frameworkId?: string) {
53+
const requirementIds = frameworkId
54+
? await db.frameworkEditorRequirement
55+
.findMany({
56+
where: { frameworkId },
57+
select: { id: true },
58+
})
59+
.then((reqs) => reqs.map((r) => ({ id: r.id })))
60+
: [];
61+
62+
const ct = await db.frameworkEditorControlTemplate.create({
63+
data: {
64+
name: dto.name,
65+
description: dto.description ?? '',
66+
...(dto.documentTypes && {
67+
documentTypes: dto.documentTypes as EvidenceFormType[],
68+
}),
69+
...(requirementIds.length > 0 && {
70+
requirements: { connect: requirementIds },
71+
}),
72+
},
73+
});
74+
this.logger.log(`Created control template: ${ct.name} (${ct.id})`);
75+
return ct;
76+
}
77+
78+
async update(id: string, dto: UpdateControlTemplateDto) {
79+
await this.findById(id);
80+
const updated = await db.frameworkEditorControlTemplate.update({
81+
where: { id },
82+
data: {
83+
...(dto.name !== undefined && { name: dto.name }),
84+
...(dto.description !== undefined && { description: dto.description }),
85+
...(dto.documentTypes !== undefined && {
86+
documentTypes: dto.documentTypes as EvidenceFormType[],
87+
}),
88+
},
89+
});
90+
this.logger.log(`Updated control template: ${updated.name} (${id})`);
91+
return updated;
92+
}
93+
94+
async delete(id: string) {
95+
await this.findById(id);
96+
try {
97+
await db.frameworkEditorControlTemplate.delete({ where: { id } });
98+
} catch (error) {
99+
if (
100+
error instanceof Prisma.PrismaClientKnownRequestError &&
101+
error.code === 'P2003'
102+
) {
103+
throw new ConflictException(
104+
'Cannot delete control template: it is referenced by existing controls',
105+
);
106+
}
107+
throw error;
108+
}
109+
this.logger.log(`Deleted control template ${id}`);
110+
return { message: 'Control template deleted successfully' };
111+
}
112+
113+
async linkRequirement(controlId: string, requirementId: string) {
114+
await db.frameworkEditorControlTemplate.update({
115+
where: { id: controlId },
116+
data: { requirements: { connect: { id: requirementId } } },
117+
});
118+
return { message: 'Requirement linked' };
119+
}
120+
121+
async unlinkRequirement(controlId: string, requirementId: string) {
122+
await db.frameworkEditorControlTemplate.update({
123+
where: { id: controlId },
124+
data: { requirements: { disconnect: { id: requirementId } } },
125+
});
126+
return { message: 'Requirement unlinked' };
127+
}
128+
129+
async linkPolicyTemplate(controlId: string, policyTemplateId: string) {
130+
await db.frameworkEditorControlTemplate.update({
131+
where: { id: controlId },
132+
data: { policyTemplates: { connect: { id: policyTemplateId } } },
133+
});
134+
return { message: 'Policy template linked' };
135+
}
136+
137+
async unlinkPolicyTemplate(controlId: string, policyTemplateId: string) {
138+
await db.frameworkEditorControlTemplate.update({
139+
where: { id: controlId },
140+
data: { policyTemplates: { disconnect: { id: policyTemplateId } } },
141+
});
142+
return { message: 'Policy template unlinked' };
143+
}
144+
145+
async linkTaskTemplate(controlId: string, taskTemplateId: string) {
146+
await db.frameworkEditorControlTemplate.update({
147+
where: { id: controlId },
148+
data: { taskTemplates: { connect: { id: taskTemplateId } } },
149+
});
150+
return { message: 'Task template linked' };
151+
}
152+
153+
async unlinkTaskTemplate(controlId: string, taskTemplateId: string) {
154+
await db.frameworkEditorControlTemplate.update({
155+
where: { id: controlId },
156+
data: { taskTemplates: { disconnect: { id: taskTemplateId } } },
157+
});
158+
return { message: 'Task template unlinked' };
159+
}
160+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import {
3+
IsString,
4+
IsNotEmpty,
5+
IsArray,
6+
IsOptional,
7+
MaxLength,
8+
} from 'class-validator';
9+
10+
export class CreateControlTemplateDto {
11+
@ApiProperty({ example: 'Access Control Policy' })
12+
@IsString()
13+
@IsNotEmpty()
14+
@MaxLength(255)
15+
name: string;
16+
17+
@ApiProperty({ example: 'Ensures access controls are properly managed' })
18+
@IsString()
19+
@MaxLength(5000)
20+
description: string;
21+
22+
@ApiPropertyOptional({ example: ['penetration-test', 'rbac-matrix'] })
23+
@IsArray()
24+
@IsString({ each: true })
25+
@IsOptional()
26+
documentTypes?: string[];
27+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { PartialType } from '@nestjs/swagger';
2+
import { CreateControlTemplateDto } from './create-control-template.dto';
3+
4+
export class UpdateControlTemplateDto extends PartialType(
5+
CreateControlTemplateDto,
6+
) {}

0 commit comments

Comments
 (0)