Skip to content

Commit 26b4263

Browse files
github-actions[bot]tofikwestMarfuen
authored
[dev] [tofikwest] tofik/customize-trust-center (#1906)
* feat(organization): add primary color field, update ui, set up trust portal settings * fix(questionnaire): adjust minimum width for answered questions display * fix(questionnaire): update minimum width class for questionnaire history * feat(questionnaire): add animations to results cards, header, and table in security questionnaire * chore(db): update version to 1.3.19 in package.json --------- Co-authored-by: Tofik Hasanov <[email protected]> Co-authored-by: Mariano Fuentes <[email protected]>
1 parent cbe83b5 commit 26b4263

File tree

30 files changed

+792
-136
lines changed

30 files changed

+792
-136
lines changed

.npmrc

Whitespace-only changes.

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
});
@@ -273,4 +276,60 @@ export class OrganizationService {
273276
throw error;
274277
}
275278
}
279+
async getPrimaryColor(organizationId: string, token?: string) {
280+
try {
281+
let targetOrgId = organizationId;
282+
283+
// If token is provided, resolve organization from the access grant
284+
if (token) {
285+
const grant = await db.trustAccessGrant.findUnique({
286+
where: { accessToken: token },
287+
select: {
288+
expiresAt: true,
289+
accessRequest: {
290+
select: {
291+
organizationId: true,
292+
},
293+
},
294+
},
295+
});
296+
297+
if (!grant) {
298+
throw new NotFoundException('Invalid or expired access token');
299+
}
300+
301+
if (grant.expiresAt && new Date() > grant.expiresAt) {
302+
throw new NotFoundException('Access token has expired');
303+
}
304+
305+
targetOrgId = grant.accessRequest.organizationId;
306+
}
307+
308+
const primaryColor = await db.organization.findUnique({
309+
where: { id: targetOrgId },
310+
select: { primaryColor: true },
311+
});
312+
313+
if (!primaryColor) {
314+
throw new NotFoundException(`Organization with ID ${targetOrgId} not found`);
315+
}
316+
317+
this.logger.log(
318+
`Retrieved organization primary color for organization ${targetOrgId}: ${primaryColor.primaryColor}`,
319+
);
320+
321+
return {
322+
primaryColor: primaryColor.primaryColor,
323+
};
324+
} catch (error) {
325+
if (error instanceof NotFoundException) {
326+
throw error;
327+
}
328+
this.logger.error(
329+
`Failed to retrieve organization primary color for organization ${organizationId}:`,
330+
error,
331+
);
332+
throw error;
333+
}
334+
}
276335
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ApiResponseOptions } from '@nestjs/swagger';
2+
3+
export const GET_ORGANIZATION_PRIMARY_COLOR_RESPONSES: Record<
4+
string,
5+
ApiResponseOptions
6+
> = {
7+
200: {
8+
status: 200,
9+
description: 'Organization primary color retrieved successfully',
10+
content: {
11+
'application/json': {
12+
schema: {
13+
type: 'object',
14+
properties: {
15+
primaryColor: {
16+
type: 'string',
17+
nullable: true,
18+
description: 'The primary color in hex format (e.g., #FF5733)',
19+
example: '#3B82F6',
20+
},
21+
authType: {
22+
type: 'string',
23+
enum: ['api-key', 'session'],
24+
description: 'How the request was authenticated',
25+
},
26+
},
27+
},
28+
},
29+
},
30+
},
31+
401: {
32+
status: 401,
33+
description:
34+
'Unauthorized - Invalid authentication or insufficient permissions',
35+
content: {
36+
'application/json': {
37+
schema: {
38+
type: 'object',
39+
properties: {
40+
message: {
41+
type: 'string',
42+
example: 'Invalid or expired API key',
43+
},
44+
},
45+
},
46+
},
47+
},
48+
},
49+
404: {
50+
status: 404,
51+
description: 'Organization not found',
52+
content: {
53+
'application/json': {
54+
schema: {
55+
type: 'object',
56+
properties: {
57+
message: {
58+
type: 'string',
59+
example: 'Organization with ID org_abc123def456 not found',
60+
},
61+
},
62+
},
63+
},
64+
},
65+
},
66+
};
67+

apps/api/src/organization/schemas/get-organization.responses.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ export const GET_ORGANIZATION_RESPONSES: Record<string, ApiResponseOptions> = {
6363
description: 'Whether FleetDM setup is completed',
6464
example: false,
6565
},
66+
primaryColor: {
67+
type: 'string',
68+
nullable: true,
69+
description: 'Organization primary color in hex format',
70+
example: '#3B82F6',
71+
},
6672
createdAt: {
6773
type: 'string',
6874
format: 'date-time',

apps/api/src/organization/schemas/organization-api-bodies.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export const UPDATE_ORGANIZATION_BODY: ApiBodyOptions = {
5050
description: 'Whether FleetDM setup is completed',
5151
example: false,
5252
},
53+
primaryColor: {
54+
type: 'string',
55+
description: 'Organization primary color in hex format',
56+
example: '#3B82F6',
57+
},
5358
},
5459
additionalProperties: false,
5560
},

apps/api/src/organization/schemas/organization-operations.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ export const ORGANIZATION_OPERATIONS: Record<string, ApiOperationOptions> = {
2121
description:
2222
'Transfers organization ownership to another member. The current owner will become an admin and keep all other roles. The new owner will receive the owner role while keeping their existing roles. Only the current organization owner can perform this action. Supports both API key authentication (X-API-Key header) and session authentication (cookies + X-Organization-Id header).',
2323
},
24+
getPrimaryColor: {
25+
summary: 'Get organization primary color',
26+
description:
27+
'Returns the primary color of the organization. Supports three access methods: 1) API key authentication (X-API-Key header), 2) Session authentication (cookies + X-Organization-Id header), or 3) Public access using an access token query parameter (?token=tok_xxx). When using an access token, no authentication is required.',
28+
},
2429
};

apps/api/src/organization/schemas/update-organization.responses.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ export const UPDATE_ORGANIZATION_RESPONSES: Record<string, ApiResponseOptions> =
6464
description: 'Whether FleetDM setup is completed',
6565
example: false,
6666
},
67+
primaryColor: {
68+
type: 'string',
69+
nullable: true,
70+
description: 'Organization primary color in hex format',
71+
example: '#3B82F6',
72+
},
6773
createdAt: {
6874
type: 'string',
6975
format: 'date-time',

apps/api/src/questionnaire/dto/export-questionnaire.dto.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IsIn, IsOptional, IsString } from 'class-validator';
1+
import { IsBoolean, IsIn, IsOptional, IsString } from 'class-validator';
22
import { ParseQuestionnaireDto } from './parse-questionnaire.dto';
33

44
export type QuestionnaireExportFormat = 'pdf' | 'csv' | 'xlsx';
@@ -13,4 +13,8 @@ export class ExportQuestionnaireDto extends ParseQuestionnaireDto {
1313
@IsOptional()
1414
@IsIn(['internal', 'external'])
1515
source?: 'internal' | 'external';
16+
17+
@IsOptional()
18+
@IsBoolean()
19+
exportInAllExtensions?: boolean;
1620
}

0 commit comments

Comments
 (0)