@@ -20,7 +20,7 @@ import { PolicyPdfRendererService } from './policy-pdf-renderer.service';
2020import { GetObjectCommand , PutObjectCommand } from '@aws-sdk/client-s3' ;
2121import { getSignedUrl } from '@aws-sdk/s3-request-presigner' ;
2222import { APP_AWS_ORG_ASSETS_BUCKET , s3Client } from '../app/s3' ;
23- import { TrustFramework } from '@prisma/client' ;
23+ import { Prisma , TrustFramework } from '@prisma/client' ;
2424import archiver from 'archiver' ;
2525import { PassThrough , Readable } from 'stream' ;
2626
@@ -35,6 +35,130 @@ export class TrustAccessService {
3535 return randomBytes ( length ) . toString ( 'base64url' ) . slice ( 0 , length ) ;
3636 }
3737
38+ /**
39+ * Normalize URL by removing trailing slash
40+ */
41+ private normalizeUrl ( input : string ) : string {
42+ return input . endsWith ( '/' ) ? input . slice ( 0 , - 1 ) : input ;
43+ }
44+
45+ /**
46+ * Normalize domain by removing protocol and path
47+ */
48+ private normalizeDomain ( input : string ) : string {
49+ const trimmed = input . trim ( ) ;
50+ const withoutProtocol = trimmed . replace ( / ^ h t t p s ? : \/ \/ / i, '' ) ;
51+ const withoutPath = withoutProtocol . split ( '/' ) [ 0 ] ?? withoutProtocol ;
52+ return withoutPath . trim ( ) . toLowerCase ( ) ;
53+ }
54+
55+ /**
56+ * Create a URL-friendly slug from organization name
57+ */
58+ private slugifyOrganizationName ( name : string ) : string {
59+ const cleaned = name
60+ . trim ( )
61+ . toLowerCase ( )
62+ . replace ( / & / g, 'and' )
63+ . replace ( / [ ^ a - z 0 - 9 ] + / g, '-' )
64+ . replace ( / - + / g, '-' )
65+ . replace ( / ^ - | - $ / g, '' ) ;
66+
67+ return cleaned . slice ( 0 , 60 ) ;
68+ }
69+
70+ /**
71+ * Ensure organization has a friendlyUrl, create one if missing
72+ */
73+ private async ensureFriendlyUrl ( params : {
74+ organizationId : string ;
75+ organizationName : string ;
76+ } ) : Promise < string > {
77+ const { organizationId, organizationName } = params ;
78+
79+ const current = await db . trust . findUnique ( {
80+ where : { organizationId } ,
81+ select : { friendlyUrl : true } ,
82+ } ) ;
83+
84+ if ( current ?. friendlyUrl ) return current . friendlyUrl ;
85+
86+ const baseCandidate =
87+ this . slugifyOrganizationName ( organizationName ) ||
88+ `org-${ organizationId . slice ( - 8 ) } ` ;
89+
90+ for ( let i = 0 ; i < 25 ; i += 1 ) {
91+ const candidate = i === 0 ? baseCandidate : `${ baseCandidate } -${ i + 1 } ` ;
92+
93+ const taken = await db . trust . findUnique ( {
94+ where : { friendlyUrl : candidate } ,
95+ select : { organizationId : true } ,
96+ } ) ;
97+
98+ if ( taken && taken . organizationId !== organizationId ) continue ;
99+
100+ try {
101+ await db . trust . upsert ( {
102+ where : { organizationId } ,
103+ update : { friendlyUrl : candidate } ,
104+ create : { organizationId, friendlyUrl : candidate } ,
105+ } ) ;
106+ return candidate ;
107+ } catch ( error : unknown ) {
108+ if (
109+ error instanceof Prisma . PrismaClientKnownRequestError &&
110+ error . code === 'P2002'
111+ ) {
112+ continue ;
113+ }
114+ throw error ;
115+ }
116+ }
117+
118+ return organizationId ;
119+ }
120+
121+ /**
122+ * Build portal base URL, checking custom domain first
123+ */
124+ private async buildPortalBaseUrl ( params : {
125+ organizationId : string ;
126+ organizationName : string ;
127+ } ) : Promise < string > {
128+ const { organizationId, organizationName } = params ;
129+
130+ const trust = await db . trust . findUnique ( {
131+ where : { organizationId } ,
132+ select : { domain : true , domainVerified : true , friendlyUrl : true } ,
133+ } ) ;
134+
135+ if ( trust ?. domain && trust . domainVerified ) {
136+ return `https://${ this . normalizeDomain ( trust . domain ) } ` ;
137+ }
138+
139+ const urlId =
140+ trust ?. friendlyUrl ||
141+ ( await this . ensureFriendlyUrl ( { organizationId, organizationName } ) ) ;
142+
143+ return `${ this . normalizeUrl ( this . TRUST_APP_URL ) } /${ urlId } ` ;
144+ }
145+
146+ /**
147+ * Build portal access URL with access token
148+ */
149+ private async buildPortalAccessUrl ( params : {
150+ organizationId : string ;
151+ organizationName : string ;
152+ accessToken : string ;
153+ } ) : Promise < string > {
154+ const { organizationId, organizationName, accessToken } = params ;
155+ const base = await this . buildPortalBaseUrl ( {
156+ organizationId,
157+ organizationName,
158+ } ) ;
159+ return `${ base } /access/${ accessToken } ` ;
160+ }
161+
38162 private async findPublishedTrustByRouteId ( id : string ) {
39163 // First, try treating `id` as the existing friendlyUrl.
40164 let trust = await db . trust . findUnique ( {
@@ -560,6 +684,66 @@ export class TrustAccessService {
560684 return updatedGrant ;
561685 }
562686
687+ async resendAccessGrantEmail ( organizationId : string , grantId : string ) {
688+ const grant = await db . trustAccessGrant . findFirst ( {
689+ where : {
690+ id : grantId ,
691+ accessRequest : {
692+ organizationId,
693+ } ,
694+ } ,
695+ include : {
696+ accessRequest : {
697+ include : {
698+ organization : {
699+ select : { name : true } ,
700+ } ,
701+ } ,
702+ } ,
703+ } ,
704+ } ) ;
705+
706+ if ( ! grant ) {
707+ throw new NotFoundException ( 'Grant not found' ) ;
708+ }
709+
710+ if ( grant . status !== 'active' ) {
711+ throw new BadRequestException (
712+ `Cannot resend access email for ${ grant . status } grant` ,
713+ ) ;
714+ }
715+
716+ // Generate a new access token if expired or missing
717+ let accessToken = grant . accessToken ;
718+ const now = new Date ( ) ;
719+
720+ if ( ! accessToken || ( grant . accessTokenExpiresAt && grant . accessTokenExpiresAt < now ) ) {
721+ accessToken = this . generateToken ( 32 ) ;
722+ const accessTokenExpiresAt = new Date ( now . getTime ( ) + 24 * 60 * 60 * 1000 ) ;
723+
724+ await db . trustAccessGrant . update ( {
725+ where : { id : grantId } ,
726+ data : { accessToken, accessTokenExpiresAt } ,
727+ } ) ;
728+ }
729+
730+ const portalUrl = await this . buildPortalAccessUrl ( {
731+ organizationId,
732+ organizationName : grant . accessRequest . organization . name ,
733+ accessToken,
734+ } ) ;
735+
736+ await this . emailService . sendAccessGrantedEmail ( {
737+ toEmail : grant . subjectEmail ,
738+ toName : grant . accessRequest . name ,
739+ organizationName : grant . accessRequest . organization . name ,
740+ expiresAt : grant . expiresAt ,
741+ portalUrl,
742+ } ) ;
743+
744+ return { message : 'Access email resent successfully' } ;
745+ }
746+
563747 async getNdaByToken ( token : string ) {
564748 const nda = await db . trustNDAAgreement . findUnique ( {
565749 where : { signToken : token } ,
@@ -577,15 +761,11 @@ export class TrustAccessService {
577761 throw new NotFoundException ( 'NDA agreement not found' ) ;
578762 }
579763
580- const trust = await db . trust . findUnique ( {
581- where : { organizationId : nda . organizationId } ,
582- select : { friendlyUrl : true } ,
764+ const portalUrl = await this . buildPortalBaseUrl ( {
765+ organizationId : nda . organizationId ,
766+ organizationName : nda . accessRequest . organization . name ,
583767 } ) ;
584768
585- const portalUrl = trust ?. friendlyUrl
586- ? `${ this . TRUST_APP_URL } /${ trust . friendlyUrl } `
587- : null ;
588-
589769 const baseResponse = {
590770 id : nda . id ,
591771 organizationName : nda . accessRequest . organization . name ,
@@ -612,11 +792,13 @@ export class TrustAccessService {
612792 }
613793
614794 if ( nda . status === 'signed' ) {
615- let accessUrl = portalUrl ;
795+ let accessUrl : string | null = portalUrl ;
616796 if ( nda . grant ?. accessToken && nda . grant . status === 'active' ) {
617- if ( trust ?. friendlyUrl ) {
618- accessUrl = `${ this . TRUST_APP_URL } /${ trust . friendlyUrl } /access/${ nda . grant . accessToken } ` ;
619- }
797+ accessUrl = await this . buildPortalAccessUrl ( {
798+ organizationId : nda . organizationId ,
799+ organizationName : nda . accessRequest . organization . name ,
800+ accessToken : nda . grant . accessToken ,
801+ } ) ;
620802 }
621803
622804 return {
@@ -683,15 +865,12 @@ export class TrustAccessService {
683865 } ) ;
684866 }
685867
686- const trust = await db . trust . findUnique ( {
687- where : { organizationId : nda . organizationId } ,
688- select : { friendlyUrl : true } ,
868+ const portalUrl = await this . buildPortalAccessUrl ( {
869+ organizationId : nda . organizationId ,
870+ organizationName : nda . accessRequest . organization . name ,
871+ accessToken,
689872 } ) ;
690873
691- const portalUrl = trust ?. friendlyUrl
692- ? `${ this . TRUST_APP_URL } /${ trust . friendlyUrl } /access/${ accessToken } `
693- : null ;
694-
695874 return {
696875 message : 'NDA already signed' ,
697876 grant : nda . grant ,
@@ -754,15 +933,12 @@ export class TrustAccessService {
754933 return { grant, updatedNda } ;
755934 } ) ;
756935
757- const trust = await db . trust . findUnique ( {
758- where : { organizationId : nda . organizationId } ,
759- select : { friendlyUrl : true } ,
936+ const portalUrl = await this . buildPortalAccessUrl ( {
937+ organizationId : nda . organizationId ,
938+ organizationName : nda . accessRequest . organization . name ,
939+ accessToken,
760940 } ) ;
761941
762- const portalUrl = trust ?. friendlyUrl
763- ? `${ this . TRUST_APP_URL } /${ trust . friendlyUrl } /access/${ accessToken } `
764- : null ;
765-
766942 await this . emailService . sendAccessGrantedEmail ( {
767943 toEmail : signerEmail ,
768944 toName : signerName ,
@@ -975,8 +1151,11 @@ export class TrustAccessService {
9751151 } ) ;
9761152 }
9771153
978- const urlId = trust . friendlyUrl || trust . organizationId ;
979- let accessLink = `${ this . TRUST_APP_URL } /${ urlId } /access/${ accessToken } ` ;
1154+ let accessLink = await this . buildPortalAccessUrl ( {
1155+ organizationId : trust . organizationId ,
1156+ organizationName : grant . accessRequest . organization . name ,
1157+ accessToken,
1158+ } ) ;
9801159
9811160 // Append query parameter if provided
9821161 if ( query ) {
0 commit comments