From 457f85217899f781b3573b1e21851c43e727b695 Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Tue, 4 Nov 2025 09:38:51 -0500 Subject: [PATCH 01/11] feat(db): add deactivated column to member table --- .../20251104143456_add_deactivated_to_member/migration.sql | 2 ++ packages/db/prisma/schema/auth.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 packages/db/prisma/migrations/20251104143456_add_deactivated_to_member/migration.sql diff --git a/packages/db/prisma/migrations/20251104143456_add_deactivated_to_member/migration.sql b/packages/db/prisma/migrations/20251104143456_add_deactivated_to_member/migration.sql new file mode 100644 index 000000000..7ff63c6e6 --- /dev/null +++ b/packages/db/prisma/migrations/20251104143456_add_deactivated_to_member/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Member" ADD COLUMN "deactivated" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/db/prisma/schema/auth.prisma b/packages/db/prisma/schema/auth.prisma index 298e379bc..0ca4bceed 100644 --- a/packages/db/prisma/schema/auth.prisma +++ b/packages/db/prisma/schema/auth.prisma @@ -93,6 +93,7 @@ model Member { department Departments @default(none) isActive Boolean @default(true) + deactivated Boolean @default(false) employeeTrainingVideoCompletion EmployeeTrainingVideoCompletion[] fleetDmLabelId Int? From 271b73e67a53786ea4c0a1f63796d7b804545233 Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Tue, 4 Nov 2025 09:50:16 -0500 Subject: [PATCH 02/11] fix(db): publish new db version: 1.3.16 --- packages/db/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/package.json b/packages/db/package.json index 0c4fb1b5f..d83dba9c2 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,7 +1,7 @@ { "name": "@trycompai/db", "description": "Database package with Prisma client and schema for Comp AI", - "version": "1.3.15", + "version": "1.3.16", "dependencies": { "@prisma/client": "^6.13.0", "dotenv": "^16.4.5" From 9bbbd0753cdfc62f70aee9943874d24abfee616c Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Tue, 4 Nov 2025 10:53:18 -0500 Subject: [PATCH 03/11] fix(api): remove access from deactivated members --- apps/api/package.json | 2 +- apps/api/src/auth/hybrid-auth.guard.ts | 1 + apps/api/src/comments/comments.service.ts | 1 + apps/api/src/devices/devices.service.ts | 2 ++ apps/api/src/people/utils/member-queries.ts | 2 +- apps/api/src/people/utils/member-validator.ts | 2 ++ 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 75fead8ab..ec95245b5 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -13,7 +13,7 @@ "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.1.5", "@nestjs/swagger": "^11.2.0", - "@trycompai/db": "^1.3.7", + "@trycompai/db": "^1.3.16", "archiver": "^7.0.1", "axios": "^1.12.2", "better-auth": "^1.3.27", diff --git a/apps/api/src/auth/hybrid-auth.guard.ts b/apps/api/src/auth/hybrid-auth.guard.ts index ea2bde011..56f86901a 100644 --- a/apps/api/src/auth/hybrid-auth.guard.ts +++ b/apps/api/src/auth/hybrid-auth.guard.ts @@ -128,6 +128,7 @@ export class HybridAuthGuard implements CanActivate { where: { userId, organizationId, + deactivated: false, }, select: { id: true, diff --git a/apps/api/src/comments/comments.service.ts b/apps/api/src/comments/comments.service.ts index 7d245dd8b..7b7d110a5 100644 --- a/apps/api/src/comments/comments.service.ts +++ b/apps/api/src/comments/comments.service.ts @@ -154,6 +154,7 @@ export class CommentsService { where: { userId, organizationId, + deactivated: false, }, include: { user: true, diff --git a/apps/api/src/devices/devices.service.ts b/apps/api/src/devices/devices.service.ts index 5519c24ad..ac4b61409 100644 --- a/apps/api/src/devices/devices.service.ts +++ b/apps/api/src/devices/devices.service.ts @@ -77,6 +77,7 @@ export class DevicesService { where: { id: memberId, organizationId: organizationId, + deactivated: false, }, select: { id: true, @@ -131,6 +132,7 @@ export class DevicesService { where: { id: memberId, organizationId: organizationId, + deactivated: false, }, select: { id: true, diff --git a/apps/api/src/people/utils/member-queries.ts b/apps/api/src/people/utils/member-queries.ts index 8cb9e6fb3..f7e7fee3d 100644 --- a/apps/api/src/people/utils/member-queries.ts +++ b/apps/api/src/people/utils/member-queries.ts @@ -38,7 +38,7 @@ export class MemberQueries { */ static async findAllByOrganization(organizationId: string): Promise { return db.member.findMany({ - where: { organizationId }, + where: { organizationId, deactivated: false }, select: this.MEMBER_SELECT, orderBy: { createdAt: 'desc' }, }); diff --git a/apps/api/src/people/utils/member-validator.ts b/apps/api/src/people/utils/member-validator.ts index bbb4a5402..696590432 100644 --- a/apps/api/src/people/utils/member-validator.ts +++ b/apps/api/src/people/utils/member-validator.ts @@ -40,6 +40,7 @@ export class MemberValidator { where: { id: memberId, organizationId, + deactivated: false, }, select: { id: true, userId: true }, }); @@ -62,6 +63,7 @@ export class MemberValidator { const whereClause: any = { userId, organizationId, + deactivated: false, }; if (excludeMemberId) { From 885ae1c7650477b967eef6b4204581eb1fd2c4b6 Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Tue, 4 Nov 2025 10:55:01 -0500 Subject: [PATCH 04/11] fix(app): remove access from deactivated members --- apps/app/package.json | 2 +- apps/app/src/actions/add-comment.ts | 1 + apps/app/src/actions/change-organization.ts | 1 + apps/app/src/actions/organization/accept-invitation.ts | 1 + .../src/actions/organization/get-organization-users-action.ts | 1 + .../app/src/actions/policies/accept-requested-policy-changes.ts | 2 ++ apps/app/src/actions/policies/create-new-policy.ts | 1 + apps/app/src/actions/policies/deny-requested-policy-changes.ts | 1 + apps/app/src/actions/policies/publish-all.ts | 2 ++ apps/app/src/actions/safe-action.ts | 1 + apps/app/src/app/(app)/[orgId]/frameworks/page.tsx | 1 + apps/app/src/app/(app)/[orgId]/layout.tsx | 1 + .../[orgId]/people/all/actions/addEmployeeWithoutInvite.ts | 1 + .../app/(app)/[orgId]/people/all/actions/revokeInvitation.ts | 1 + .../src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx | 1 + .../[orgId]/people/dashboard/components/EmployeesOverview.tsx | 1 + apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts | 1 + apps/app/src/app/(app)/[orgId]/people/layout.tsx | 1 + .../app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts | 1 + apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx | 1 + apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx | 1 + apps/app/src/app/(app)/[orgId]/tasks/page.tsx | 1 + .../(app)/[orgId]/vendors/(overview)/actions/deleteVendor.ts | 1 + .../src/app/(app)/[orgId]/vendors/(overview)/data/queries.ts | 1 + apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/page.tsx | 1 + .../(app)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx | 1 + .../src/app/(app)/[orgId]/vendors/backup-overview/layout.tsx | 1 + .../app/src/app/(app)/onboarding/actions/complete-onboarding.ts | 1 + apps/app/src/app/(app)/upgrade/[orgId]/page.tsx | 1 + apps/app/src/app/api/secrets/[id]/route.ts | 2 ++ apps/app/src/app/api/secrets/route.ts | 1 + apps/app/src/app/page.tsx | 1 + .../jobs/tasks/onboarding/backfill-training-videos-for-org.ts | 1 + .../src/jobs/tasks/onboarding/onboard-organization-helpers.ts | 1 + apps/app/src/jobs/tasks/onboarding/onboard-organization.ts | 1 + 35 files changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/app/package.json b/apps/app/package.json index 5d795da36..ef25b36f4 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -51,7 +51,7 @@ "@tiptap/extension-table-row": "^3.4.4", "@trigger.dev/react-hooks": "4.0.0", "@trigger.dev/sdk": "4.0.0", - "@trycompai/db": "^1.3.7", + "@trycompai/db": "^1.3.16", "@trycompai/email": "workspace:*", "@types/canvas-confetti": "^1.9.0", "@types/react-syntax-highlighter": "^15.5.13", diff --git a/apps/app/src/actions/add-comment.ts b/apps/app/src/actions/add-comment.ts index 40a92dbf7..0ef626f17 100644 --- a/apps/app/src/actions/add-comment.ts +++ b/apps/app/src/actions/add-comment.ts @@ -38,6 +38,7 @@ export const addCommentAction = authActionClient where: { userId: session.userId, organizationId: session.activeOrganizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/change-organization.ts b/apps/app/src/actions/change-organization.ts index 32e768bb0..9dc48ea53 100644 --- a/apps/app/src/actions/change-organization.ts +++ b/apps/app/src/actions/change-organization.ts @@ -28,6 +28,7 @@ export const changeOrganizationAction = authActionClient where: { userId: user.id, organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/organization/accept-invitation.ts b/apps/app/src/actions/organization/accept-invitation.ts index 78e0bf7ac..a507fbf6d 100644 --- a/apps/app/src/actions/organization/accept-invitation.ts +++ b/apps/app/src/actions/organization/accept-invitation.ts @@ -69,6 +69,7 @@ export const completeInvitation = authActionClientWithoutOrg where: { userId: user.id, organizationId: invitation.organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/organization/get-organization-users-action.ts b/apps/app/src/actions/organization/get-organization-users-action.ts index dcd33e6a8..aaa9d2e49 100644 --- a/apps/app/src/actions/organization/get-organization-users-action.ts +++ b/apps/app/src/actions/organization/get-organization-users-action.ts @@ -26,6 +26,7 @@ export const getOrganizationUsersAction = authActionClient const users = await db.member.findMany({ where: { organizationId: ctx.session.activeOrganizationId, + deactivated: false, }, select: { user: { diff --git a/apps/app/src/actions/policies/accept-requested-policy-changes.ts b/apps/app/src/actions/policies/accept-requested-policy-changes.ts index c1ca22993..3b8b7a283 100644 --- a/apps/app/src/actions/policies/accept-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/accept-requested-policy-changes.ts @@ -82,6 +82,7 @@ export const acceptRequestedPolicyChangesAction = authActionClient where: { organizationId: session.activeOrganizationId, isActive: true, + deactivated: false, }, include: { user: true, @@ -131,6 +132,7 @@ export const acceptRequestedPolicyChangesAction = authActionClient where: { userId: user.id, organizationId: session.activeOrganizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/policies/create-new-policy.ts b/apps/app/src/actions/policies/create-new-policy.ts index 9f7e75a82..146528fb6 100644 --- a/apps/app/src/actions/policies/create-new-policy.ts +++ b/apps/app/src/actions/policies/create-new-policy.ts @@ -39,6 +39,7 @@ export const createPolicyAction = authActionClient where: { userId: user.id, organizationId: activeOrganizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/policies/deny-requested-policy-changes.ts b/apps/app/src/actions/policies/deny-requested-policy-changes.ts index 5c937edbe..a61dcec38 100644 --- a/apps/app/src/actions/policies/deny-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/deny-requested-policy-changes.ts @@ -68,6 +68,7 @@ export const denyRequestedPolicyChangesAction = authActionClient where: { userId: user.id, organizationId: session.activeOrganizationId, + deactivated: false, }, }); diff --git a/apps/app/src/actions/policies/publish-all.ts b/apps/app/src/actions/policies/publish-all.ts index 4aa24a9e5..eca0356eb 100644 --- a/apps/app/src/actions/policies/publish-all.ts +++ b/apps/app/src/actions/policies/publish-all.ts @@ -41,6 +41,7 @@ export const publishAllPoliciesAction = authActionClient where: { userId: user.id, organizationId: parsedInput.organizationId, + deactivated: false, }, }); @@ -104,6 +105,7 @@ export const publishAllPoliciesAction = authActionClient where: { organizationId: parsedInput.organizationId, isActive: true, + deactivated: false, role: { contains: Role.employee, }, diff --git a/apps/app/src/actions/safe-action.ts b/apps/app/src/actions/safe-action.ts index 3814b4f65..d7e3cbd70 100644 --- a/apps/app/src/actions/safe-action.ts +++ b/apps/app/src/actions/safe-action.ts @@ -237,6 +237,7 @@ export const authWithOrgAccessClient = authActionClient.use(async ({ next, clien where: { userId: ctx.user.id, organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx index d26062890..7c87b44eb 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/page.tsx @@ -39,6 +39,7 @@ export default async function DashboardPage({ params }: { params: Promise<{ orgI where: { userId: session.user.id, organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/layout.tsx b/apps/app/src/app/(app)/[orgId]/layout.tsx index b988987d9..2ce4512fb 100644 --- a/apps/app/src/app/(app)/[orgId]/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/layout.tsx @@ -55,6 +55,7 @@ export default async function Layout({ where: { userId: session.user.id, organizationId: requestedOrgId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts index 5ee3eae37..5264ae45d 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts @@ -25,6 +25,7 @@ export const addEmployeeWithoutInvite = async ({ where: { organizationId: organizationId, userId: currentUserId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts index 8558f1805..d826135a4 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/revokeInvitation.ts @@ -37,6 +37,7 @@ export const revokeInvitation = authActionClient where: { organizationId: ctx.session.activeOrganizationId, userId: ctx.user.id, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx index fdeff7173..ed20bda8b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx @@ -45,6 +45,7 @@ export async function TeamMembers() { const fetchedMembers = await db.member.findMany({ where: { organizationId: organizationId, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx index 95f93acda..ee58e46bf 100644 --- a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeesOverview.tsx @@ -41,6 +41,7 @@ export async function EmployeesOverview() { const fetchedMembers = await db.member.findMany({ where: { organizationId: organizationId, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts b/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts index 3ec1d54f5..965348591 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts +++ b/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts @@ -23,6 +23,7 @@ export const getEmployeeDevices: () => Promise = async () => { const employees = await db.member.findMany({ where: { organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/people/layout.tsx b/apps/app/src/app/(app)/[orgId]/people/layout.tsx index 508fe95f3..3ebecd953 100644 --- a/apps/app/src/app/(app)/[orgId]/people/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/layout.tsx @@ -18,6 +18,7 @@ export default async function Layout({ children }: { children: React.ReactNode } const allMembers = await db.member.findMany({ where: { organizationId: orgId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts index ebbf58b83..cfbb2dfdb 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts @@ -137,6 +137,7 @@ export const getAssignees = async () => { role: { notIn: ['employee'], }, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx b/apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx index 87115bbce..c33c22f1b 100644 --- a/apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/risk/(overview)/page.tsx @@ -107,6 +107,7 @@ const getAssignees = cache(async () => { where: { organizationId: session.session.activeOrganizationId, isActive: true, + deactivated: false, role: { notIn: ['employee'], }, diff --git a/apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx b/apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx index 1c860f8a0..7a8903628 100644 --- a/apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx @@ -91,6 +91,7 @@ const getAssignees = cache(async () => { role: { notIn: ['employee'], }, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/tasks/page.tsx b/apps/app/src/app/(app)/[orgId]/tasks/page.tsx index cfb7cb04b..cc981d7b0 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/page.tsx @@ -85,6 +85,7 @@ const getMembersWithMetadata = async () => { role: { notIn: [Role.employee, Role.auditor], }, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/vendors/(overview)/actions/deleteVendor.ts b/apps/app/src/app/(app)/[orgId]/vendors/(overview)/actions/deleteVendor.ts index 51ef230c3..3ce5bb4c2 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/(overview)/actions/deleteVendor.ts +++ b/apps/app/src/app/(app)/[orgId]/vendors/(overview)/actions/deleteVendor.ts @@ -34,6 +34,7 @@ export const deleteVendor = authActionClient where: { organizationId: ctx.session.activeOrganizationId, userId: ctx.user.id, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/[orgId]/vendors/(overview)/data/queries.ts b/apps/app/src/app/(app)/[orgId]/vendors/(overview)/data/queries.ts index 1579a8172..c70b4507d 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/(overview)/data/queries.ts +++ b/apps/app/src/app/(app)/[orgId]/vendors/(overview)/data/queries.ts @@ -59,6 +59,7 @@ export const getAssignees = cache(async (orgId: string): Promise { role: { notIn: ['employee'], }, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx index 2276efa3a..d3afa5a5a 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx @@ -50,6 +50,7 @@ export default async function TaskPage({ params }: PageProps) { role: { notIn: ['employee'], }, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/layout.tsx b/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/layout.tsx index 056f41476..202fbf2ed 100644 --- a/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/vendors/backup-overview/layout.tsx @@ -96,6 +96,7 @@ const getAssignees = cache(async () => { role: { notIn: ['employee'], }, + deactivated: false, }, include: { user: true, diff --git a/apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts b/apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts index bbe85dc1f..42dd4b20d 100644 --- a/apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts +++ b/apps/app/src/app/(app)/onboarding/actions/complete-onboarding.ts @@ -56,6 +56,7 @@ export const completeOnboarding = authActionClient where: { userId: ctx.user.id, organizationId: parsedInput.organizationId, + deactivated: false, }, }); diff --git a/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx b/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx index 2abf813fe..db8841713 100644 --- a/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx +++ b/apps/app/src/app/(app)/upgrade/[orgId]/page.tsx @@ -28,6 +28,7 @@ export default async function UpgradePage({ params }: PageProps) { where: { organizationId: orgId, userId: authSession.user.id, + deactivated: false, }, include: { organization: true, diff --git a/apps/app/src/app/api/secrets/[id]/route.ts b/apps/app/src/app/api/secrets/[id]/route.ts index 9871f6620..6f98f3b0e 100644 --- a/apps/app/src/app/api/secrets/[id]/route.ts +++ b/apps/app/src/app/api/secrets/[id]/route.ts @@ -79,6 +79,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ where: { organizationId: validatedData.organizationId, userId: session.user.id, + deactivated: false, }, }); @@ -175,6 +176,7 @@ export async function DELETE( where: { organizationId, userId: session.user.id, + deactivated: false, }, }); diff --git a/apps/app/src/app/api/secrets/route.ts b/apps/app/src/app/api/secrets/route.ts index 8cbcce324..ea1215a54 100644 --- a/apps/app/src/app/api/secrets/route.ts +++ b/apps/app/src/app/api/secrets/route.ts @@ -80,6 +80,7 @@ export async function POST(request: NextRequest) { where: { organizationId: validatedData.organizationId, userId: session.user.id, + deactivated: false, }, }); diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx index c83d58de7..57170a935 100644 --- a/apps/app/src/app/page.tsx +++ b/apps/app/src/app/page.tsx @@ -69,6 +69,7 @@ export default async function RootPage({ where: { organizationId: orgId, userId: session.user.id, + deactivated: false, }, }); diff --git a/apps/app/src/jobs/tasks/onboarding/backfill-training-videos-for-org.ts b/apps/app/src/jobs/tasks/onboarding/backfill-training-videos-for-org.ts index 21828c0ea..2b805517c 100644 --- a/apps/app/src/jobs/tasks/onboarding/backfill-training-videos-for-org.ts +++ b/apps/app/src/jobs/tasks/onboarding/backfill-training-videos-for-org.ts @@ -15,6 +15,7 @@ export const backfillTrainingVideosForOrg = task({ const members = await db.member.findMany({ where: { organizationId: payload.organizationId, + deactivated: false, }, select: { id: true, diff --git a/apps/app/src/jobs/tasks/onboarding/onboard-organization-helpers.ts b/apps/app/src/jobs/tasks/onboarding/onboard-organization-helpers.ts index c0ae68613..7e17ab9ce 100644 --- a/apps/app/src/jobs/tasks/onboarding/onboard-organization-helpers.ts +++ b/apps/app/src/jobs/tasks/onboarding/onboard-organization-helpers.ts @@ -257,6 +257,7 @@ export async function findCommentAuthor(organizationId: string) { where: { organizationId, OR: [{ role: { contains: 'owner' } }, { role: { contains: 'admin' } }], + deactivated: false, }, orderBy: [ { role: 'desc' }, // Prefer owner over admin diff --git a/apps/app/src/jobs/tasks/onboarding/onboard-organization.ts b/apps/app/src/jobs/tasks/onboarding/onboard-organization.ts index c6a3e656d..84431cfc9 100644 --- a/apps/app/src/jobs/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/jobs/tasks/onboarding/onboard-organization.ts @@ -49,6 +49,7 @@ export const onboardOrganization = task({ role: { contains: 'owner', }, + deactivated: false, }, }); From f88c97279c3be26af12d41922e4889cc6b60cb4f Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Tue, 4 Nov 2025 10:55:55 -0500 Subject: [PATCH 05/11] fix(portal): remove access from deactivated members --- apps/portal/package.json | 2 +- apps/portal/src/app/(app)/(home)/[orgId]/page.tsx | 1 + .../src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx | 1 + apps/portal/src/app/(app)/(home)/actions/getPolicyPdfUrl.ts | 2 +- .../src/app/(app)/(home)/actions/markPolicyAsCompleted.ts | 1 + .../portal/src/app/(app)/(home)/actions/markVideoAsCompleted.ts | 1 + apps/portal/src/app/api/download-agent/utils.ts | 1 + 7 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/portal/package.json b/apps/portal/package.json index b14d95424..ebf9fa4bf 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -9,7 +9,7 @@ "@react-email/render": "^1.1.2", "@t3-oss/env-nextjs": "^0.13.8", "@trycompai/analytics": "workspace:*", - "@trycompai/db": "^1.3.7", + "@trycompai/db": "^1.3.16", "@trycompai/email": "workspace:*", "@trycompai/kv": "workspace:*", "@trycompai/ui": "workspace:*", diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx index 79b16971c..06e715624 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx @@ -28,6 +28,7 @@ export default async function OrganizationPage({ params }: { params: Promise<{ o where: { userId: session.user.id, organizationId: orgId, + deactivated: false, }, include: { user: true, diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx index 6bec73ba3..54ed5cee5 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx @@ -43,6 +43,7 @@ export default async function PolicyPage({ where: { userId: session.user.id, organizationId: orgId, + deactivated: false, }, }); diff --git a/apps/portal/src/app/(app)/(home)/actions/getPolicyPdfUrl.ts b/apps/portal/src/app/(app)/(home)/actions/getPolicyPdfUrl.ts index 092a45211..cd431dc53 100644 --- a/apps/portal/src/app/(app)/(home)/actions/getPolicyPdfUrl.ts +++ b/apps/portal/src/app/(app)/(home)/actions/getPolicyPdfUrl.ts @@ -35,7 +35,7 @@ export const getPolicyPdfUrl = authActionClient } const member = await db.member.findFirst({ - where: { userId: user.id, organizationId: policy.organizationId }, + where: { userId: user.id, organizationId: policy.organizationId, deactivated: false }, }); if (!member) { diff --git a/apps/portal/src/app/(app)/(home)/actions/markPolicyAsCompleted.ts b/apps/portal/src/app/(app)/(home)/actions/markPolicyAsCompleted.ts index 7439e294d..0c709cd6a 100644 --- a/apps/portal/src/app/(app)/(home)/actions/markPolicyAsCompleted.ts +++ b/apps/portal/src/app/(app)/(home)/actions/markPolicyAsCompleted.ts @@ -32,6 +32,7 @@ export const markPolicyAsCompleted = authActionClient const member = await db.member.findFirst({ where: { userId: user.id, + deactivated: false, }, }); diff --git a/apps/portal/src/app/(app)/(home)/actions/markVideoAsCompleted.ts b/apps/portal/src/app/(app)/(home)/actions/markVideoAsCompleted.ts index 87e23f5bd..fa28b61a6 100644 --- a/apps/portal/src/app/(app)/(home)/actions/markVideoAsCompleted.ts +++ b/apps/portal/src/app/(app)/(home)/actions/markVideoAsCompleted.ts @@ -39,6 +39,7 @@ export const markVideoAsCompleted = authActionClient where: { userId: user.id, organizationId: organizationId, + deactivated: false, }, }); diff --git a/apps/portal/src/app/api/download-agent/utils.ts b/apps/portal/src/app/api/download-agent/utils.ts index bc14f8c5a..21b179d6a 100644 --- a/apps/portal/src/app/api/download-agent/utils.ts +++ b/apps/portal/src/app/api/download-agent/utils.ts @@ -32,6 +32,7 @@ export async function validateMemberAndOrg(userId: string, orgId: string) { where: { userId, organizationId: orgId, + deactivated: false, }, }); From b05f4d36a4fa61ad869fff6580c1c4846b3e6a61 Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Tue, 4 Nov 2025 10:56:32 -0500 Subject: [PATCH 06/11] fix(app): make member deactivated when removing --- .../app/(app)/[orgId]/people/all/actions/removeMember.ts | 9 +++++++-- .../[orgId]/people/all/components/TeamMembersClient.tsx | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts index a734a9124..0e10c46e3 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts @@ -36,6 +36,7 @@ export const removeMember = authActionClient where: { organizationId: ctx.session.activeOrganizationId, userId: ctx.user.id, + deactivated: false, }, }); @@ -80,11 +81,15 @@ export const removeMember = authActionClient }; } - // Remove the member - await db.member.delete({ + // Mark the member as deactivated instead of deleting + await db.member.update({ where: { id: memberId, }, + data: { + deactivated: true, + isActive: false, + }, }); // Consider if deleting sessions is still desired here diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx index 358470b3a..545542d8f 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx @@ -140,7 +140,7 @@ export function TeamMembersClient({ const handleRemoveMember = async (memberId: string) => { const result = await removeMemberAction({ memberId }); - if (result?.data) { + if (result?.data?.success) { // Success case toast.success('has been removed from the organization'); router.refresh(); // Add client-side refresh as well From 1d05e9f5845e1b39f63326316c0400b744b71a68 Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Tue, 4 Nov 2025 11:59:45 -0500 Subject: [PATCH 07/11] fix(api): include deactivated value to comments API response --- apps/api/src/comments/comments.service.ts | 3 +++ apps/api/src/comments/dto/comment-responses.dto.ts | 7 +++++++ packages/docs/openapi.json | 9 ++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/api/src/comments/comments.service.ts b/apps/api/src/comments/comments.service.ts index 7b7d110a5..864ce6774 100644 --- a/apps/api/src/comments/comments.service.ts +++ b/apps/api/src/comments/comments.service.ts @@ -116,6 +116,7 @@ export class CommentsService { name: comment.author.user.name, email: comment.author.user.email, image: comment.author.user.image, + deactivated: comment.author.deactivated, }, attachments, createdAt: comment.createdAt, @@ -212,6 +213,7 @@ export class CommentsService { name: member.user.name, email: member.user.email, image: member.user.image, + deactivated: member.deactivated, }, attachments: result.attachments, createdAt: result.comment.createdAt, @@ -285,6 +287,7 @@ export class CommentsService { name: existingComment.author.user.name, email: existingComment.author.user.email, image: existingComment.author.user.image, + deactivated: existingComment.author.deactivated, }, attachments, createdAt: updatedComment.createdAt, diff --git a/apps/api/src/comments/dto/comment-responses.dto.ts b/apps/api/src/comments/dto/comment-responses.dto.ts index e98f366ce..dcf7ddcd0 100644 --- a/apps/api/src/comments/dto/comment-responses.dto.ts +++ b/apps/api/src/comments/dto/comment-responses.dto.ts @@ -89,6 +89,13 @@ export class AuthorResponseDto { nullable: true, }) image: string | null; + + @ApiProperty({ + description: 'Whether the user is deactivated', + example: false, + nullable: true, + }) + deactivated: boolean; } export class CommentResponseDto { diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index 45206dbef..603caf758 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -9342,13 +9342,20 @@ "description": "User profile image URL", "example": "https://example.com/avatar.jpg", "nullable": true + }, + "deactivated": { + "type": "boolean", + "description": "Whether the user is deactivated", + "example": false, + "nullable": true } }, "required": [ "id", "name", "email", - "image" + "image", + "deactivated" ] }, "AttachmentMetadataDto": { From 283a91a004a1c3b16a8713129be282fd65116ccf Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Tue, 4 Nov 2025 12:20:51 -0500 Subject: [PATCH 08/11] fix(app): show alert icon for deactivated users on RecentLogs and comments --- .../[policyId]/components/RecentAuditLogs.tsx | 33 ++++++++++-- .../[orgId]/policies/[policyId]/data/index.ts | 1 + .../src/components/comments/CommentItem.tsx | 50 +++++++++++++++---- apps/app/src/components/comments/Comments.tsx | 1 + apps/app/src/hooks/use-comments-api.ts | 2 + 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/RecentAuditLogs.tsx b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/RecentAuditLogs.tsx index f9af8cb7b..0a78bb0cd 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/RecentAuditLogs.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/components/RecentAuditLogs.tsx @@ -3,10 +3,17 @@ import { Badge } from '@comp/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@comp/ui/card'; import { cn } from '@comp/ui/cn'; import { ScrollArea } from '@comp/ui/scroll-area'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@comp/ui/tooltip'; import { AuditLog, AuditLogEntityType } from '@db'; import { format } from 'date-fns'; import { ActivityIcon, + AlertTriangle, CalendarIcon, ClockIcon, FileIcon, @@ -90,6 +97,7 @@ const getUserInfo = (log: AuditLogWithRelations) => { name: log.user.name, email: log.user.email, avatarUrl: log.user.image || undefined, + deactivated: log.member?.deactivated || false, }; } @@ -98,6 +106,7 @@ const getUserInfo = (log: AuditLogWithRelations) => { name: undefined, email: undefined, avatarUrl: undefined, + deactivated: false, }; }; @@ -110,10 +119,26 @@ const LogItem = ({ log }: { log: AuditLogWithRelations }) => {
- - - {getInitials(userInfo.name)} - +
+ + + {getInitials(userInfo.name)} + + {userInfo.deactivated && ( + + + +
+ +
+
+ +

This user is deactivated.

+
+
+
+ )} +
diff --git a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts index cfbb2dfdb..8e1c32c59 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/[policyId]/data/index.ts @@ -194,6 +194,7 @@ export const getComments = async (policyId: string): Promise ({ id: att.id, diff --git a/apps/app/src/components/comments/CommentItem.tsx b/apps/app/src/components/comments/CommentItem.tsx index cdadb4746..8b15b718b 100644 --- a/apps/app/src/components/comments/CommentItem.tsx +++ b/apps/app/src/components/comments/CommentItem.tsx @@ -19,7 +19,21 @@ import { DropdownMenuTrigger, } from '@comp/ui/dropdown-menu'; import { Textarea } from '@comp/ui/textarea'; -import { FileIcon, FileText, ImageIcon, MoreHorizontal, Pencil, Trash2 } from 'lucide-react'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@comp/ui/tooltip'; +import { + AlertTriangle, + FileIcon, + FileText, + ImageIcon, + MoreHorizontal, + Pencil, + Trash2, +} from 'lucide-react'; import type React from 'react'; import { useState } from 'react'; import { toast } from 'sonner'; @@ -158,15 +172,31 @@ export function CommentItem({ comment, refreshComments }: CommentItemProps) { return ( <>
- - - - {comment.author.name?.charAt(0).toUpperCase() ?? '?'} - - +
+ + + + {comment.author.name?.charAt(0).toUpperCase() ?? '?'} + + + {comment.author.deactivated && ( + + + +
+ +
+
+ +

This user is deactivated.

+
+
+
+ )} +
diff --git a/apps/app/src/components/comments/Comments.tsx b/apps/app/src/components/comments/Comments.tsx index fb040115b..ba49f19e4 100644 --- a/apps/app/src/components/comments/Comments.tsx +++ b/apps/app/src/components/comments/Comments.tsx @@ -14,6 +14,7 @@ export type CommentWithAuthor = { name: string; email: string; image: string | null; + deactivated: boolean; }; attachments: Array<{ id: string; diff --git a/apps/app/src/hooks/use-comments-api.ts b/apps/app/src/hooks/use-comments-api.ts index 715828287..dbab2e0dd 100644 --- a/apps/app/src/hooks/use-comments-api.ts +++ b/apps/app/src/hooks/use-comments-api.ts @@ -20,6 +20,7 @@ export interface Comment { name: string; email: string; image: string | null; + deactivated: boolean; }; attachments: Array<{ id: string; @@ -175,6 +176,7 @@ export function useOptimisticComments(entityId: string, entityType: CommentEntit name: 'You', // Will be replaced with real author data email: '', image: null, + deactivated: false, }, attachments: [], // Will be populated by real response createdAt: new Date().toISOString(), From a3374c010c80b8987032fc0f52f804c42922f59b Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Thu, 13 Nov 2025 14:22:43 -0500 Subject: [PATCH 09/11] fix(db): remove duplicated migration script for user deactivation --- .../20251104143456_add_deactivated_to_member/migration.sql | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 packages/db/prisma/migrations/20251104143456_add_deactivated_to_member/migration.sql diff --git a/packages/db/prisma/migrations/20251104143456_add_deactivated_to_member/migration.sql b/packages/db/prisma/migrations/20251104143456_add_deactivated_to_member/migration.sql deleted file mode 100644 index 7ff63c6e6..000000000 --- a/packages/db/prisma/migrations/20251104143456_add_deactivated_to_member/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Member" ADD COLUMN "deactivated" BOOLEAN NOT NULL DEFAULT false; From 892eeae2486454f89a468707091f794ed38535a9 Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Mon, 17 Nov 2025 21:52:35 -0500 Subject: [PATCH 10/11] fix(app): reinvite the deactivate employee --- .../all/actions/addEmployeeWithoutInvite.ts | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts index 5264ae45d..66d1da59d 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/addEmployeeWithoutInvite.ts @@ -58,16 +58,45 @@ export const addEmployeeWithoutInvite = async ({ userId = newUser.id; } - const member = await auth.api.addMember({ - body: { - userId: existingUser?.id ?? userId, + const finalUserId = existingUser?.id ?? userId; + + // Check if there's an existing member (including deactivated ones) for this user and organization + const existingMember = await db.member.findFirst({ + where: { + userId: finalUserId, organizationId, - role: roles, // Auth API expects role or role array }, }); - // Create training video completion entries for the new member - if (member?.id) { + let member; + if (existingMember) { + // If member exists but is deactivated, reactivate it and update roles + if (existingMember.deactivated) { + const roleString = roles.sort().join(','); + member = await db.member.update({ + where: { id: existingMember.id }, + data: { + deactivated: false, + role: roleString, + }, + }); + } else { + // Member already exists and is active, return existing member + member = existingMember; + } + } else { + // No existing member, create a new one + member = await auth.api.addMember({ + body: { + userId: finalUserId, + organizationId, + role: roles, // Auth API expects role or role array + }, + }); + } + + // Create training video completion entries for the new member (only if member was just created/reactivated) + if (member?.id && !existingMember) { await createTrainingVideoEntries(member.id); } From 24d7c635201078b57f5ca3390de34fc5b47cd4bd Mon Sep 17 00:00:00 2001 From: chasprowebdev Date: Wed, 19 Nov 2025 21:01:03 -0500 Subject: [PATCH 11/11] feat(app): send an email to owner when the user is an assignee when removing a member --- .../people/all/actions/removeMember.ts | 165 +++++++++++++++++ .../emails/unassigned-items-notification.tsx | 167 ++++++++++++++++++ packages/email/index.ts | 2 + .../lib/unassigned-items-notification.ts | 58 ++++++ 4 files changed, 392 insertions(+) create mode 100644 packages/email/emails/unassigned-items-notification.tsx create mode 100644 packages/email/lib/unassigned-items-notification.ts diff --git a/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts b/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts index 0e10c46e3..822eef671 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/actions/removeMember.ts @@ -6,6 +6,7 @@ import { z } from 'zod'; // Adjust safe-action import for colocalized structure import { authActionClient } from '@/actions/safe-action'; import type { ActionResponse } from '@/actions/types'; +import { sendUnassignedItemsNotificationEmail, type UnassignedItem } from '@comp/email'; const removeMemberSchema = z.object({ memberId: z.string(), @@ -56,6 +57,9 @@ export const removeMember = authActionClient id: memberId, organizationId: ctx.session.activeOrganizationId, }, + include: { + user: true, + }, }); if (!targetMember) { @@ -81,6 +85,139 @@ export const removeMember = authActionClient }; } + // Get organization name + const organization = await db.organization.findUnique({ + where: { + id: ctx.session.activeOrganizationId, + }, + select: { + name: true, + }, + }); + + // Check for assignments and collect unassigned items + const unassignedItems: UnassignedItem[] = []; + + // Check tasks + const assignedTasks = await db.task.findMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + select: { + id: true, + title: true, + }, + }); + + for (const task of assignedTasks) { + unassignedItems.push({ + type: 'task', + id: task.id, + name: task.title, + }); + } + + // Check policies + const assignedPolicies = await db.policy.findMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + select: { + id: true, + name: true, + }, + }); + + for (const policy of assignedPolicies) { + unassignedItems.push({ + type: 'policy', + id: policy.id, + name: policy.name, + }); + } + + // Check risks + const assignedRisks = await db.risk.findMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + select: { + id: true, + title: true, + }, + }); + + for (const risk of assignedRisks) { + unassignedItems.push({ + type: 'risk', + id: risk.id, + name: risk.title, + }); + } + + // Check vendors + const assignedVendors = await db.vendor.findMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + select: { + id: true, + name: true, + }, + }); + + for (const vendor of assignedVendors) { + unassignedItems.push({ + type: 'vendor', + id: vendor.id, + name: vendor.name, + }); + } + + // Clear all assignments + await Promise.all([ + db.task.updateMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + data: { + assigneeId: null, + }, + }), + db.policy.updateMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + data: { + assigneeId: null, + }, + }), + db.risk.updateMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + data: { + assigneeId: null, + }, + }), + db.vendor.updateMany({ + where: { + assigneeId: memberId, + organizationId: ctx.session.activeOrganizationId, + }, + data: { + assigneeId: null, + }, + }), + ]); + // Mark the member as deactivated instead of deleting await db.member.update({ where: { @@ -99,6 +236,34 @@ export const removeMember = authActionClient }, }); + // Notify admins if there are unassigned items + if (unassignedItems.length > 0 && organization) { + const owner = await db.member.findFirst({ + where: { + organizationId: ctx.session.activeOrganizationId, + role: { contains: 'owner' }, + deactivated: false, + }, + include: { + user: true, + }, + }); + + const removedMemberName = targetMember.user.name || targetMember.user.email || 'Member'; + + if (owner) { + // Send email to the org owner + sendUnassignedItemsNotificationEmail({ + email: owner.user.email, + userName: owner.user.name || owner.user.email || 'Owner', + organizationName: organization.name, + organizationId: ctx.session.activeOrganizationId, + removedMemberName, + unassignedItems, + }); + } + } + revalidatePath(`/${ctx.session.activeOrganizationId}/settings/users`); revalidateTag(`user_${ctx.user.id}`); diff --git a/packages/email/emails/unassigned-items-notification.tsx b/packages/email/emails/unassigned-items-notification.tsx new file mode 100644 index 000000000..a943b86e8 --- /dev/null +++ b/packages/email/emails/unassigned-items-notification.tsx @@ -0,0 +1,167 @@ +import { + Body, + Container, + Font, + Heading, + Html, + Link, + Section, + Preview, + Tailwind, + Text, +} from '@react-email/components'; +import { Footer } from '../components/footer'; +import { Logo } from '../components/logo'; + +interface UnassignedItem { + type: 'task' | 'policy' | 'risk' | 'vendor'; + id: string; + name: string; +} + +interface Props { + userName: string; + organizationName: string; + organizationId: string; + removedMemberName: string; + unassignedItems: UnassignedItem[]; +} + +export const UnassignedItemsNotificationEmail = ({ + userName, + organizationName, + organizationId, + removedMemberName, + unassignedItems, +}: Props) => { + const baseUrl = process.env.NEXT_PUBLIC_BETTER_AUTH_URL ?? 'https://app.trycomp.ai'; + const link = `${baseUrl}/${organizationId}`; + + + const getItemTypeLabel = (type: UnassignedItem['type']) => { + switch (type) { + case 'task': + return 'Task'; + case 'policy': + return 'Policy'; + case 'risk': + return 'Risk'; + case 'vendor': + return 'Vendor'; + } + }; + + const getItemUrl = (item: UnassignedItem) => { + switch (item.type) { + case 'task': + return `${baseUrl}/${organizationId}/tasks/${item.id}`; + case 'policy': + return `${baseUrl}/${organizationId}/policies/${item.id}`; + case 'risk': + return `${baseUrl}/${organizationId}/risk/${item.id}`; + case 'vendor': + return `${baseUrl}/${organizationId}/vendors/${item.id}`; + } + }; + + const groupedItems = unassignedItems.reduce( + (acc, item) => { + if (!acc[item.type]) { + acc[item.type] = []; + } + acc[item.type].push(item); + return acc; + }, + {} as Record, + ); + + return ( + + + + + + + + + Member removed - items require reassignment + + + + + + Member Removed - Items Require Reassignment + + + + Hi {userName}, + + + + {removedMemberName} has been removed from {organizationName}. + As a result, the following items that were previously assigned to them now require a new assignee: + + + {Object.entries(groupedItems).map(([type, items]) => ( +
+ + {getItemTypeLabel(type as UnassignedItem['type'])}s ({items.length}) + +
    + {items.map((item) => ( +
  • + + {item.name} + +
  • + ))} +
+
+ ))} + + + Please log in to assign these items to appropriate team members. + + +
+ + View Organization + +
+ +
+ +