Skip to content

Commit 36492f9

Browse files
committed
Merge branch 'main' of https://github.com/trycompai/comp into mariano/agent-screenshots
2 parents 3eda590 + 89188e4 commit 36492f9

Some content is hidden

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

41 files changed

+1195
-242
lines changed

.npmrc

Whitespace-only changes.

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# [1.70.0](https://github.com/trycompai/comp/compare/v1.69.0...v1.70.0) (2025-12-12)
2+
3+
4+
### Bug Fixes
5+
6+
* **auditor:** increase max poll duration to 30 minutes and add limit to scrape ([#1897](https://github.com/trycompai/comp/issues/1897)) ([764d605](https://github.com/trycompai/comp/commit/764d6055fb8a1c7d4fa90d8d41c8234fc55c3de9))
7+
8+
9+
### Features
10+
11+
* **api:** add access request notification email functionality ([#1910](https://github.com/trycompai/comp/issues/1910)) ([8d2a811](https://github.com/trycompai/comp/commit/8d2a81127c523e92decc56e29a7ef4f53e28986d))
12+
113
# [1.69.0](https://github.com/trycompai/comp/compare/v1.68.1...v1.69.0) (2025-12-08)
214

315

apps/api/buildspec.yml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,20 @@ phases:
5050

5151
- echo "Checking build output..."
5252
- ls -la dist/
53-
- ls -la dist/src/
54-
- '[ -f "dist/src/main.js" ] || { echo "❌ main.js not found in dist/src"; exit 1; }'
53+
- ls -la dist/apps/api/src/ || ls -la dist/src/
54+
- '[ -f "dist/apps/api/src/main.js" ] || [ -f "dist/src/main.js" ] || { echo "❌ main.js not found"; exit 1; }'
5555

5656
- mkdir -p ../docker-build
57-
- cp -r dist/* ../docker-build/
57+
# Handle both output structures (dist/apps/api/src or dist/src)
58+
- |
59+
if [ -d "dist/apps/api/src" ]; then
60+
echo "Using composite output structure..."
61+
cp -r dist/apps/api/* ../docker-build/
62+
else
63+
echo "Using standard output structure..."
64+
cp -r dist/* ../docker-build/
65+
fi
5866
- cp -r prisma ../docker-build/
59-
6067
- echo "Verifying copied files..."
6168
- ls -la ../docker-build/
6269
- ls -la ../docker-build/src/

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@react-email/components": "^0.0.41",
2626
"@trigger.dev/build": "4.0.6",
2727
"@trigger.dev/sdk": "4.0.6",
28-
"@trycompai/db": "^1.3.17",
28+
"@trycompai/db": "1.3.19",
2929
"@upstash/vector": "^1.2.2",
3030
"adm-zip": "^0.5.16",
3131
"ai": "^5.0.60",
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
Body,
3+
Button,
4+
Container,
5+
Font,
6+
Heading,
7+
Html,
8+
Preview,
9+
Section,
10+
Tailwind,
11+
Text,
12+
} from '@react-email/components';
13+
import { Footer } from '../components/footer';
14+
import { Logo } from '../components/logo';
15+
16+
interface Props {
17+
organizationName: string;
18+
requesterName: string;
19+
requesterEmail: string;
20+
requesterCompany?: string | null;
21+
requesterJobTitle?: string | null;
22+
purpose?: string | null;
23+
requestedDurationDays?: number | null;
24+
reviewUrl: string;
25+
}
26+
27+
export const AccessRequestNotificationEmail = ({
28+
organizationName,
29+
requesterName,
30+
requesterEmail,
31+
requesterCompany,
32+
requesterJobTitle,
33+
purpose,
34+
requestedDurationDays,
35+
reviewUrl,
36+
}: Props) => {
37+
return (
38+
<Html>
39+
<Tailwind>
40+
<head>
41+
<Font
42+
fontFamily="Geist"
43+
fallbackFontFamily="Helvetica"
44+
fontWeight={400}
45+
fontStyle="normal"
46+
/>
47+
<Font
48+
fontFamily="Geist"
49+
fallbackFontFamily="Helvetica"
50+
fontWeight={500}
51+
fontStyle="normal"
52+
/>
53+
</head>
54+
<Preview>New Trust Portal Access Request from {requesterName}</Preview>
55+
56+
<Body className="mx-auto my-auto bg-[#fff] font-sans">
57+
<Container
58+
className="mx-auto my-[40px] max-w-[600px] border-transparent p-[20px] md:border-[#E8E7E1]"
59+
style={{ borderStyle: 'solid', borderWidth: 1 }}
60+
>
61+
<Logo />
62+
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-[#121212]">
63+
New Access Request
64+
</Heading>
65+
66+
<Text className="text-[14px] leading-[24px] text-[#121212]">
67+
A new request to access <strong>{organizationName}</strong>'s
68+
trust portal has been submitted and is awaiting your review.
69+
</Text>
70+
71+
<Section
72+
className="mt-[20px] mb-[20px] rounded-[3px] p-[15px]"
73+
style={{ backgroundColor: '#f8f9fa' }}
74+
>
75+
<Text className="m-0 mb-[10px] text-[14px] font-semibold text-[#121212]">
76+
Requester Details
77+
</Text>
78+
<Text className="m-0 text-[14px] leading-[20px] text-[#121212]">
79+
<strong>Name:</strong> {requesterName}
80+
<br />
81+
<strong>Email:</strong> {requesterEmail}
82+
{requesterCompany && (
83+
<>
84+
<br />
85+
<strong>Company:</strong> {requesterCompany}
86+
</>
87+
)}
88+
{requesterJobTitle && (
89+
<>
90+
<br />
91+
<strong>Job Title:</strong> {requesterJobTitle}
92+
</>
93+
)}
94+
</Text>
95+
</Section>
96+
97+
{purpose && (
98+
<Section className="mb-[20px]">
99+
<Text className="m-0 mb-[8px] text-[14px] font-semibold text-[#121212]">
100+
Purpose
101+
</Text>
102+
<Text className="m-0 text-[14px] leading-[20px] text-[#121212]">
103+
{purpose}
104+
</Text>
105+
</Section>
106+
)}
107+
108+
{requestedDurationDays && (
109+
<Text className="text-[14px] leading-[24px] text-[#121212]">
110+
<strong>Requested Access Duration:</strong>{' '}
111+
{requestedDurationDays} days
112+
</Text>
113+
)}
114+
115+
<Section className="mt-[32px] mb-[32px] text-center">
116+
<Button
117+
className="rounded-[3px] bg-[#121212] px-[20px] py-[12px] text-center text-[14px] font-semibold text-white no-underline"
118+
href={reviewUrl}
119+
>
120+
Review Request
121+
</Button>
122+
</Section>
123+
124+
<Section
125+
className="mt-[30px] mb-[20px] rounded-[3px] border-l-4 p-[15px]"
126+
style={{ backgroundColor: '#fff4e6', borderColor: '#f59e0b' }}
127+
>
128+
<Text className="m-0 text-[14px] leading-[24px] text-[#121212]">
129+
<strong>Action Required</strong>
130+
<br />
131+
Please review this request and either approve or deny access.
132+
Approved requests will require the requester to sign an NDA
133+
before accessing your trust portal.
134+
</Text>
135+
</Section>
136+
137+
<br />
138+
139+
<Footer />
140+
</Container>
141+
</Body>
142+
</Tailwind>
143+
</Html>
144+
);
145+
};
146+
147+
export default AccessRequestNotificationEmail;

apps/api/src/organization/dto/update-organization.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export interface UpdateOrganizationDto {
88
hasAccess?: boolean;
99
fleetDmLabelId?: number;
1010
isFleetSetupCompleted?: boolean;
11+
primaryColor?: string;
1112
}

apps/api/src/organization/organization.controller.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import {
66
Get,
77
Patch,
88
Post,
9+
Query,
910
UseGuards,
1011
} from '@nestjs/common';
1112
import {
1213
ApiBody,
1314
ApiHeader,
1415
ApiOperation,
16+
ApiQuery,
1517
ApiResponse,
1618
ApiSecurity,
1719
ApiTags,
@@ -30,6 +32,7 @@ import { GET_ORGANIZATION_RESPONSES } from './schemas/get-organization.responses
3032
import { UPDATE_ORGANIZATION_RESPONSES } from './schemas/update-organization.responses';
3133
import { DELETE_ORGANIZATION_RESPONSES } from './schemas/delete-organization.responses';
3234
import { TRANSFER_OWNERSHIP_RESPONSES } from './schemas/transfer-ownership.responses';
35+
import { GET_ORGANIZATION_PRIMARY_COLOR_RESPONSES } from './schemas/get-organization-primary-color';
3336
import {
3437
UPDATE_ORGANIZATION_BODY,
3538
TRANSFER_OWNERSHIP_BODY,
@@ -162,4 +165,47 @@ export class OrganizationController {
162165
}),
163166
};
164167
}
168+
169+
@Get('primary-color')
170+
@ApiOperation(ORGANIZATION_OPERATIONS.getPrimaryColor)
171+
@ApiQuery({
172+
name: 'token',
173+
required: false,
174+
description:
175+
'Access token for public access (alternative to authentication). When provided, authentication is not required.',
176+
example: 'tok_abc123def456',
177+
})
178+
@ApiResponse(GET_ORGANIZATION_PRIMARY_COLOR_RESPONSES[200])
179+
@ApiResponse(GET_ORGANIZATION_PRIMARY_COLOR_RESPONSES[401])
180+
@ApiResponse(GET_ORGANIZATION_PRIMARY_COLOR_RESPONSES[404])
181+
async getPrimaryColor(
182+
@Query('token') token: string | undefined,
183+
@OrganizationId() organizationId?: string,
184+
@AuthContext() authContext?: AuthContextType,
185+
) {
186+
// If token is provided, use it to resolve organization
187+
// Otherwise, require organizationId from auth
188+
if (!token && !organizationId) {
189+
throw new BadRequestException(
190+
'Either authentication or access token is required',
191+
);
192+
}
193+
194+
const primaryColor = await this.organizationService.getPrimaryColor(
195+
organizationId || '',
196+
token,
197+
);
198+
199+
return {
200+
...primaryColor,
201+
authType: token ? 'access-token' : authContext?.authType,
202+
// Include user context for session auth (helpful for debugging)
203+
...(authContext?.userId && {
204+
authenticatedUser: {
205+
id: authContext.userId,
206+
email: authContext.userEmail,
207+
},
208+
}),
209+
};
210+
}
165211
}

apps/api/src/organization/organization.service.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class OrganizationService {
2828
hasAccess: true,
2929
fleetDmLabelId: true,
3030
isFleetSetupCompleted: true,
31+
primaryColor: true,
3132
createdAt: true,
3233
},
3334
});
@@ -63,6 +64,7 @@ export class OrganizationService {
6364
hasAccess: true,
6465
fleetDmLabelId: true,
6566
isFleetSetupCompleted: true,
67+
primaryColor: true,
6668
createdAt: true,
6769
},
6870
});
@@ -86,6 +88,7 @@ export class OrganizationService {
8688
hasAccess: true,
8789
fleetDmLabelId: true,
8890
isFleetSetupCompleted: true,
91+
primaryColor: true,
8992
createdAt: true,
9093
},
9194
});
@@ -271,4 +274,60 @@ export class OrganizationService {
271274
throw error;
272275
}
273276
}
277+
async getPrimaryColor(organizationId: string, token?: string) {
278+
try {
279+
let targetOrgId = organizationId;
280+
281+
// If token is provided, resolve organization from the access grant
282+
if (token) {
283+
const grant = await db.trustAccessGrant.findUnique({
284+
where: { accessToken: token },
285+
select: {
286+
expiresAt: true,
287+
accessRequest: {
288+
select: {
289+
organizationId: true,
290+
},
291+
},
292+
},
293+
});
294+
295+
if (!grant) {
296+
throw new NotFoundException('Invalid or expired access token');
297+
}
298+
299+
if (grant.expiresAt && new Date() > grant.expiresAt) {
300+
throw new NotFoundException('Access token has expired');
301+
}
302+
303+
targetOrgId = grant.accessRequest.organizationId;
304+
}
305+
306+
const primaryColor = await db.organization.findUnique({
307+
where: { id: targetOrgId },
308+
select: { primaryColor: true },
309+
});
310+
311+
if (!primaryColor) {
312+
throw new NotFoundException(`Organization with ID ${targetOrgId} not found`);
313+
}
314+
315+
this.logger.log(
316+
`Retrieved organization primary color for organization ${targetOrgId}: ${primaryColor.primaryColor}`,
317+
);
318+
319+
return {
320+
primaryColor: primaryColor.primaryColor,
321+
};
322+
} catch (error) {
323+
if (error instanceof NotFoundException) {
324+
throw error;
325+
}
326+
this.logger.error(
327+
`Failed to retrieve organization primary color for organization ${organizationId}:`,
328+
error,
329+
);
330+
throw error;
331+
}
332+
}
274333
}

0 commit comments

Comments
 (0)