Skip to content

Commit 2ba3d5d

Browse files
ItsnotakaMarfuen
andauthored
feat(trust-access): implement trust access request management system (#1739)
* feat(trust-access): implement trust access request management system * feat(trust-access): implement trust access request management system * feat: clean up logs * feat(trust-access): validate user ID from member ID in trust access service * feat(trust-access): enhance member ID handling and user verification * feat: added trust page navigation * feat(api): update dependencies and add new packages for nanoid and pdf-lib * feat(trust-access): implement NDA signing email resend functionality * feat(trust-access): add endpoint to preview NDA with watermark * feat(trust-access): add reclaim access endpoint and email notification * feat(trust-access): add document access and download endpoints with email updates * feat(trust-access): integrate @tanstack/react-form for access request dialogs * feat(trust-access): enforce scope selection and validate NDA status * feat(trust-access): update approve button to include request details * fix: import fix for monorepo * fix: build fix * feat(trust-access): add requested duration days to access request * refactor(trust-access): remove scopes from access request and related services * feat(trust-access): implement policies access and download endpoints * chore: format * refactor(trust-access): rename TrustAccessRequestsClient and remove unused components * style(trust-access): update dialog components for consistent layout * chore(workflows): add daniel/* to auto PR trigger paths * chore(db): refactor migration --------- Signed-off-by: Mariano Fuentes <marfuen98@gmail.com> Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
1 parent 60dfb28 commit 2ba3d5d

File tree

98 files changed

+7066
-1662
lines changed

Some content is hidden

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

98 files changed

+7066
-1662
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ TRIGGER_SECRET_KEY="" # Secret key from Trigger.dev
2626
OPENAI_API_KEY="" # AI Chat + Auto Generated Policies, Risks + Vendors
2727
FIRECRAWL_API_KEY="" # For research, self-host or use cloud-version @ https://firecrawl.dev
2828

29+
TRUST_APP_URL="http://localhost:3008" # Trust portal public site for NDA signing and access requests
30+
2931
AUTH_TRUSTED_ORIGINS=http://localhost:3000,https://*.trycomp.ai,http://localhost:3002

.github/workflows/auto-pr-to-main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
- claudio/*
1414
- mariano/*
1515
- lewis/*
16+
- daniel/*
1617
- COMP-*
1718
- cursor/*
1819
- codex/*

apps/api/buildspec.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ phases:
2626
- '[ -n "$DATABASE_URL" ] || { echo "❌ DATABASE_URL is not set"; exit 1; }'
2727
- '[ -n "$BASE_URL" ] || { echo "❌ BASE_URL is not set"; exit 1; }'
2828
- '[ -n "$BETTER_AUTH_URL" ] || { echo "❌ BETTER_AUTH_URL is not set"; exit 1; }'
29+
- '[ -n "$TRUST_APP_URL" ] || { echo "❌ TRUST_APP_URL is not set"; exit 1; }'
2930

3031
# Install only API workspace dependencies
3132
- echo "Installing API dependencies only..."

apps/api/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
"class-validator": "^0.14.2",
2121
"dotenv": "^17.2.3",
2222
"jose": "^6.0.12",
23+
"jspdf": "^3.0.3",
24+
"nanoid": "^5.1.6",
25+
"pdf-lib": "^1.17.1",
2326
"prisma": "^6.13.0",
2427
"reflect-metadata": "^0.2.2",
28+
"resend": "^6.4.2",
2529
"rxjs": "^7.8.1",
2630
"swagger-ui-express": "^5.0.1",
2731
"zod": "^4.0.14"

apps/api/src/attachments/attachments.service.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,60 @@ export class AttachmentsService {
267267
});
268268
}
269269

270-
/**
271-
* Sanitize filename for S3 storage
272-
*/
270+
async uploadToS3(
271+
fileBuffer: Buffer,
272+
fileName: string,
273+
contentType: string,
274+
organizationId: string,
275+
entityType: string,
276+
entityId: string,
277+
): Promise<string> {
278+
const fileId = randomBytes(16).toString('hex');
279+
const sanitizedFileName = this.sanitizeFileName(fileName);
280+
const timestamp = Date.now();
281+
const s3Key = `${organizationId}/attachments/${entityType}/${entityId}/${timestamp}-${fileId}-${sanitizedFileName}`;
282+
283+
const putCommand = new PutObjectCommand({
284+
Bucket: this.bucketName,
285+
Key: s3Key,
286+
Body: fileBuffer,
287+
ContentType: contentType,
288+
Metadata: {
289+
originalFileName: this.sanitizeHeaderValue(fileName),
290+
organizationId,
291+
entityId,
292+
entityType,
293+
},
294+
});
295+
296+
await this.s3Client.send(putCommand);
297+
return s3Key;
298+
}
299+
300+
async getPresignedDownloadUrl(s3Key: string): Promise<string> {
301+
return this.generateSignedUrl(s3Key);
302+
}
303+
304+
async getObjectBuffer(s3Key: string): Promise<Buffer> {
305+
const getCommand = new GetObjectCommand({
306+
Bucket: this.bucketName,
307+
Key: s3Key,
308+
});
309+
310+
const response = await this.s3Client.send(getCommand);
311+
const chunks: Uint8Array[] = [];
312+
313+
if (!response.Body) {
314+
throw new InternalServerErrorException('No file data received from S3');
315+
}
316+
317+
for await (const chunk of response.Body as any) {
318+
chunks.push(chunk);
319+
}
320+
321+
return Buffer.concat(chunks);
322+
}
323+
273324
private sanitizeFileName(fileName: string): string {
274325
return fileName.replace(/[^a-zA-Z0-9.-]/g, '_');
275326
}
@@ -281,6 +332,7 @@ export class AttachmentsService {
281332
* - Trim whitespace
282333
*/
283334
private sanitizeHeaderValue(value: string): string {
335+
// eslint-disable-next-line no-control-regex
284336
const withoutControls = value.replace(/[\x00-\x1F\x7F]/g, '');
285337
const asciiOnly = withoutControls.replace(/[^\x20-\x7E]/g, '_');
286338
return asciiOnly.trim();

apps/api/src/comments/dto/update-comment.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ export class UpdateCommentDto {
1111
@IsNotEmpty()
1212
@MaxLength(2000)
1313
content: string;
14-
}
14+
}

apps/api/src/context/context.controller.ts

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import {
2-
Controller,
3-
Get,
1+
import {
2+
Controller,
3+
Get,
44
Post,
55
Patch,
66
Delete,
77
Body,
88
Param,
9-
UseGuards
9+
UseGuards,
1010
} from '@nestjs/common';
1111
import {
1212
ApiBody,
@@ -17,10 +17,7 @@ import {
1717
ApiSecurity,
1818
ApiTags,
1919
} from '@nestjs/swagger';
20-
import {
21-
AuthContext,
22-
OrganizationId,
23-
} from '../auth/auth-context.decorator';
20+
import { AuthContext, OrganizationId } from '../auth/auth-context.decorator';
2421
import { HybridAuthGuard } from '../auth/hybrid-auth.guard';
2522
import type { AuthContext as AuthContextType } from '../auth/types';
2623
import { CreateContextDto } from './dto/create-context.dto';
@@ -58,18 +55,20 @@ export class ContextController {
5855
@OrganizationId() organizationId: string,
5956
@AuthContext() authContext: AuthContextType,
6057
) {
61-
const contextEntries = await this.contextService.findAllByOrganization(organizationId);
58+
const contextEntries =
59+
await this.contextService.findAllByOrganization(organizationId);
6260

6361
return {
6462
data: contextEntries,
6563
count: contextEntries.length,
6664
authType: authContext.authType,
67-
...(authContext.userId && authContext.userEmail && {
68-
authenticatedUser: {
69-
id: authContext.userId,
70-
email: authContext.userEmail,
71-
},
72-
}),
65+
...(authContext.userId &&
66+
authContext.userEmail && {
67+
authenticatedUser: {
68+
id: authContext.userId,
69+
email: authContext.userEmail,
70+
},
71+
}),
7372
};
7473
}
7574

@@ -85,17 +84,21 @@ export class ContextController {
8584
@OrganizationId() organizationId: string,
8685
@AuthContext() authContext: AuthContextType,
8786
) {
88-
const contextEntry = await this.contextService.findById(contextId, organizationId);
87+
const contextEntry = await this.contextService.findById(
88+
contextId,
89+
organizationId,
90+
);
8991

9092
return {
9193
...contextEntry,
9294
authType: authContext.authType,
93-
...(authContext.userId && authContext.userEmail && {
94-
authenticatedUser: {
95-
id: authContext.userId,
96-
email: authContext.userEmail,
97-
},
98-
}),
95+
...(authContext.userId &&
96+
authContext.userEmail && {
97+
authenticatedUser: {
98+
id: authContext.userId,
99+
email: authContext.userEmail,
100+
},
101+
}),
99102
};
100103
}
101104

@@ -112,17 +115,21 @@ export class ContextController {
112115
@OrganizationId() organizationId: string,
113116
@AuthContext() authContext: AuthContextType,
114117
) {
115-
const contextEntry = await this.contextService.create(organizationId, createContextDto);
118+
const contextEntry = await this.contextService.create(
119+
organizationId,
120+
createContextDto,
121+
);
116122

117123
return {
118124
...contextEntry,
119125
authType: authContext.authType,
120-
...(authContext.userId && authContext.userEmail && {
121-
authenticatedUser: {
122-
id: authContext.userId,
123-
email: authContext.userEmail,
124-
},
125-
}),
126+
...(authContext.userId &&
127+
authContext.userEmail && {
128+
authenticatedUser: {
129+
id: authContext.userId,
130+
email: authContext.userEmail,
131+
},
132+
}),
126133
};
127134
}
128135

@@ -150,12 +157,13 @@ export class ContextController {
150157
return {
151158
...updatedContextEntry,
152159
authType: authContext.authType,
153-
...(authContext.userId && authContext.userEmail && {
154-
authenticatedUser: {
155-
id: authContext.userId,
156-
email: authContext.userEmail,
157-
},
158-
}),
160+
...(authContext.userId &&
161+
authContext.userEmail && {
162+
authenticatedUser: {
163+
id: authContext.userId,
164+
email: authContext.userEmail,
165+
},
166+
}),
159167
};
160168
}
161169

@@ -171,17 +179,21 @@ export class ContextController {
171179
@OrganizationId() organizationId: string,
172180
@AuthContext() authContext: AuthContextType,
173181
) {
174-
const result = await this.contextService.deleteById(contextId, organizationId);
182+
const result = await this.contextService.deleteById(
183+
contextId,
184+
organizationId,
185+
);
175186

176187
return {
177188
...result,
178189
authType: authContext.authType,
179-
...(authContext.userId && authContext.userEmail && {
180-
authenticatedUser: {
181-
id: authContext.userId,
182-
email: authContext.userEmail,
183-
},
184-
}),
190+
...(authContext.userId &&
191+
authContext.userEmail && {
192+
authenticatedUser: {
193+
id: authContext.userId,
194+
email: authContext.userEmail,
195+
},
196+
}),
185197
};
186198
}
187199
}

apps/api/src/context/context.service.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,37 @@ export class ContextService {
1414
orderBy: { createdAt: 'desc' },
1515
});
1616

17-
this.logger.log(`Retrieved ${contextEntries.length} context entries for organization ${organizationId}`);
17+
this.logger.log(
18+
`Retrieved ${contextEntries.length} context entries for organization ${organizationId}`,
19+
);
1820
return contextEntries;
1921
} catch (error) {
20-
this.logger.error(`Failed to retrieve context entries for organization ${organizationId}:`, error);
22+
this.logger.error(
23+
`Failed to retrieve context entries for organization ${organizationId}:`,
24+
error,
25+
);
2126
throw error;
2227
}
2328
}
2429

