Skip to content

Commit 3c40a3e

Browse files
authored
Merge branch 'main' into lewis/comp-settings-secrets-ui-fix
2 parents 51a9918 + 0246a10 commit 3c40a3e

6 files changed

Lines changed: 150 additions & 312 deletions

File tree

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

Lines changed: 49 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -104,69 +104,38 @@ export class TrustAccessService {
104104
}
105105

106106
/**
107-
* Create a URL-friendly slug from organization name
107+
* Ensure organization has a friendlyUrl, defaulting to organizationId
108108
*/
109-
private slugifyOrganizationName(name: string): string {
110-
const cleaned = name
111-
.trim()
112-
.toLowerCase()
113-
.replace(/&/g, 'and')
114-
.replace(/[^a-z0-9]+/g, '-')
115-
.replace(/-+/g, '-')
116-
.replace(/^-|-$/g, '');
117-
118-
return cleaned.slice(0, 60);
119-
}
120-
121-
/**
122-
* Ensure organization has a friendlyUrl, create one if missing
123-
*/
124-
private async ensureFriendlyUrl(params: {
125-
organizationId: string;
126-
organizationName: string;
127-
}): Promise<string> {
128-
const { organizationId, organizationName } = params;
129-
109+
private async ensureFriendlyUrl(organizationId: string): Promise<string> {
130110
const current = await db.trust.findUnique({
131111
where: { organizationId },
132112
select: { friendlyUrl: true },
133113
});
134114

135115
if (current?.friendlyUrl) return current.friendlyUrl;
136116

137-
const baseCandidate =
138-
this.slugifyOrganizationName(organizationName) ||
139-
`org-${organizationId.slice(-8)}`;
140-
141-
for (let i = 0; i < 25; i += 1) {
142-
const candidate = i === 0 ? baseCandidate : `${baseCandidate}-${i + 1}`;
143-
144-
const taken = await db.trust.findUnique({
145-
where: { friendlyUrl: candidate },
146-
select: { organizationId: true },
117+
// Use organizationId as the default friendlyUrl (guaranteed unique)
118+
try {
119+
await db.trust.upsert({
120+
where: { organizationId },
121+
update: { friendlyUrl: organizationId },
122+
create: { organizationId, friendlyUrl: organizationId, status: 'published' },
147123
});
148-
149-
if (taken && taken.organizationId !== organizationId) continue;
150-
151-
try {
152-
await db.trust.upsert({
124+
return organizationId;
125+
} catch (error: unknown) {
126+
if (
127+
error instanceof Prisma.PrismaClientKnownRequestError &&
128+
error.code === 'P2002'
129+
) {
130+
// If somehow there's a conflict, the friendlyUrl already exists
131+
const existing = await db.trust.findUnique({
153132
where: { organizationId },
154-
update: { friendlyUrl: candidate },
155-
create: { organizationId, friendlyUrl: candidate },
133+
select: { friendlyUrl: true },
156134
});
157-
return candidate;
158-
} catch (error: unknown) {
159-
if (
160-
error instanceof Prisma.PrismaClientKnownRequestError &&
161-
error.code === 'P2002'
162-
) {
163-
continue;
164-
}
165-
throw error;
135+
return existing?.friendlyUrl ?? organizationId;
166136
}
137+
throw error;
167138
}
168-
169-
return organizationId;
170139
}
171140

172141
/**
@@ -176,7 +145,7 @@ export class TrustAccessService {
176145
organizationId: string;
177146
organizationName: string;
178147
}): Promise<string> {
179-
const { organizationId, organizationName } = params;
148+
const { organizationId } = params;
180149

181150
const trust = await db.trust.findUnique({
182151
where: { organizationId },
@@ -188,8 +157,7 @@ export class TrustAccessService {
188157
}
189158

190159
const urlId =
191-
trust?.friendlyUrl ||
192-
(await this.ensureFriendlyUrl({ organizationId, organizationName }));
160+
trust?.friendlyUrl || (await this.ensureFriendlyUrl(organizationId));
193161

194162
return `${this.normalizeUrl(this.TRUST_APP_URL)}/${urlId}`;
195163
}
@@ -225,8 +193,34 @@ export class TrustAccessService {
225193
});
226194
}
227195

228-
if (!trust || trust.status !== 'published') {
229-
throw new NotFoundException('Trust site not found or not published');
196+
// If still no trust record but we have an organization, auto-create it
197+
if (!trust) {
198+
const organization = await db.organization.findUnique({
199+
where: { id },
200+
});
201+
202+
if (!organization) {
203+
throw new NotFoundException('Trust site not found');
204+
}
205+
206+
// Auto-create trust record with organizationId as friendlyUrl
207+
trust = await db.trust.create({
208+
data: {
209+
organizationId: id,
210+
friendlyUrl: id,
211+
status: 'published',
212+
},
213+
include: { organization: true },
214+
});
215+
}
216+
217+
// Ensure the trust portal is published (auto-publish if draft)
218+
if (trust.status !== 'published') {
219+
trust = await db.trust.update({
220+
where: { organizationId: trust.organizationId },
221+
data: { status: 'published' },
222+
include: { organization: true },
223+
});
230224
}
231225

232226
return trust;

apps/app/src/app/(app)/[orgId]/trust/page.tsx

Lines changed: 40 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { env } from '@/env.mjs';
22
import { auth } from '@/utils/auth';
33
import { db } from '@db';
4-
import { PageHeader, PageLayout } from '@trycompai/design-system';
4+
import { Button, PageHeader, PageLayout } from '@trycompai/design-system';
5+
import { Launch } from '@trycompai/design-system/icons';
56
import { Prisma } from '@prisma/client';
67
import type { Metadata } from 'next';
78
import { headers } from 'next/headers';
9+
import Link from 'next/link';
810
import { TrustPortalSwitch } from './portal-settings/components/TrustPortalSwitch';
911

1012
export default async function TrustPage({ params }: { params: Promise<{ orgId: string }> }) {
1113
const { orgId } = await params;
1214

13-
// Fetch portal settings data
14-
await ensureFriendlyUrlIfEnabled(orgId);
15+
// Ensure Trust record exists with default friendlyUrl
16+
await ensureTrustRecord(orgId);
1517
const trustPortal = await getTrustPortal(orgId);
1618
const certificateFiles = await fetchComplianceCertificates(orgId);
1719
const faqs = await fetchOrganizationFaqs(orgId);
@@ -21,13 +23,28 @@ export default async function TrustPage({ params }: { params: Promise<{ orgId: s
2123
orderBy: { createdAt: 'desc' },
2224
});
2325

26+
// Build the public trust portal URL
27+
const portalUrl =
28+
trustPortal?.domainVerified && trustPortal.domain
29+
? `https://${trustPortal.domain}`
30+
: `https://trust.inc/${trustPortal?.friendlyUrl ?? orgId}`;
31+
2432
return (
25-
<PageLayout header={<PageHeader title="Portal Settings" />}>
33+
<PageLayout
34+
header={
35+
<PageHeader
36+
title="Trust Portal"
37+
actions={
38+
<Button iconRight={<Launch />}>
39+
<Link href={portalUrl} target="_blank" rel="noopener noreferrer">
40+
Visit Trust Portal
41+
</Link>
42+
</Button>
43+
}
44+
/>
45+
}
46+
>
2647
<TrustPortalSwitch
27-
enabled={trustPortal?.enabled ?? false}
28-
slug={trustPortal?.friendlyUrl ?? orgId}
29-
domain={trustPortal?.domain ?? ''}
30-
domainVerified={trustPortal?.domainVerified ?? false}
3148
orgId={orgId}
3249
soc2type1={trustPortal?.soc2type1 ?? false}
3350
soc2type2={trustPortal?.soc2type2 ?? false}
@@ -85,7 +102,6 @@ const getTrustPortal = async (orgId: string) => {
85102
});
86103

87104
return {
88-
enabled: trustPortal?.status === 'published',
89105
domain: trustPortal?.domain,
90106
domainVerified: trustPortal?.domainVerified,
91107
soc2type1: trustPortal?.soc2type1,
@@ -114,66 +130,32 @@ const getTrustPortal = async (orgId: string) => {
114130
};
115131

116132
/**
117-
* Create a URL-friendly slug from organization name
118-
*/
119-
const slugifyOrganizationName = (name: string): string => {
120-
const cleaned = name
121-
.trim()
122-
.toLowerCase()
123-
.replace(/&/g, 'and')
124-
.replace(/[^a-z0-9]+/g, '-')
125-
.replace(/-+/g, '-')
126-
.replace(/^-|-$/g, '');
127-
128-
return cleaned.slice(0, 60);
129-
};
130-
131-
/**
132-
* Ensure friendlyUrl exists for enabled trust portals
133-
* This auto-heals cases where portal was enabled before friendlyUrl was required
133+
* Ensure Trust record exists with friendlyUrl defaulting to organizationId
134134
*/
135-
const ensureFriendlyUrlIfEnabled = async (organizationId: string): Promise<void> => {
135+
const ensureTrustRecord = async (organizationId: string): Promise<void> => {
136136
const trust = await db.trust.findUnique({
137137
where: { organizationId },
138-
select: { status: true, friendlyUrl: true },
138+
select: { friendlyUrl: true },
139139
});
140140

141-
// Only sync if portal is enabled and friendlyUrl is missing
142-
if (!trust || trust.status !== 'published' || trust.friendlyUrl) {
141+
// If trust record exists with friendlyUrl, nothing to do
142+
if (trust?.friendlyUrl) {
143143
return;
144144
}
145145

146-
const org = await db.organization.findUnique({
147-
where: { id: organizationId },
148-
select: { name: true },
149-
});
150-
151-
if (!org) return;
152-
153-
const baseCandidate = slugifyOrganizationName(org.name) || `org-${organizationId.slice(-8)}`;
154-
155-
for (let i = 0; i < 25; i += 1) {
156-
const candidate = i === 0 ? baseCandidate : `${baseCandidate}-${i + 1}`;
157-
158-
const taken = await db.trust.findUnique({
159-
where: { friendlyUrl: candidate },
160-
select: { organizationId: true },
146+
// Create or update trust record with organizationId as default friendlyUrl
147+
try {
148+
await db.trust.upsert({
149+
where: { organizationId },
150+
update: { friendlyUrl: organizationId },
151+
create: { organizationId, friendlyUrl: organizationId, status: 'published' },
161152
});
162-
163-
if (taken && taken.organizationId !== organizationId) continue;
164-
165-
try {
166-
await db.trust.update({
167-
where: { organizationId },
168-
data: { friendlyUrl: candidate },
169-
});
153+
} catch (error: unknown) {
154+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
155+
// Conflict on unique constraint - record already exists
170156
return;
171-
} catch (error: unknown) {
172-
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
173-
continue;
174-
}
175-
throw error;
176157
}
158+
throw error;
177159
}
178160
};
179161

0 commit comments

Comments
 (0)