Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection
NODE_ENV=development

DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=true

SOURCEBOT_LIGHTHOUSE_URL=http://localhost:3003
7 changes: 7 additions & 0 deletions packages/backend/src/__mocks__/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { vi } from 'vitest';

export const prisma = {
license: {
findUnique: vi.fn().mockResolvedValue(null),
},
};
5 changes: 3 additions & 2 deletions packages/backend/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PrismaClient, RepoIndexingJobType } from '@sourcebot/db';
import { createLogger, env, hasEntitlement, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from '@sourcebot/shared';
import { createLogger, env, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from '@sourcebot/shared';
import { hasEntitlement } from './entitlements.js';
import express, { Request, Response } from 'express';
import 'express-async-errors';
import * as http from "http";
Expand Down Expand Up @@ -100,7 +101,7 @@ export class Api {
}

private async triggerAccountPermissionSync(req: Request, res: Response) {
if (env.PERMISSION_SYNC_ENABLED !== 'true' || !hasEntitlement('permission-syncing')) {
if (env.PERMISSION_SYNC_ENABLED !== 'true' || !await hasEntitlement('permission-syncing')) {
res.status(403).json({ error: 'Permission syncing is not enabled.' });
return;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/backend/src/ee/accountPermissionSyncer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Sentry from "@sentry/node";
import { PrismaClient, AccountPermissionSyncJobStatus, Account, PermissionSyncSource} from "@sourcebot/db";
import { env, hasEntitlement, createLogger, loadConfig, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from "@sourcebot/shared";
import { env, createLogger, loadConfig, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from "@sourcebot/shared";
import { hasEntitlement } from "../entitlements.js";
import { ensureFreshAccountToken } from "./tokenRefresh.js";
import { Job, Queue, Worker } from "bullmq";
import { Redis } from "ioredis";
Expand Down Expand Up @@ -50,8 +51,8 @@ export class AccountPermissionSyncer {
this.worker.on('failed', this.onJobFailed.bind(this));
}

public startScheduler() {
if (!hasEntitlement('permission-syncing')) {
public async startScheduler() {
if (!await hasEntitlement('permission-syncing')) {
throw new Error('Permission syncing is not supported in current plan.');
}

Expand Down
7 changes: 4 additions & 3 deletions packages/backend/src/ee/repoPermissionSyncer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as Sentry from "@sentry/node";
import { PermissionSyncSource, PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db";
import { createLogger, PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "@sourcebot/shared";
import { env, hasEntitlement } from "@sourcebot/shared";
import { env } from "@sourcebot/shared";
import { hasEntitlement } from "../entitlements.js";
import { Job, Queue, Worker } from 'bullmq';
import { Redis } from 'ioredis';
import { createOctokitFromToken, getRepoCollaborators, GITHUB_CLOUD_HOSTNAME } from "../github.js";
Expand Down Expand Up @@ -44,8 +45,8 @@ export class RepoPermissionSyncer {
this.worker.on('failed', this.onJobFailed.bind(this));
}

public startScheduler() {
if (!hasEntitlement('permission-syncing')) {
public async startScheduler() {
if (!await hasEntitlement('permission-syncing')) {
throw new Error('Permission syncing is not supported in current plan.');
}

Expand Down
7 changes: 5 additions & 2 deletions packages/backend/src/ee/syncSearchContexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ vi.mock('@sourcebot/shared', async (importOriginal) => {
error: vi.fn(),
debug: vi.fn(),
})),
hasEntitlement: vi.fn(() => true),
getPlan: vi.fn(() => 'enterprise'),
SOURCEBOT_SUPPORT_EMAIL: 'support@sourcebot.dev',
};
});

vi.mock('../entitlements.js', () => ({
hasEntitlement: vi.fn(() => Promise.resolve(true)),
getPlan: vi.fn(() => Promise.resolve('enterprise')),
}));

import { syncSearchContexts } from './syncSearchContexts.js';

// Helper to build a repo record with GitLab topics stored in metadata.
Expand Down
7 changes: 4 additions & 3 deletions packages/backend/src/ee/syncSearchContexts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import micromatch from "micromatch";
import { createLogger } from "@sourcebot/shared";
import { PrismaClient } from "@sourcebot/db";
import { getPlan, hasEntitlement, repoMetadataSchema, SOURCEBOT_SUPPORT_EMAIL } from "@sourcebot/shared";
import { repoMetadataSchema, SOURCEBOT_SUPPORT_EMAIL } from "@sourcebot/shared";
import { getPlan, hasEntitlement } from "../entitlements.js";
import { SearchContext } from "@sourcebot/schemas/v3/index.type";

const logger = createLogger('sync-search-contexts');
Expand All @@ -15,9 +16,9 @@ interface SyncSearchContextsParams {
export const syncSearchContexts = async (params: SyncSearchContextsParams) => {
const { contexts, orgId, db } = params;

if (!hasEntitlement("search-contexts")) {
if (!await hasEntitlement("search-contexts")) {
if (contexts) {
const plan = getPlan();
const plan = await getPlan();
logger.warn(`Skipping search context sync. Reason: "Search contexts are not supported in your current plan: ${plan}. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}."`);
}
return false;
Expand Down
36 changes: 36 additions & 0 deletions packages/backend/src/entitlements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
Entitlement,
Plan,
getPlan as _getPlan,
getSeats as _getSeats,
hasEntitlement as _hasEntitlement,
getEntitlements as _getEntitlements,
} from "@sourcebot/shared";
import { prisma } from "./prisma.js";
import { SINGLE_TENANT_ORG_ID } from "./constants.js";

const getLicense = async () => {
return prisma.license.findUnique({
where: { orgId: SINGLE_TENANT_ORG_ID },
});
}

export const getPlan = async (): Promise<Plan> => {
const license = await getLicense();
return _getPlan(license);
}

export const getSeats = async (): Promise<number> => {
const license = await getLicense();
return _getSeats(license);
}

export const hasEntitlement = async (entitlement: Entitlement): Promise<boolean> => {
const license = await getLicense();
return _hasEntitlement(entitlement, license);
}

export const getEntitlements = async (): Promise<Entitlement[]> => {
const license = await getLicense();
return _getEntitlements(license);
}
5 changes: 3 additions & 2 deletions packages/backend/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as Sentry from "@sentry/node";
import { getTokenFromConfig } from "@sourcebot/shared";
import { createLogger } from "@sourcebot/shared";
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
import { env, hasEntitlement } from "@sourcebot/shared";
import { env } from "@sourcebot/shared";
import { hasEntitlement } from "./entitlements.js";
import micromatch from "micromatch";
import pLimit from "p-limit";
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
Expand Down Expand Up @@ -124,7 +125,7 @@ const getOctokitWithGithubApp = async (
url: string | undefined,
context: string
): Promise<Octokit> => {
if (!hasEntitlement('github-app') || !GithubAppManager.getInstance().appsConfigured()) {
if (!await hasEntitlement('github-app') || !GithubAppManager.getInstance().appsConfigured()) {
return octokit;
}

Expand Down
18 changes: 6 additions & 12 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import "./instrument.js";

import * as Sentry from "@sentry/node";
import { PrismaClient } from "@sourcebot/db";
import { createLogger, env, getConfigSettings, getDBConnectionString, hasEntitlement } from "@sourcebot/shared";
import { createLogger, env, getConfigSettings } from "@sourcebot/shared";
import { hasEntitlement } from "./entitlements.js";
import { prisma } from "./prisma.js";
import 'express-async-errors';
import { existsSync } from 'fs';
import { mkdir } from 'fs/promises';
Expand Down Expand Up @@ -31,13 +32,6 @@ if (!existsSync(indexPath)) {
await mkdir(indexPath, { recursive: true });
}

const prisma = new PrismaClient({
datasources: {
db: {
url: getDBConnectionString(),
},
},
});

try {
await redis.ping();
Expand All @@ -51,7 +45,7 @@ const promClient = new PromClient();

const settings = await getConfigSettings(env.CONFIG_PATH);

if (hasEntitlement('github-app')) {
if (await hasEntitlement('github-app')) {
await GithubAppManager.getInstance().init(prisma);
}

Expand All @@ -66,11 +60,11 @@ connectionManager.startScheduler();
await repoIndexManager.startScheduler();
auditLogPruner.startScheduler();

if (env.PERMISSION_SYNC_ENABLED === 'true' && !hasEntitlement('permission-syncing')) {
if (env.PERMISSION_SYNC_ENABLED === 'true' && !await hasEntitlement('permission-syncing')) {
logger.error('Permission syncing is not supported in current plan. Please contact team@sourcebot.dev for assistance.');
process.exit(1);
}
else if (env.PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing')) {
else if (env.PERMISSION_SYNC_ENABLED === 'true' && await hasEntitlement('permission-syncing')) {
if (env.PERMISSION_SYNC_REPO_DRIVEN_ENABLED === 'true') {
repoPermissionSyncer.startScheduler();
}
Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PrismaClient } from "@sourcebot/db";
import { getDBConnectionString } from "@sourcebot/shared";

export const prisma = new PrismaClient({
datasources: {
db: {
url: getDBConnectionString(),
},
},
});
4 changes: 2 additions & 2 deletions packages/backend/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getTokenFromConfig } from "@sourcebot/shared";
import * as Sentry from "@sentry/node";
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
import { GithubAppManager } from "./ee/githubAppManager.js";
import { hasEntitlement } from "@sourcebot/shared";
import { hasEntitlement } from "./entitlements.js";
import { StatusCodes } from "http-status-codes";
import { isOctokitRequestError } from "./github.js";

Expand Down Expand Up @@ -116,7 +116,7 @@ export const fetchWithRetry = async <T>(
// may technically cause syncing to fail if that connection's token just so happens to not have access to the repo it's referencing.
export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, logger?: Logger): Promise<RepoAuthCredentials | undefined> => {
// If we have github apps configured we assume that we must use them for github service auth
if (repo.external_codeHostType === 'github' && hasEntitlement('github-app') && GithubAppManager.getInstance().appsConfigured()) {
if (repo.external_codeHostType === 'github' && await hasEntitlement('github-app') && GithubAppManager.getInstance().appsConfigured()) {
logger?.debug(`Using GitHub App for service auth for repo ${repo.displayName} hosted at ${repo.external_codeHostUrl}`);

const owner = repo.displayName?.split('/')[0];
Expand Down
6 changes: 5 additions & 1 deletion packages/backend/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
test: {
environment: 'node',
watch: false,
env: {
DATA_CACHE_DIR: 'test-data'
}
},
alias: {
'./prisma.js': path.resolve(__dirname, 'src/__mocks__/prisma.ts'),
},
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "License" (
"id" TEXT NOT NULL,
"orgId" INTEGER NOT NULL,
"activationCode" TEXT NOT NULL,
"plan" TEXT,
"seats" INTEGER,
"status" TEXT,
"lastSyncAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "License_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "License_orgId_key" ON "License"("orgId");

-- AddForeignKey
ALTER TABLE "License" ADD CONSTRAINT "License_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
15 changes: 15 additions & 0 deletions packages/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,21 @@ model Org {
searchContexts SearchContext[]

chats Chat[]

license License?
}

model License {
id String @id @default(cuid())
orgId Int @unique
org Org @relation(fields: [orgId], references: [id])
activationCode String
plan String?
seats Int?
status String?
lastSyncAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

enum OrgRole {
Expand Down
Loading