Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions apps/api/src/finding-template/finding-template.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ export class FindingTemplateService {
throw new NotFoundException(`Finding template with ID ${id} not found`);
}

this.logger.log(
`Retrieved finding template: ${template.title} (${id})`,
);
this.logger.log(`Retrieved finding template: ${template.title} (${id})`);
return template;
} catch (error) {
if (error instanceof NotFoundException) {
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/findings/dto/update-finding.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export class UpdateFindingDto {
content?: string;

@ApiProperty({
description: 'Auditor note when requesting revision (only for needs_revision status)',
description:
'Auditor note when requesting revision (only for needs_revision status)',
example: 'Please provide clearer screenshots showing the timestamp.',
maxLength: 2000,
required: false,
Expand Down
96 changes: 78 additions & 18 deletions apps/api/src/findings/finding-notifier.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ const NOVU_CONTENT_MAX_LENGTH = 100;
// Types
// ============================================================================

type FindingAction = 'created' | 'ready_for_review' | 'needs_revision' | 'closed';
type FindingAction =
| 'created'
| 'ready_for_review'
| 'needs_revision'
| 'closed';

interface Recipient {
userId: string;
Expand Down Expand Up @@ -99,9 +103,20 @@ export class FindingNotifierService {
* Recipients: Task assignee + Organization admins/owners
*/
async notifyFindingCreated(params: NotificationParams): Promise<void> {
const { organizationId, taskId, taskTitle, findingType, actorUserId, actorName } = params;
const {
organizationId,
taskId,
taskTitle,
findingType,
actorUserId,
actorName,
} = params;

const recipients = await this.getTaskAssigneeAndAdmins(organizationId, taskId, actorUserId);
const recipients = await this.getTaskAssigneeAndAdmins(
organizationId,
taskId,
actorUserId,
);

if (recipients.length === 0) {
this.logger.log('No recipients for finding created notification');
Expand All @@ -125,13 +140,22 @@ export class FindingNotifierService {
async notifyReadyForReview(
params: NotificationParams & { findingCreatorMemberId: string },
): Promise<void> {
const { findingId, taskTitle, actorUserId, actorName, findingCreatorMemberId } = params;
const {
findingId,
taskTitle,
actorUserId,
actorName,
findingCreatorMemberId,
} = params;

this.logger.log(
`[notifyReadyForReview] Finding ${findingId}: Looking for creator (memberId: ${findingCreatorMemberId}), excluding actor (userId: ${actorUserId})`,
);

const recipients = await this.getFindingCreator(findingCreatorMemberId, actorUserId);
const recipients = await this.getFindingCreator(
findingCreatorMemberId,
actorUserId,
);

if (recipients.length === 0) {
this.logger.warn(
Expand Down Expand Up @@ -160,9 +184,14 @@ export class FindingNotifierService {
* Recipients: Task assignee + Organization admins/owners
*/
async notifyNeedsRevision(params: NotificationParams): Promise<void> {
const { organizationId, taskId, taskTitle, actorUserId, actorName } = params;
const { organizationId, taskId, taskTitle, actorUserId, actorName } =
params;

const recipients = await this.getTaskAssigneeAndAdmins(organizationId, taskId, actorUserId);
const recipients = await this.getTaskAssigneeAndAdmins(
organizationId,
taskId,
actorUserId,
);

if (recipients.length === 0) {
this.logger.log('No recipients for needs revision notification');
Expand All @@ -185,9 +214,14 @@ export class FindingNotifierService {
* Recipients: Task assignee + Organization admins/owners
*/
async notifyFindingClosed(params: NotificationParams): Promise<void> {
const { organizationId, taskId, taskTitle, actorUserId, actorName } = params;
const { organizationId, taskId, taskTitle, actorUserId, actorName } =
params;

const recipients = await this.getTaskAssigneeAndAdmins(organizationId, taskId, actorUserId);
const recipients = await this.getTaskAssigneeAndAdmins(
organizationId,
taskId,
actorUserId,
);

if (recipients.length === 0) {
this.logger.log('No recipients for finding closed notification');
Expand All @@ -213,7 +247,9 @@ export class FindingNotifierService {
* Send notifications to all recipients via email and in-app (Novu).
* Failures are logged but don't throw - fire-and-forget pattern.
*/
private async sendNotifications(params: SendNotificationParams): Promise<void> {
private async sendNotifications(
params: SendNotificationParams,
): Promise<void> {
const {
organizationId,
findingId,
Expand Down Expand Up @@ -301,10 +337,16 @@ export class FindingNotifierService {

try {
// Check unsubscribe preferences
const isUnsubscribed = await isUserUnsubscribed(db, recipient.email, 'findingNotifications');
const isUnsubscribed = await isUserUnsubscribed(
db,
recipient.email,
'findingNotifications',
);

if (isUnsubscribed) {
this.logger.log(`Skipping notification: ${recipient.email} is unsubscribed`);
this.logger.log(
`Skipping notification: ${recipient.email} is unsubscribed`,
);
return;
}

Expand All @@ -320,7 +362,10 @@ export class FindingNotifierService {
taskTitle,
organizationName,
findingType,
findingContent: truncateContent(findingContent, EMAIL_CONTENT_MAX_LENGTH),
findingContent: truncateContent(
findingContent,
EMAIL_CONTENT_MAX_LENGTH,
),
newStatus,
findingUrl,
}),
Expand All @@ -332,7 +377,10 @@ export class FindingNotifierService {
taskId,
taskTitle,
findingType,
findingContent: truncateContent(findingContent, NOVU_CONTENT_MAX_LENGTH),
findingContent: truncateContent(
findingContent,
NOVU_CONTENT_MAX_LENGTH,
),
action,
heading,
message,
Expand Down Expand Up @@ -508,15 +556,20 @@ export class FindingNotifierService {

// Filter for admins/owners (roles can be comma-separated, e.g., "admin,auditor")
const adminMembers = allMembers.filter(
(member) => member.role.includes('admin') || member.role.includes('owner'),
(member) =>
member.role.includes('admin') || member.role.includes('owner'),
);

const recipients: Recipient[] = [];
const addedUserIds = new Set<string>();

// Add task assignee
const assigneeUser = task?.assignee?.user;
if (assigneeUser && assigneeUser.id !== excludeUserId && assigneeUser.email) {
if (
assigneeUser &&
assigneeUser.id !== excludeUserId &&
assigneeUser.email
) {
recipients.push({
userId: assigneeUser.id,
email: assigneeUser.email,
Expand All @@ -528,7 +581,11 @@ export class FindingNotifierService {
// Add org admins/owners (deduplicated)
for (const member of adminMembers) {
const user = member.user;
if (user.id !== excludeUserId && user.email && !addedUserIds.has(user.id)) {
if (
user.id !== excludeUserId &&
user.email &&
!addedUserIds.has(user.id)
) {
recipients.push({
userId: user.id,
email: user.email,
Expand All @@ -552,7 +609,10 @@ export class FindingNotifierService {
* Get the finding creator as recipient (for Ready for Review notifications).
* Excludes the actor (person who triggered the action).
*/
private async getFindingCreator(creatorMemberId: string, excludeUserId: string): Promise<Recipient[]> {
private async getFindingCreator(
creatorMemberId: string,
excludeUserId: string,
): Promise<Recipient[]> {
try {
const member = await db.member.findUnique({
where: { id: creatorMemberId },
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/findings/findings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ export class FindingsService {
) {
// Verify finding exists and get current state for audit
const finding = await this.findById(organizationId, findingId);
const previousStatus = finding.status as FindingStatus;
const previousType = finding.type as FindingType;
const previousStatus = finding.status;
const previousType = finding.type;
const previousContent = finding.content;

// Validate status transition permissions
Expand Down Expand Up @@ -421,7 +421,7 @@ export class FindingsService {
taskId: finding.taskId,
taskTitle: finding.task.title,
findingContent: updatedFinding.content,
findingType: updatedFinding.type as FindingType,
findingType: updatedFinding.type,
actorUserId: userId,
actorName,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ export class VariablesController {
await this.credentialVaultService.getDecryptedCredentials(connectionId);

const accessToken =
typeof credentials?.access_token === 'string' ? credentials.access_token : undefined;
typeof credentials?.access_token === 'string'
? credentials.access_token
: undefined;
if (!accessToken) {
throw new HttpException(
'No valid credentials found',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export class ConnectionAuthTeardownService {
const credentials =
await this.credentialVaultService.getDecryptedCredentials(connectionId);
const accessToken =
typeof credentials?.access_token === 'string' ? credentials.access_token : undefined;
typeof credentials?.access_token === 'string'
? credentials.access_token
: undefined;

if (providerSlug && accessToken) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ export class CredentialVaultService {

if (Array.isArray(value)) {
const encryptedItems = await Promise.all(
value.map((item) => (typeof item === 'string' ? this.encrypt(item) : item)),
value.map((item) =>
typeof item === 'string' ? this.encrypt(item) : item,
),
);
encryptedPayload[key] = encryptedItems;
continue;
Expand Down Expand Up @@ -289,7 +291,9 @@ export class CredentialVaultService {
*/
async getRefreshToken(connectionId: string): Promise<string | null> {
const credentials = await this.getDecryptedCredentials(connectionId);
return typeof credentials?.refresh_token === 'string' ? credentials.refresh_token : null;
return typeof credentials?.refresh_token === 'string'
? credentials.refresh_token
: null;
}

/**
Expand Down Expand Up @@ -414,6 +418,8 @@ export class CredentialVaultService {

// Get current credentials
const credentials = await this.getDecryptedCredentials(connectionId);
return typeof credentials?.access_token === 'string' ? credentials.access_token : null;
return typeof credentials?.access_token === 'string'
? credentials.access_token
: null;
}
}
3 changes: 2 additions & 1 deletion apps/api/src/policies/dto/version.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export class CreateVersionDto {

export class UpdateVersionContentDto {
@ApiProperty({
description: 'Content of the policy version as TipTap JSON (array of nodes)',
description:
'Content of the policy version as TipTap JSON (array of nodes)',
example: [
{
type: 'heading',
Expand Down
20 changes: 13 additions & 7 deletions apps/api/src/policies/policies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,14 @@ export class PoliciesService {
if (pdfUrlsToDelete.length > 0) {
await Promise.allSettled(
pdfUrlsToDelete.map((pdfUrl) =>
this.attachmentsService.deletePolicyVersionPdf(pdfUrl).catch((err) => {
this.logger.warn(`Failed to delete PDF from S3: ${pdfUrl}`, err);
}),
this.attachmentsService
.deletePolicyVersionPdf(pdfUrl)
.catch((err) => {
this.logger.warn(
`Failed to delete PDF from S3: ${pdfUrl}`,
err,
);
}),
),
);
}
Expand Down Expand Up @@ -799,7 +804,9 @@ export class PoliciesService {

// Cannot assign a deactivated member as approver - they can't log in to approve
if (approver.deactivated) {
throw new BadRequestException('Cannot assign a deactivated member as approver');
throw new BadRequestException(
'Cannot assign a deactivated member as approver',
);
}

await db.policy.update({
Expand Down Expand Up @@ -951,9 +958,8 @@ export class PoliciesService {

if (hasUploadedPdf) {
try {
const pdfBuffer = await this.attachmentsService.getObjectBuffer(
pdfUrl!,
);
const pdfBuffer =
await this.attachmentsService.getObjectBuffer(pdfUrl);
return {
policy,
pdfBuffer: Buffer.from(pdfBuffer),
Expand Down
Loading