Skip to content

Commit e551f04

Browse files
committed
chore: merge main into release for new releases
2 parents 1fe796b + 02b7e22 commit e551f04

File tree

112 files changed

+8999
-607
lines changed

Some content is hidden

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

112 files changed

+8999
-607
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ APP_AWS_SECRET_ACCESS_KEY="" # AWS Secret Access Key
1818
APP_AWS_REGION="" # AWS Region
1919
APP_AWS_BUCKET_NAME="" # AWS Bucket Name
2020
APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET="" # AWS, Required for Security Questionnaire feature
21-
21+
APP_AWS_KNOWLEDGE_BASE_BUCKET="" # AWS Required for the Knowledge Base feature in Security Questionnaire
2222
TRIGGER_SECRET_KEY="" # For background jobs. Self-host or use cloud-version @ https://trigger.dev
2323
# TRIGGER_API_URL="" # Only set if you are self-hosting
2424
TRIGGER_API_KEY="" # API key from Trigger.dev

SELF_HOSTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ App (`apps/app`):
4545

4646
- **APP_AWS_REGION**, **APP_AWS_ACCESS_KEY_ID**, **APP_AWS_SECRET_ACCESS_KEY**, **APP_AWS_BUCKET_NAME**: AWS S3 credentials for file storage (attachments, general uploads).
4747
- **APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET**: AWS S3 bucket name specifically for questionnaire file uploads. Required for the Security Questionnaire feature. If not set, users will see an error when trying to parse questionnaires.
48+
- **APP_AWS_KNOWLEDGE_BASE_BUCKET**: AWS S3 bucket name specifically for knowledge base documents. Required for the Knowledge Base feature in Security Questionnaire. If not set, users will see an error when trying to upload knowledge base documents.
4849
- **OPENAI_API_KEY**: Enables AI features that call OpenAI models.
4950
- **UPSTASH_REDIS_REST_URL**, **UPSTASH_REDIS_REST_TOKEN**: Optional Redis (Upstash) used for rate limiting/queues/caching.
5051
- **NEXT_PUBLIC_POSTHOG_KEY**, **NEXT_PUBLIC_POSTHOG_HOST**: Client analytics via PostHog; leave unset to disable.
@@ -151,6 +152,7 @@ NEXT_PUBLIC_BETTER_AUTH_URL_PORTAL=http://localhost:3002
151152
# APP_AWS_SECRET_ACCESS_KEY=
152153
# APP_AWS_BUCKET_NAME=
153154
# APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET=
155+
# APP_AWS_KNOWLEDGE_BASE_BUCKET=
154156
# OPENAI_API_KEY=
155157
# UPSTASH_REDIS_REST_URL=
156158
# UPSTASH_REDIS_REST_TOKEN=