2530
async findById(id: string, organizationId: string) {
2631
try {
2732
const contextEntry = await db.context.findFirst({
28-
where: {
33+
where: {
2934
id,
30-
organizationId
35+
organizationId,
3136
},
3237
});
3338

3439
if (!contextEntry) {
35-
throw new NotFoundException(`Context entry with ID ${id} not found in organization ${organizationId}`);
40+
throw new NotFoundException(
41+
`Context entry with ID ${id} not found in organization ${organizationId}`,
42+
);
3643
}
3744

38-
this.logger.log(`Retrieved context entry: ${contextEntry.question.substring(0, 50)}... (${id})`);
45+
this.logger.log(
46+
`Retrieved context entry: ${contextEntry.question.substring(0, 50)}... (${id})`,
47+
);
3948
return contextEntry;
4049
} catch (error) {
4150
if (error instanceof NotFoundException) {
@@ -55,15 +64,24 @@ export class ContextService {
5564
},
5665
});
5766

58-
this.logger.log(`Created new context entry: ${contextEntry.question.substring(0, 50)}... (${contextEntry.id}) for organization ${organizationId}`);
67+
this.logger.log(
68+
`Created new context entry: ${contextEntry.question.substring(0, 50)}... (${contextEntry.id}) for organization ${organizationId}`,
69+
);
5970
return contextEntry;
6071
} catch (error) {
61-
this.logger.error(`Failed to create context entry for organization ${organizationId}:`, error);
72+
this.logger.error(
73+
`Failed to create context entry for organization ${organizationId}:`,
74+
error,
75+
);
6276
throw error;
6377
}
6478
}
6579

