Skip to content

Commit abe2158

Browse files
wip on lighthouse client
1 parent c99b3d0 commit abe2158

File tree

5 files changed

+109
-51
lines changed

5 files changed

+109
-51
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ServiceError } from "@/lib/serviceError";
88
import { StatusCodes } from "http-status-codes";
99
import { ErrorCode } from "@/lib/errorCodes";
1010
import { env, encryptActivationCode, decryptActivationCode } from "@sourcebot/shared";
11-
import { sendServicePing } from "@/ee/features/lighthouse/servicePing";
11+
import { syncWithLighthouse } from "@/ee/features/lighthouse/servicePing";
1212
import { fetchWithRetry } from "@/lib/utils";
1313
import { checkoutResponseSchema, portalResponseSchema } from "./types";
1414

@@ -37,7 +37,7 @@ export const activateLicense = async (activationCode: string): Promise<{ success
3737

3838
// Immediately ping Lighthouse to validate and sync license data
3939
try {
40-
await sendServicePing();
40+
await syncWithLighthouse(org.id);
4141
} catch {
4242
// If the ping fails, remove the license record
4343
await prisma.license.delete({
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { fetchWithRetry, isServiceError } from "@/lib/utils";
2+
import { env } from "@sourcebot/shared";
3+
import { ServicePingRequest, ServicePingResponse, servicePingResponseSchema } from "./types";
4+
import { ServiceError } from "@/lib/serviceError";
5+
import { ErrorCode } from "@/lib/errorCodes";
6+
import { StatusCodes } from "http-status-codes";
7+
import { z } from "zod";
8+
9+
export const sendServicePing = async (body: ServicePingRequest): Promise<ServicePingResponse | ServiceError> => {
10+
const response = await fetchWithRetry(`${env.SOURCEBOT_LIGHTHOUSE_URL}/ping`, {
11+
method: 'POST',
12+
headers: { 'Content-Type': 'application/json' },
13+
body: JSON.stringify(body),
14+
});
15+
16+
const result = await parseResponseBody(response, servicePingResponseSchema);
17+
return result;
18+
}
19+
20+
const parseResponseBody = async <T extends z.ZodTypeAny>(
21+
response: Response,
22+
schema: T,
23+
): Promise<z.infer<T> | ServiceError> => {
24+
let body: unknown;
25+
try {
26+
body = await response.json();
27+
} catch (error) {
28+
return {
29+
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
30+
errorCode: ErrorCode.INVALID_RESPONSE_BODY,
31+
message: `Failed to parse response body as JSON: ${error instanceof Error ? error.message : String(error)}`,
32+
};
33+
}
34+
35+
if (isServiceError(body)) {
36+
return body;
37+
}
38+
39+
const parsed = schema.safeParse(body);
40+
if (!parsed.success) {
41+
return {
42+
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
43+
errorCode: ErrorCode.INVALID_RESPONSE_BODY,
44+
message: `Response body failed schema validation: ${parsed.error.message}`,
45+
};
46+
}
47+
48+
return parsed.data;
49+
}
Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,70 @@
1-
import { fetchWithRetry } from "@/lib/utils";
1+
import { SINGLE_TENANT_ORG_ID, SOURCEBOT_GUEST_USER_ID } from "@/lib/constants";
2+
import { isServiceError } from "@/lib/utils";
23
import { __unsafePrisma } from "@/prisma";
3-
import { createLogger, env, SOURCEBOT_VERSION, decryptActivationCode } from "@sourcebot/shared";
4-
import { SINGLE_TENANT_ORG_ID } from "@/lib/constants";
5-
import { lighthouseResponseSchema } from "./types";
4+
import { createLogger, decryptActivationCode, env, SOURCEBOT_VERSION } from "@sourcebot/shared";
5+
import { sendServicePing } from "./client";
6+
import { ServicePingRequest } from "./types";
67

78
const logger = createLogger('service-ping');
89

910
const SERVICE_PING_INTERVAL_MS = 24 * 60 * 60 * 1000; // 1 day
1011

11-
export const sendServicePing = async () => {
12+
export const syncWithLighthouse = async (orgId: number) => {
1213
// Look up the activation code from the License record
1314
const license = await __unsafePrisma.license.findUnique({
14-
where: { orgId: SINGLE_TENANT_ORG_ID },
15+
where: { orgId },
16+
});
17+
18+
const userCount = await __unsafePrisma.userToOrg.count({
19+
where: {
20+
orgId,
21+
userId: {
22+
not: SOURCEBOT_GUEST_USER_ID,
23+
}
24+
},
1525
});
1626

1727
const activationCode = license?.activationCode
1828
? decryptActivationCode(license.activationCode)
1929
: undefined;
2030

21-
const payload = {
31+
const payload: ServicePingRequest = {
2232
installId: env.SOURCEBOT_INSTALL_ID,
2333
version: SOURCEBOT_VERSION,
34+
userCount,
2435
...(activationCode && { activationCode }),
2536
};
2637

27-
try {
28-
const response = await fetchWithRetry(`${env.SOURCEBOT_LIGHTHOUSE_URL}/ping`, {
29-
method: 'POST',
30-
headers: { 'Content-Type': 'application/json' },
31-
body: JSON.stringify(payload),
32-
});
33-
34-
if (!response.ok) {
35-
logger.error(`Service ping failed with status ${response.status}`);
36-
return;
37-
}
38-
39-
logger.info(`Service ping sent successfully`);
38+
const response = await sendServicePing(payload);
39+
if (isServiceError(response)) {
40+
logger.error(`Service ping failed:\n ${JSON.stringify(response, null, 2)}`)
41+
return;
42+
}
4043

41-
// If we have a license, update it with the Lighthouse response
42-
if (license) {
43-
const body = await response.json();
44-
const result = lighthouseResponseSchema.safeParse(body);
44+
logger.info(`Service ping sent successfully`);
4545

46-
if (!result.success) {
47-
logger.error(`Invalid Lighthouse response: ${result.error}`);
48-
return;
49-
}
46+
// If we have a license and Lighthouse returned license data, sync it
47+
if (license && response.license) {
48+
const { plan, seats, status } = response.license;
5049

51-
const { plan, seats, status } = result.data;
52-
53-
await __unsafePrisma.license.update({
54-
where: { orgId: SINGLE_TENANT_ORG_ID },
55-
data: {
56-
plan,
57-
seats,
58-
status,
59-
lastSyncAt: new Date(),
60-
},
61-
});
50+
await __unsafePrisma.license.update({
51+
where: { orgId: SINGLE_TENANT_ORG_ID },
52+
data: {
53+
plan,
54+
seats,
55+
status,
56+
lastSyncAt: new Date(),
57+
},
58+
});
6259

63-
logger.info(`License synced: plan=${plan}, seats=${seats}, status=${status}`);
64-
}
65-
} catch (error) {
66-
logger.error(`Service ping failed: ${error}`);
60+
logger.info(`License synced: plan=${plan}, seats=${seats}, status=${status}`);
6761
}
6862
};
6963

7064
export const startServicePingCronJob = () => {
71-
sendServicePing();
72-
setInterval(sendServicePing, SERVICE_PING_INTERVAL_MS);
65+
syncWithLighthouse(SINGLE_TENANT_ORG_ID);
66+
setInterval(
67+
() => syncWithLighthouse(SINGLE_TENANT_ORG_ID),
68+
SERVICE_PING_INTERVAL_MS
69+
);
7370
};

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import { z } from "zod";
22

3-
export const lighthouseResponseSchema = z.object({
4-
plan: z.string(),
5-
seats: z.number(),
6-
status: z.string(),
3+
export const servicePingRequestSchema = z.object({
4+
installId: z.string(),
5+
version: z.string(),
6+
userCount: z.number(),
7+
activationCode: z.string().optional(),
78
});
9+
export type ServicePingRequest = z.infer<typeof servicePingRequestSchema>;
10+
11+
export const servicePingResponseSchema = z.object({
12+
license: z.object({
13+
plan: z.string(),
14+
seats: z.number(),
15+
status: z.string(),
16+
}).optional(),
17+
});
18+
export type ServicePingResponse = z.infer<typeof servicePingResponseSchema>;
819

920
export const checkoutResponseSchema = z.object({
1021
url: z.string(),

packages/web/src/lib/errorCodes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export enum ErrorCode {
55
REPOSITORY_NOT_FOUND = 'REPOSITORY_NOT_FOUND',
66
FILE_NOT_FOUND = 'FILE_NOT_FOUND',
77
INVALID_REQUEST_BODY = 'INVALID_REQUEST_BODY',
8+
INVALID_RESPONSE_BODY = 'INVALID_RESPONSE_BODY',
89
INVALID_QUERY_PARAMS = 'INVALID_QUERY_PARAMS',
910
NOT_AUTHENTICATED = 'NOT_AUTHENTICATED',
1011
NOT_FOUND = 'NOT_FOUND',

0 commit comments

Comments
 (0)