apps/api/src/trust-portal/trust-access.controller.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import {
1515
ApiHeader,
1616
ApiOperation,
17+
ApiParam,
1718
ApiResponse,
1819
ApiSecurity,
1920
ApiTags,
@@ -44,11 +45,16 @@ export class TrustAccessController {
4445
description:
4546
'External users submit request for data access from trust site',
4647
})
48+
@ApiParam({
49+
name: 'friendlyUrl',
50+
description: 'Trust Portal friendly URL or Organization ID',
51+
})
4752
@ApiResponse({
4853
status: HttpStatus.CREATED,
4954
description: 'Access request created and sent for review',
5055
})
5156
async createAccessRequest(
57+
// Note: friendlyUrl can be either the custom friendly URL or the organization ID
5258
@Param('friendlyUrl') friendlyUrl: string,
5359
@Body() dto: CreateAccessRequestDto,
5460
@Req() req: Request,
@@ -365,11 +371,16 @@ export class TrustAccessController {
365371
description:
366372
'Generate access link for users with existing grants to redownload data',
367373
})
374+
@ApiParam({
375+
name: 'friendlyUrl',
376+
description: 'Trust Portal friendly URL or Organization ID',
377+
})
368378
@ApiResponse({
369379
status: HttpStatus.OK,
370380
description: 'Access link sent to email',
371381
})
372382
async reclaimAccess(
383+
// Note: friendlyUrl can be either the custom friendly URL or the organization ID
373384
@Param('friendlyUrl') friendlyUrl: string,
374385
@Body() dto: ReclaimAccessDto,
375386
) {

apps/api/src/trust-portal/trust-access.service.ts

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ export class TrustAccessService {
2828
return randomBytes(length).toString('base64url').slice(0, length);
2929
}
3030

31+
private async findPublishedTrustByRouteId(id: string) {
32+
// First, try treating `id` as the existing friendlyUrl.
33+
let trust = await db.trust.findUnique({
34+
where: { friendlyUrl: id },
35+
include: { organization: true },
36+
});
37+
38+
// If none found, fall back to treating `id` as organizationId.
39+
if (!trust) {
40+
trust = await db.trust.findFirst({
41+
where: { organizationId: id },
42+
include: { organization: true },
43+
});
44+
}
45+
46+
if (!trust || trust.status !== 'published') {
47+
throw new NotFoundException('Trust site not found or not published');
48+
}
49+
50+
return trust;
51+
}
52+
3153
constructor(
3254
private readonly ndaPdfService: NdaPdfService,
3355
private readonly emailService: TrustEmailService,
@@ -60,19 +82,12 @@ export class TrustAccessService {
6082
}
6183

6284
async createAccessRequest(
63-
friendlyUrl: string,
85+
id: string,
6486
dto: CreateAccessRequestDto,
6587
ipAddress: string | undefined,
6688
userAgent: string | undefined,
6789
) {
68-
const trust = await db.trust.findUnique({
69-
where: { friendlyUrl },
70-
include: { organization: true },
71-
});
72-
73-
if (!trust || trust.status !== 'published') {
74-
throw new NotFoundException('Trust site not found or not published');
75-
}
90+
const trust = await this.findPublishedTrustByRouteId(id);
7691

7792
// Check if the email already has an active grant
7893
const existingGrant = await db.trustAccessGrant.findFirst({
@@ -470,33 +485,67 @@ export class TrustAccessService {
470485
organization: true,
471486
},
472487
},
488+
grant: true,
473489
},
474490
});
475491

476492
if (!nda) {
477493
throw new NotFoundException('NDA agreement not found');
478494
}
479495

496+
const trust = await db.trust.findUnique({
497+
where: { organizationId: nda.organizationId },
498+
select: { friendlyUrl: true },
499+
});
500+
501+
const portalUrl = trust?.friendlyUrl
502+
? `${this.TRUST_APP_URL}/${trust.friendlyUrl}`
503+
: null;
504+
505+
const baseResponse = {
506+
id: nda.id,
507+
organizationName: nda.accessRequest.organization.name,
508+
requesterName: nda.accessRequest.name,
509+
requesterEmail: nda.accessRequest.email,
510+
expiresAt: nda.signTokenExpiresAt,
511+
portalUrl,
512+
};
513+
480514
if (nda.signTokenExpiresAt < new Date()) {
481-
throw new BadRequestException('NDA signing link has expired');
515+
return {
516+
...baseResponse,
517+
status: 'expired',
518+
message: 'NDA signing link has expired',
519+
};
482520
}
483521

484522
if (nda.status === 'void') {
485-
throw new BadRequestException(
486-
'This NDA has been revoked and is no longer valid',
487-
);
523+
return {
524+
...baseResponse,
525+
status: 'void',
526+
message: 'This NDA has been revoked and is no longer valid',
527+
};
488528
}
489529

490-
if (nda.status !== 'pending') {
491-
throw new BadRequestException('NDA has already been signed');
530+
if (nda.status === 'signed') {
531+
let accessUrl = portalUrl;
532+
if (nda.grant?.accessToken && nda.grant.status === 'active') {
533+
if (trust?.friendlyUrl) {
534+
accessUrl = `${this.TRUST_APP_URL}/${trust.friendlyUrl}/access/${nda.grant.accessToken}`;
535+
}
536+
}
537+
538+
return {
539+
...baseResponse,
540+
status: 'signed',
541+
message: 'NDA has already been signed',
542+
portalUrl: accessUrl,
543+
};
492544
}
493545

494546
return {
495-
id: nda.id,
496-
organizationName: nda.accessRequest.organization.name,
497-
requesterName: nda.accessRequest.name,
498-
requesterEmail: nda.accessRequest.email,
499-
expiresAt: nda.signTokenExpiresAt,
547+
...baseResponse,
548+
status: 'pending',
500549
};
501550
}
502551

@@ -791,15 +840,8 @@ export class TrustAccessService {
791840
};
792841
}
793842

794-
async reclaimAccess(friendlyUrl: string, email: string) {
795-
const trust = await db.trust.findUnique({
796-
where: { friendlyUrl },
797-
include: { organization: true },
798-
});
799-
800-
if (!trust || trust.status !== 'published') {
801-
throw new NotFoundException('Trust site not found or not published');
802-
}
843+
async reclaimAccess(id: string, email: string) {
844+
const trust = await this.findPublishedTrustByRouteId(id);
803845

804846
const grant = await db.trustAccessGrant.findFirst({
805847
where: {
@@ -849,7 +891,8 @@ export class TrustAccessService {
849891
});
850892
}
851893

852-
const accessLink = `${this.TRUST_APP_URL}/${friendlyUrl}/access/${accessToken}`;
894+
const urlId = trust.friendlyUrl || trust.organizationId;
895+
const accessLink = `${this.TRUST_APP_URL}/${urlId}/access/${accessToken}`;
853896

854897
await this.emailService.sendAccessReclaimEmail({
855898
toEmail: email,

apps/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"geist": "^1.3.1",
7676
"jspdf": "^3.0.2",
7777
"lucide-react": "^0.544.0",
78+
"mammoth": "^1.11.0",
7879
"motion": "^12.9.2",
7980
"next": "^15.4.6",
8081
"next-safe-action": "^8.0.3",

apps/app/public/badges/iso9001.svg

Lines changed: 21 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)