Skip to content

Commit cae073d

Browse files
add explicit service ping calls to user <> organization update paths
1 parent f80a11d commit cae073d

File tree

3 files changed

+83
-80
lines changed

3 files changed

+83
-80
lines changed

packages/web/src/ee/features/lighthouse/servicePing.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ export const syncWithLighthouse = async (orgId: number) => {
4545
const { entitlements, seats, status } = response.license;
4646

4747
await __unsafePrisma.license.update({
48-
where: { orgId: SINGLE_TENANT_ORG_ID },
48+
where: {
49+
orgId
50+
},
4951
data: {
5052
entitlements,
5153
seats,

packages/web/src/features/userManagement/actions.ts

Lines changed: 71 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use server';
22

33
import { createAudit } from "@/ee/features/audit/audit";
4+
import { syncWithLighthouse } from "@/ee/features/lighthouse/servicePing";
45
import InviteUserEmail from "@/emails/inviteUserEmail";
56
import JoinRequestApprovedEmail from "@/emails/joinRequestApprovedEmail";
67
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
@@ -11,7 +12,7 @@ import { sew } from "@/middleware/sew";
1112
import { withAuth } from "@/middleware/withAuth";
1213
import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole";
1314
import { render } from "@react-email/components";
14-
import { OrgRole, Prisma } from "@sourcebot/db";
15+
import { OrgRole, Prisma, PrismaClient } from "@sourcebot/db";
1516
import { createLogger, env, getSMTPConnectionURL } from "@sourcebot/shared";
1617
import { StatusCodes } from "http-status-codes";
1718
import { createTransport } from "nodemailer";
@@ -21,48 +22,11 @@ const logger = createLogger('user-management');
2122
export 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

7539
export 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+
116114
export const rejectAccountRequest = async (requestId: string) => sew(() =>
117115
withAuth(async ({ org, role, prisma }) =>
118116
withMinimumOrgRole(role, OrgRole.OWNER, async () => {

packages/web/src/lib/authUtils.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import type { User as AuthJsUser } from "next-auth";
22
import { __unsafePrisma } from "@/prisma";
33
import { OrgRole } from "@sourcebot/db";
44
import { SINGLE_TENANT_ORG_ID } from "@/lib/constants";
5-
import { isServiceError } from "@/lib/utils";
65
import { orgNotFound, ServiceError, userNotFound } from "@/lib/serviceError";
76
import { createLogger, getSeatCap } from "@sourcebot/shared";
87
import { createAudit } from "@/ee/features/audit/audit";
98
import { StatusCodes } from "http-status-codes";
109
import { ErrorCode } from "./errorCodes";
10+
import { syncWithLighthouse } from "@/ee/features/lighthouse/servicePing";
1111

1212
const logger = createLogger('web-auth-utils');
1313

@@ -124,6 +124,10 @@ export const onCreateUser = async ({ user }: { user: AuthJsUser }) => {
124124
// authUtils -> posthog -> auth -> authUtils
125125
const { captureEvent } = await import("@/lib/posthog");
126126
await captureEvent('wa_user_created', { userId: user.id });
127+
128+
// Sync with lighthouse s.t., the subscription
129+
// quantity will update immediately.
130+
await syncWithLighthouse(defaultOrg.id);
127131
};
128132

129133

@@ -188,7 +192,7 @@ export const addUserToOrganization = async (userId: string, orgId: number): Prom
188192
} satisfies ServiceError;
189193
}
190194

191-
const res = await __unsafePrisma.$transaction(async (tx) => {
195+
await __unsafePrisma.$transaction(async (tx) => {
192196
await tx.userToOrg.create({
193197
data: {
194198
userId: user.id,
@@ -234,10 +238,9 @@ export const addUserToOrganization = async (userId: string, orgId: number): Prom
234238
}
235239
});
236240

237-
if (isServiceError(res)) {
238-
logger.error(`addUserToOrganization: failed to add user ${userId} to org ${orgId}: ${res.message}`);
239-
return res;
240-
}
241+
// Sync with lighthouse s.t., the subscription
242+
// quantity will update immediately.
243+
await syncWithLighthouse(org.id);
241244

242245
return {
243246
success: true,

0 commit comments

Comments
 (0)