66-
async updateById(id: string, organizationId: string, updateContextDto: UpdateContextDto) {
80+
async updateById(
81+
id: string,
82+
organizationId: string,
83+
updateContextDto: UpdateContextDto,
84+
) {
6785
try {
6886
// First check if the context entry exists in the organization
6987
await this.findById(id, organizationId);
@@ -73,7 +91,9 @@ export class ContextService {
7391
data: updateContextDto,
7492
});
7593

76-
this.logger.log(`Updated context entry: ${updatedContextEntry.question.substring(0, 50)}... (${id})`);
94+
this.logger.log(
95+
`Updated context entry: ${updatedContextEntry.question.substring(0, 50)}... (${id})`,
96+
);
7797
return updatedContextEntry;
7898
} catch (error) {
7999
if (error instanceof NotFoundException) {
@@ -93,13 +113,15 @@ export class ContextService {
93113
where: { id },
94114
});
95115

96-
this.logger.log(`Deleted context entry: ${existingContextEntry.question.substring(0, 50)}... (${id})`);
97-
return {
116+
this.logger.log(
117+
`Deleted context entry: ${existingContextEntry.question.substring(0, 50)}... (${id})`,
118+
);
119+
return {
98120
message: 'Context entry deleted successfully',
99121
deletedContext: {
100122
id: existingContextEntry.id,
101123
question: existingContextEntry.question,
102-
}
124+
},
103125
};
104126
} catch (error) {
105127
if (error instanceof NotFoundException) {

apps/api/src/context/dto/context-response.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export class ContextResponseDto {
2121

2222
@ApiProperty({
2323
description: 'The answer or detailed explanation for the question',
24-
example: 'We use a hybrid authentication system supporting both API keys and session-based authentication.',
24+
example:
25+
'We use a hybrid authentication system supporting both API keys and session-based authentication.',
2526
})
2627
answer: string;
2728

0 commit comments

Comments
 (0)