11'use server' ;
22
33import { createAudit } from "@/ee/features/audit/audit" ;
4+ import { syncWithLighthouse } from "@/ee/features/lighthouse/servicePing" ;
45import InviteUserEmail from "@/emails/inviteUserEmail" ;
56import JoinRequestApprovedEmail from "@/emails/joinRequestApprovedEmail" ;
67import { addUserToOrganization , orgHasAvailability } from "@/lib/authUtils" ;
@@ -11,7 +12,7 @@ import { sew } from "@/middleware/sew";
1112import { withAuth } from "@/middleware/withAuth" ;
1213import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole" ;
1314import { render } from "@react-email/components" ;
14- import { OrgRole , Prisma } from "@sourcebot/db" ;
15+ import { OrgRole , Prisma , PrismaClient } from "@sourcebot/db" ;
1516import { createLogger , env , getSMTPConnectionURL } from "@sourcebot/shared" ;
1617import { StatusCodes } from "http-status-codes" ;
1718import { createTransport } from "nodemailer" ;
@@ -21,48 +22,11 @@ const logger = createLogger('user-management');
2122export const removeMemberFromOrg = async ( memberId : string ) : Promise < { success : boolean } | ServiceError > => sew ( ( ) =>
2223 withAuth ( async ( { org, role, prisma } ) =>
2324 withMinimumOrgRole ( role , OrgRole . OWNER , async ( ) => {
24- const guardError = await prisma . $transaction ( async ( tx ) => {
25- const targetMember = await tx . userToOrg . findUnique ( {
26- where : {
27- orgId_userId : {
28- orgId : org . id ,
29- userId : memberId ,
30- }
31- }
32- } ) ;
33-
34- if ( ! targetMember ) {
35- return notFound ( "Member not found in this organization" ) ;
36- }
37-
38- if ( targetMember . role === OrgRole . OWNER ) {
39- const ownerCount = await tx . userToOrg . count ( {
40- where : {
41- orgId : org . id ,
42- role : OrgRole . OWNER ,
43- } ,
44- } ) ;
45-
46- if ( ownerCount <= 1 ) {
47- return {
48- statusCode : StatusCodes . FORBIDDEN ,
49- errorCode : ErrorCode . LAST_OWNER_CANNOT_BE_REMOVED ,
50- message : "Cannot remove the last owner of the organization." ,
51- } satisfies ServiceError ;
52- }
53- }
54-
55- await tx . userToOrg . delete ( {
56- where : {
57- orgId_userId : {
58- orgId : org . id ,
59- userId : memberId ,
60- }
61- }
62- } ) ;
63-
64- return null ;
65- } , { isolationLevel : Prisma . TransactionIsolationLevel . Serializable } ) ;
25+ const guardError = await _removeUserFromOrg ( prisma , {
26+ orgId : org . id ,
27+ userId : memberId ,
28+ lastOwnerMessage : "Cannot remove the last owner of the organization." ,
29+ } ) ;
6630
6731 if ( guardError ) {
6832 return guardError ;
@@ -73,36 +37,12 @@ export const removeMemberFromOrg = async (memberId: string): Promise<{ success:
7337) ;
7438
7539export const leaveOrg = async ( ) : Promise < { success : boolean } | ServiceError > => sew ( ( ) =>
76- withAuth ( async ( { user, org, role, prisma } ) => {
77- const guardError = await prisma . $transaction ( async ( tx ) => {
78- if ( role === OrgRole . OWNER ) {
79- const ownerCount = await tx . userToOrg . count ( {
80- where : {
81- orgId : org . id ,
82- role : OrgRole . OWNER ,
83- } ,
84- } ) ;
85-
86- if ( ownerCount <= 1 ) {
87- return {
88- statusCode : StatusCodes . FORBIDDEN ,
89- errorCode : ErrorCode . LAST_OWNER_CANNOT_BE_REMOVED ,
90- message : "You are the last owner of this organization. Promote another member to owner before leaving." ,
91- } satisfies ServiceError ;
92- }
93- }
94-
95- await tx . userToOrg . delete ( {
96- where : {
97- orgId_userId : {
98- orgId : org . id ,
99- userId : user . id ,
100- }
101- }
102- } ) ;
103-
104- return null ;
105- } , { isolationLevel : Prisma . TransactionIsolationLevel . Serializable } ) ;
40+ withAuth ( async ( { user, org, prisma } ) => {
41+ const guardError = await _removeUserFromOrg ( prisma , {
42+ orgId : org . id ,
43+ userId : user . id ,
44+ lastOwnerMessage : "You are the last owner of this organization. Promote another member to owner before leaving." ,
45+ } ) ;
10646
10747 if ( guardError ) {
10848 return guardError ;
@@ -113,6 +53,64 @@ export const leaveOrg = async (): Promise<{ success: boolean } | ServiceError> =
11353 }
11454 } ) ) ;
11555
56+
57+ const _removeUserFromOrg = async (
58+ prisma : PrismaClient ,
59+ { orgId, userId, lastOwnerMessage } : { orgId : number ; userId : string ; lastOwnerMessage : string } ,
60+ ) : Promise < ServiceError | null > => {
61+ const result = await prisma . $transaction ( async ( tx ) => {
62+ const target = await tx . userToOrg . findUnique ( {
63+ where : {
64+ orgId_userId : {
65+ orgId,
66+ userId,
67+ }
68+ }
69+ } ) ;
70+
71+ if ( ! target ) {
72+ return notFound ( "Member not found in this organization" ) ;
73+ }
74+
75+ if ( target . role === OrgRole . OWNER ) {
76+ const ownerCount = await tx . userToOrg . count ( {
77+ where : {
78+ orgId,
79+ role : OrgRole . OWNER ,
80+ } ,
81+ } ) ;
82+
83+ if ( ownerCount <= 1 ) {
84+ return {
85+ statusCode : StatusCodes . FORBIDDEN ,
86+ errorCode : ErrorCode . LAST_OWNER_CANNOT_BE_REMOVED ,
87+ message : lastOwnerMessage ,
88+ } satisfies ServiceError ;
89+ }
90+ }
91+
92+ await tx . userToOrg . delete ( {
93+ where : {
94+ orgId_userId : {
95+ orgId,
96+ userId,
97+ }
98+ }
99+ } ) ;
100+
101+ return null ;
102+ } , { isolationLevel : Prisma . TransactionIsolationLevel . Serializable } ) ;
103+
104+ // Sync with lighthouse s.t., the subscription
105+ // quantity will update immediately.
106+ if ( ! isServiceError ( result ) ) {
107+ await syncWithLighthouse ( orgId ) ;
108+ }
109+
110+ return result ;
111+ } ;
112+
113+
116114export const rejectAccountRequest = async ( requestId : string ) => sew ( ( ) =>
117115 withAuth ( async ( { org, role, prisma } ) =>
118116 withMinimumOrgRole ( role , OrgRole . OWNER , async ( ) => {
0 commit comments