sourcebot/packages/web/src/actions.ts

1569 lines
51 KiB
TypeScript
Raw Normal View History

v3 effort (#158) * SQL Database (#157) * point zoekt to v3 branch * bump zoekt version * Add tenant ID concept into web app and backend (#160) * hacked together a example of using zoekt grpc api * provide tenant id to zoekt git indexer * update zoekt version to point to multitenant branch * pipe tenant id through header to zoekt * remove incorrect submodule reference and settings typo * update zoekt commit * remove unused yarn script * remove unused grpc client in web server * remove unneeded deps and improve tenant id log * pass tenant id when creating repo in db * add mt yarn script * add nocheckin comment to tenant id in v2 schema --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * bump zoekt version * parallelize repo indexing (#163) * hacked together a example of using zoekt grpc api * provide tenant id to zoekt git indexer * update zoekt version to point to multitenant branch * pipe tenant id through header to zoekt * remove incorrect submodule reference and settings typo * update zoekt commit * remove unused yarn script * remove unused grpc client in web server * remove unneeded deps and improve tenant id log * pass tenant id when creating repo in db * add mt yarn script * add pol of bullmq into backend * add better error handling and concurrency setting * spin up redis instance in dockerfile * cleanup transaction logic when adding repos to index queue * add NEW index status fetch condition * move bullmq deps to backend --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * Authentication (#164) * Add Org table (#167) * Move logout button & profile picture into settings dropdown (#172) * Multi tenancy support in config syncer (#171) * [wip] initial mt support in config syncer * Move logout button & profile picture into settings dropdown (#172) * update sync status properly and fix bug with multiple config in db case * make config path required in single tenant mode NOTE: deleting config/repos is currently not supported in multi tenancy case. Support for this will be added in a future PR --------- Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com> * add tenant mode support in docker container: * Organization switching & active org management (#173) * updated syncedAt date after config sync: * Migrate to postgres (#174) * spin up postgres in docker container * get initial pol of postgres db working in docker image * spin up postgres server in dev case * updated syncedAt date after config sync: * remove unnecessary port expose in docker file * Connection creation form (#175) * fix issue with yarn dev startup * init (#176) * Add `@sourcebot/schemas` package (#177) * Connection management (#178) * add concept of secrets (#180) * add @sourcebot/schemas package * migrate things to use the schemas package * Dockerfile support * add secret table to schema * Add concept of connection manager * Rename Config->Connection * Handle job failures * Add join table between repo and connection * nits * create first version of crypto package * add crypto package as deps to others * forgot to add package changes * add server action for adding and listing secrets, create test page for it * add secrets page to nav menu * add secret to config and support fetching it in backend * reset secret form on successful submission * add toast feedback for secrets form * add instructions for adding encryption key to dev instructions * add encryption key support in docker file * add delete secret button * fix nits from pr review --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * bump zoekt version * enforce tenancy on search and repo listing endpoints (#181) * enforce tenancy on search and repo listing * remove orgId from request schemas * adds garbage collection for repos (#182) * refactor repo indexing logic into RepoManager * wip cleanup stale repos * add rest of gc logic * set status to indexing properly * add initial logic for staging environment * try to move encryption key env decleration in docker file to fix build issues * switch encryption key as build arg to se if that fixes build issues * add deployment action for staging image * try using mac github action runners instead * switch to using arm64 runners on arm64 build * change workflow names to fix trigger issue * trigger staging actions to see if it works * fix working directory typo and pray it doesnt push to prod * checkout v3 when deploying staging * try to change into the staging dir manuall * dummy commit to trigger v3 workflows to test * update staging deploy script to match new version in main * reference proper image:tag in staging fly config * update staging fly config to point to ghcr * Connection management (#183) * add invite system and google oauth provider (#185) * add settings page with members list * add invite to schema and basic create form * add invite table * add basic invite link copy button * add auth invite accept case * add non auth logic * add google oauth provider * fix reference to header component in connections * add google logo to google oauth * fix web build errors * bump staging resources * change staging cpu to perf * add side bar nav in settings page * improve styling of members page * wip adding stripe checkout button * wip onboarding flow * add stripe subscription id to org * save stripe session id and add manage subscription button in settings * properly block access to pages if user isn't in an org * wip add paywall * Domain support * Domain support (#188) * Update Makefile to include crypto package when doing a make clean * Add default for AUTH_URL in attempt to fix build * attempt 2 * fix attempt #3: Do not require a encrpytion key at build time * Fix generate script race condition * Attempt #4 * add back paywall and also add support for incrememnting seat count on invite redemption * prevent self invite * action button styling in settings and toast on copy * add ability to remove member from org * move stripe product id to env var * add await for blocking loop in backend * add subscription info to billing page * handle trial case in billing info page * add trial duration indicator to nav bar * check if domain starts or ends with dash * remove unused no org component * Generate AUTH_SECRET if not provided (#189) * remove package lock file and fix prisma dep version * revert dep version updates * fix yarn.lock * add auth and membership check to fetchSubscription * properly handle invite redeem with no valid subscription case * change back fetch subscription to not require org membership * add back subscription check in invite redeem page * Add stripe billing logic (#190) * add side bar nav in settings page * improve styling of members page * wip adding stripe checkout button * wip onboarding flow * add stripe subscription id to org * save stripe session id and add manage subscription button in settings * properly block access to pages if user isn't in an org * wip add paywall * Domain support * add back paywall and also add support for incrememnting seat count on invite redemption * prevent self invite * action button styling in settings and toast on copy * add ability to remove member from org * move stripe product id to env var * add await for blocking loop in backend * add subscription info to billing page * handle trial case in billing info page * add trial duration indicator to nav bar * check if domain starts or ends with dash * remove unused no org component * remove package lock file and fix prisma dep version * revert dep version updates * fix yarn.lock * add auth and membership check to fetchSubscription * properly handle invite redeem with no valid subscription case * change back fetch subscription to not require org membership * add back subscription check in invite redeem page --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * fix nits * remove providers check * fix more nits * change stripe init to be behind function * fix publishible stripe key handling in docker container * enforce owner perms (#191) * add make owner logic, and owner perms for removal, invite, and manage subscription * add change billing email card to billing settings * enforce owner role in action level * remove unused hover card component * cleanup * add back gitlab, gitea, and gerrit support (#184) * add non github config definitions * refactor github config compilation to seperate file * add gitlab config compilation * Connection management (#183) * wip gitlab repo sync support * fix gitlab zoekt metadata * add gitea support * add gerrit support * Connection management (#183) * add gerrit config compilation * Connection management (#183) --------- Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com> * fix apos usage in redeem page * change csrf cookie to secure not host * Credentials provider (#192) * email password functionality * feedback * cleanup org's repos and shards if it's inactive (#194) * add stripe subscription status and webhook * add inactive org repo cleanup logic * mark reactivated org connections for sync * connections qol improvements (#195) * add client side polling to connections list * properly fetch repo image url * add client polling to connection management page, and add ability to sync failed connections * Fix build with suspense boundary * improved fix * add retries for 429 issues (#196) * add connection compile retry and hard repo limit * add more retry checks * cleanup unused change * address feedback * fix build errors and add index concurrency env var * add config upsert timeout env var * Membership settings rework (#198) * Add refined members list * futher progress on members settings polish * Remove old components * feedback * Magic links (#199) * wip on magic link support * Switch to nodemailer / resend for transactional mail * Further cleanup * Add stylized email using react-email * fix * Fix build * db performance improvements and job resilience (#200) * replace upsert with seperate create many and raw update many calls * add bulk repo status update and queue addition with priority * add support for managed redis * add note for changing raw sql on schema change * remove non secret token options * fix token examples in schema * add better visualization for connection/repo errors and warnings (#201) * replace upsert with seperate create many and raw update many calls * add bulk repo status update and queue addition with priority * add support for managed redis * add note for changing raw sql on schema change * add error package and use BackendException in connection manager * handle connection failure display on web app * add warning banner for not found orgs/repos/users * add failure handling for gerrit * add gitea notfound warning support * add warning icon in connections list * style nits * add failed repo vis in connections list * added retry failed repo index buttons * move nav indicators to client with polling * fix indicator flash issue and truncate large list results * display error nav better * truncate failed repo list in connection list item * fix merge error * fix merge bug * add connection util file [wip] * refactor notfound fetch logic and add missing error package to dockerfile * move repeated logic to function and add zod schema for syncStatusMetadata * add orgid unique constraint to repo * revert repo compile update logic to upsert loop * log upsert stats * [temp] disable polling everywhere (#205) * add health check endpoint * Refined onboarding flow (#202) * Redeem UX pass (#204) * add log for health check * fix new connection complete callback route * add cpu split logic and only wait for postgres if we're going to connec to it * Inline secret creation (#207) * use docker scopes to try and improve caching * Dummy change * remove cpu split logic * Add some instrumentation to web * add posthog events on various user actions (#208) * add page view event support * add posthog events * nit: remove unused import * feedback * fix merge error * use staging posthog papik when building staging image * fix other merge error and build warnings * Add invite email (#209) * wrap posthog provider in suspense to fix build error * add grafana alloy config and setup (#210) * add grafana alloy config and setup * add basic repo prom metrics * nits in dockerfile * remove invalid characters when auto filling domain * add login posthog events * remove hard coded sourcebot.app references * make repo garbage collection async (#211) * add gc queue logic * fix missing switch cases for gc status * style org create form better with new staging domain * change repo rm logic to be async * simplify repo for inactive org query * add grace period for garbage collecting repos * make prom scrape interval 500ms * fix typo in trial card * onboarding tweaks * rename some prom metrics and cleanup unused * wipe existing repo if we've picked up a killed job to ensure good state * Connections UX pass + query optimizations (#212) * remove git & local schemas (#213) * skip stripe checkout for trial + fix indexing in progress UI + additional schema validation (#214) * add additional config validation * wip bypass stripe checkout for trial * fix stripe trial checkout bypass * fix indexing in progress ui on home page * add subscription checks, more schema validation, and fix issue with complete page * dont display if no indexed repos * fix skipping onboard complete check * fix build error * add back button in onboard connection creation flow * Add back revision support (#215) * fix build * Fix bug with repository snapshot * fix share links * fix repo rm issue, 502 page, condition on test clock * Make login and onboarding mobile friendly * fix ordering of quick actions * remove error msg dump on failed repo index job, and update indexedAt field * Add mobile unsupported splash screne * cherry pick fix for file links * [Cherry Pick] Syntax reference guide (#169) (#216) * Add .env to db gitignore * fix case where we have repos but they're all failed for repo snapshot * /settings/secrets page (#217) * display domain properly in org create form * Quick action tweaks (#218) * revamp repo page (#220) * wip repo table * new repo page * add indicator for when feedback is applied in repo page * add repo button * fetch connection data in one query * fix styling * fix (#219) * remove / keyboard shortcut hint in search bar * prevent switching to first page on data update and truncate long repo names in repo list * General settings + cleanup (#221) * General settings * Add alert to org domain change * First attempt at sending logs to grafana * logs wip * add alloy logs * wip * [temp] comment out loki for now * update trial card content and add events for code host selection on onboard * reduce scraping interval to 15s * Add prometheus metric for pending repo indexing jobs * switch magic link to invite code (#222) * wip magic link codes * pipe email to email provider properly * remove magic link data cookie after sign in * clean up unused imports * dont remove cookie before we use it * rm package-lock.json * revert yarn files to v3 state * switch email passing from cookie to search param * add comment for settings dropdown auth update * remove unused middleware file * fix build error and warnings * fix build error with useSearchParam not wrapped in suspense * add sentry support to backend and webapp (#223) * add sentry to web app * set sentry environemnt from env var * add sentry env replace logic in docker container * wip add backend sentry * add sentry to backend * move dns to env var * remove test exception * Fix root domain issue on onboarding * add setup sentry cli step to github action * login to sentry * fix sentry login in action * Update grafana loki endpoint * switch source map publish to runtime in entrypoint * catch and rethrow simplegit exceptions * alloy nits * fix alloy * backend logging (#224) * revert grafana loki config * fix login ui nits * fix quick actions * fix typo in secret creation * fix private repo clone issue for gitlab * add repo index timeout logic * add posthog identify call after registeration * various changes to add terms and security info (#225) * add terms and security to footer * add security card * add demo card * fix build error * nit fix: center 'get in touch' on security card * Dark theme improvements (#226) * (fix) Fixed bug with gitlab and gitea not including hostname in the repoName * Switch to using t3-env for env-var management (#230) * Add missing env var * fix build * Centralize to using a single .env.development for development workflows (#231) * Make billing optional (#232) * Massage environment variables from strings to numbers (#234) * Single tenancy & auth modes (#233) * Add docs to this repo * dummy change * Declarative connection configuration (#235) * fix build * upgrade to next 14.2.25 * Improved database DX * migrate to yarn v4 * Use origin from header for baseUrl of emails (instead of AUTH_URL). Also removed reference to hide scrollbars * Remove SOURCEBOT_ENCRYPTION_KEY from build arg * Fix issue with linking default user to org in single tenant + no-auth mode * Fix fallback tokens (#242) * add SECURITY_CARD_ENABLED flag * Add repository weburl (#243) * Random fixes and improvements (#244) * add zoekt max wall time env var * remove empty warning in docs * fix reference in sh docs * add connection manager upsert timeout env var * Declarative connection cleanup + improvements (#245) * change contact us footer in app to point to main contact form * PostHog event pass (#246) * fix typo * Add sourcebot cloud environment prop to staging workflow * Update generated files * remove AUTH_URL since it unused and (likely) unnecessary * Revert "remove AUTH_URL since it unused and (likely) unnecessary" This reverts commit 1f4a5aed22fa94bace899262e8576427fc852f61. * cleanup GitHub action releases (#252) * remove alloy, change auth defaul to disabled, add settings page in me dropdown * enforce connection management perms to owner (#253) * enforce conneciton management perms to owner * fix formatting * more formatting * naming nits * fix var name error * change empty repo set copy if auth is disabled * add CONTRIBUTING.md file * hide settings in dropdown with auth isnt enabled * handle case where gerrit weburl is just gitiles path * Docs overhall (#251) * remove nocheckin * fix build error * remove v3 trigger from deploy staging * fix build errors round 2 * another error fix --------- Co-authored-by: msukkari <michael.sukkarieh@mail.mcgill.ca>
2025-04-01 05:34:42 +00:00
'use server';
import Ajv from "ajv";
import * as Sentry from '@sentry/nextjs';
import { auth } from "./auth";
import { notAuthenticated, notFound, ServiceError, unexpectedError, orgInvalidSubscription, secretAlreadyExists, stripeClientNotInitialized } from "@/lib/serviceError";
import { prisma } from "@/prisma";
import { StatusCodes } from "http-status-codes";
import { ErrorCode } from "@/lib/errorCodes";
import { isServiceError } from "@/lib/utils";
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
import { decrypt, encrypt } from "@sourcebot/crypto"
import { getConnection } from "./data/connection";
import { ConnectionSyncStatus, Prisma, OrgRole, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
import { cookies, headers } from "next/headers"
import { Session } from "next-auth";
import { env } from "@/env.mjs";
import Stripe from "stripe";
import { render } from "@react-email/components";
import InviteUserEmail from "./emails/inviteUserEmail";
import { createTransport } from "nodemailer";
import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
import { TenancyMode } from "./lib/types";
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_USER_EMAIL, SINGLE_TENANT_USER_ID } from "./lib/constants";
import { stripeClient } from "./lib/stripe";
import { IS_BILLING_ENABLED } from "./lib/stripe";
const ajv = new Ajv({
validateFormats: false,
});
/**
* "Service Error Wrapper".
*
* Captures any thrown exceptions and converts them to a unexpected
* service error. Also logs them with Sentry.
*/
export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> => {
try {
return await fn();
} catch (e) {
Sentry.captureException(e);
console.error(e);
return unexpectedError(`An unexpected error occurred. Please try again later.`);
}
}
export const withAuth = async <T>(fn: (session: Session) => Promise<T>, allowSingleTenantUnauthedAccess: boolean = false) => {
const session = await auth();
if (!session) {
if (
env.SOURCEBOT_TENANCY_MODE === 'single' &&
env.SOURCEBOT_AUTH_ENABLED === 'false' &&
allowSingleTenantUnauthedAccess === true
) {
// To allow for unauthed acccess in single-tenant mode, we can
// create a fake session with the default user. This user has membership
// in the default org.
// @see: initialize.ts
return fn({
user: {
id: SINGLE_TENANT_USER_ID,
email: SINGLE_TENANT_USER_EMAIL,
},
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30).toISOString(),
});
}
return notAuthenticated();
}
return fn(session);
}
export const withOrgMembership = async <T>(session: Session, domain: string, fn: (params: { orgId: number, userRole: OrgRole }) => Promise<T>, minRequiredRole: OrgRole = OrgRole.MEMBER) => {
const org = await prisma.org.findUnique({
where: {
domain,
},
});
if (!org) {
return notFound();
}
const membership = await prisma.userToOrg.findUnique({
where: {
orgId_userId: {
userId: session.user.id,
orgId: org.id,
}
},
});
if (!membership) {
return notFound();
}
const getAuthorizationPrecendence = (role: OrgRole): number => {
switch (role) {
case OrgRole.MEMBER:
return 0;
case OrgRole.OWNER:
return 1;
}
}
if (getAuthorizationPrecendence(membership.role) < getAuthorizationPrecendence(minRequiredRole)) {
return {
statusCode: StatusCodes.FORBIDDEN,
errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,
message: "You do not have sufficient permissions to perform this action.",
} satisfies ServiceError;
}
return fn({
orgId: org.id,
userRole: membership.role,
});
}
export const withTenancyModeEnforcement = async<T>(mode: TenancyMode, fn: () => Promise<T>) => {
if (env.SOURCEBOT_TENANCY_MODE !== mode) {
return {
statusCode: StatusCodes.FORBIDDEN,
errorCode: ErrorCode.ACTION_DISALLOWED_IN_TENANCY_MODE,
message: "This action is not allowed in the current tenancy mode.",
} satisfies ServiceError;
}
return fn();
}
////// Actions ///////
export const createOrg = (name: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() =>
withTenancyModeEnforcement('multi', () =>
withAuth(async (session) => {
const org = await prisma.org.create({
data: {
name,
domain,
members: {
create: {
role: "OWNER",
user: {
connect: {
id: session.user.id,
}
}
}
}
}
});
return {
id: org.id,
}
})));
export const updateOrgName = async (name: string, domain: string) => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const { success } = orgNameSchema.safeParse(name);
if (!success) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "Invalid organization url",
} satisfies ServiceError;
}
await prisma.org.update({
where: { id: orgId },
data: { name },
});
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
export const updateOrgDomain = async (newDomain: string, existingDomain: string) => sew(() =>
withTenancyModeEnforcement('multi', () =>
withAuth((session) =>
withOrgMembership(session, existingDomain, async ({ orgId }) => {
const { success } = await orgDomainSchema.safeParseAsync(newDomain);
if (!success) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "Invalid organization url",
} satisfies ServiceError;
}
await prisma.org.update({
where: { id: orgId },
data: { domain: newDomain },
});
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.OWNER)
)));
export const completeOnboarding = async (domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const org = await prisma.org.findUnique({
where: { id: orgId },
});
if (!org) {
return notFound();
}
// If billing is not enabled, we can just mark the org as onboarded.
if (!IS_BILLING_ENABLED) {
await prisma.org.update({
where: { id: orgId },
data: {
isOnboarded: true,
}
});
// Else, validate that the org has an active subscription.
} else {
const subscriptionOrError = await fetchSubscription(domain);
if (isServiceError(subscriptionOrError)) {
return subscriptionOrError;
}
await prisma.org.update({
where: { id: orgId },
data: {
isOnboarded: true,
stripeSubscriptionStatus: StripeSubscriptionStatus.ACTIVE,
stripeLastUpdatedAt: new Date(),
}
});
}
return {
success: true,
}
})
));
export const getSecrets = (domain: string): Promise<{ createdAt: Date; key: string; }[] | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const secrets = await prisma.secret.findMany({
where: {
orgId,
},
select: {
key: true,
createdAt: true
}
});
return secrets.map((secret) => ({
key: secret.key,
createdAt: secret.createdAt,
}));
})));
export const createSecret = async (key: string, value: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const encrypted = encrypt(value);
const existingSecret = await prisma.secret.findUnique({
where: {
orgId_key: {
orgId,
key,
}
}
});
if (existingSecret) {
return secretAlreadyExists();
}
await prisma.secret.create({
data: {
orgId,
key,
encryptedValue: encrypted.encryptedData,
iv: encrypted.iv,
}
});
return {
success: true,
}
})));
export const checkIfSecretExists = async (key: string, domain: string): Promise<boolean | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const secret = await prisma.secret.findUnique({
where: {
orgId_key: {
orgId,
key,
}
}
});
return !!secret;
})));
export const deleteSecret = async (key: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
await prisma.secret.delete({
where: {
orgId_key: {
orgId,
key,
}
}
});
return {
success: true,
}
})));
export const getConnections = async (domain: string, filter: { status?: ConnectionSyncStatus[] } = {}) => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const connections = await prisma.connection.findMany({
where: {
orgId,
...(filter.status ? {
syncStatus: { in: filter.status }
} : {}),
},
include: {
repos: {
include: {
repo: true,
}
}
}
});
return connections.map((connection) => ({
id: connection.id,
name: connection.name,
syncStatus: connection.syncStatus,
syncStatusMetadata: connection.syncStatusMetadata,
connectionType: connection.connectionType,
updatedAt: connection.updatedAt,
syncedAt: connection.syncedAt ?? undefined,
linkedRepos: connection.repos.map(({ repo }) => ({
id: repo.id,
name: repo.name,
repoIndexingStatus: repo.repoIndexingStatus,
})),
}));
}), /* allowSingleTenantUnauthedAccess = */ true));
v3 effort (#158) * SQL Database (#157) * point zoekt to v3 branch * bump zoekt version * Add tenant ID concept into web app and backend (#160) * hacked together a example of using zoekt grpc api * provide tenant id to zoekt git indexer * update zoekt version to point to multitenant branch * pipe tenant id through header to zoekt * remove incorrect submodule reference and settings typo * update zoekt commit * remove unused yarn script * remove unused grpc client in web server * remove unneeded deps and improve tenant id log * pass tenant id when creating repo in db * add mt yarn script * add nocheckin comment to tenant id in v2 schema --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * bump zoekt version * parallelize repo indexing (#163) * hacked together a example of using zoekt grpc api * provide tenant id to zoekt git indexer * update zoekt version to point to multitenant branch * pipe tenant id through header to zoekt * remove incorrect submodule reference and settings typo * update zoekt commit * remove unused yarn script * remove unused grpc client in web server * remove unneeded deps and improve tenant id log * pass tenant id when creating repo in db * add mt yarn script * add pol of bullmq into backend * add better error handling and concurrency setting * spin up redis instance in dockerfile * cleanup transaction logic when adding repos to index queue * add NEW index status fetch condition * move bullmq deps to backend --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * Authentication (#164) * Add Org table (#167) * Move logout button & profile picture into settings dropdown (#172) * Multi tenancy support in config syncer (#171) * [wip] initial mt support in config syncer * Move logout button & profile picture into settings dropdown (#172) * update sync status properly and fix bug with multiple config in db case * make config path required in single tenant mode NOTE: deleting config/repos is currently not supported in multi tenancy case. Support for this will be added in a future PR --------- Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com> * add tenant mode support in docker container: * Organization switching & active org management (#173) * updated syncedAt date after config sync: * Migrate to postgres (#174) * spin up postgres in docker container * get initial pol of postgres db working in docker image * spin up postgres server in dev case * updated syncedAt date after config sync: * remove unnecessary port expose in docker file * Connection creation form (#175) * fix issue with yarn dev startup * init (#176) * Add `@sourcebot/schemas` package (#177) * Connection management (#178) * add concept of secrets (#180) * add @sourcebot/schemas package * migrate things to use the schemas package * Dockerfile support * add secret table to schema * Add concept of connection manager * Rename Config->Connection * Handle job failures * Add join table between repo and connection * nits * create first version of crypto package * add crypto package as deps to others * forgot to add package changes * add server action for adding and listing secrets, create test page for it * add secrets page to nav menu * add secret to config and support fetching it in backend * reset secret form on successful submission * add toast feedback for secrets form * add instructions for adding encryption key to dev instructions * add encryption key support in docker file * add delete secret button * fix nits from pr review --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * bump zoekt version * enforce tenancy on search and repo listing endpoints (#181) * enforce tenancy on search and repo listing * remove orgId from request schemas * adds garbage collection for repos (#182) * refactor repo indexing logic into RepoManager * wip cleanup stale repos * add rest of gc logic * set status to indexing properly * add initial logic for staging environment * try to move encryption key env decleration in docker file to fix build issues * switch encryption key as build arg to se if that fixes build issues * add deployment action for staging image * try using mac github action runners instead * switch to using arm64 runners on arm64 build * change workflow names to fix trigger issue * trigger staging actions to see if it works * fix working directory typo and pray it doesnt push to prod * checkout v3 when deploying staging * try to change into the staging dir manuall * dummy commit to trigger v3 workflows to test * update staging deploy script to match new version in main * reference proper image:tag in staging fly config * update staging fly config to point to ghcr * Connection management (#183) * add invite system and google oauth provider (#185) * add settings page with members list * add invite to schema and basic create form * add invite table * add basic invite link copy button * add auth invite accept case * add non auth logic * add google oauth provider * fix reference to header component in connections * add google logo to google oauth * fix web build errors * bump staging resources * change staging cpu to perf * add side bar nav in settings page * improve styling of members page * wip adding stripe checkout button * wip onboarding flow * add stripe subscription id to org * save stripe session id and add manage subscription button in settings * properly block access to pages if user isn't in an org * wip add paywall * Domain support * Domain support (#188) * Update Makefile to include crypto package when doing a make clean * Add default for AUTH_URL in attempt to fix build * attempt 2 * fix attempt #3: Do not require a encrpytion key at build time * Fix generate script race condition * Attempt #4 * add back paywall and also add support for incrememnting seat count on invite redemption * prevent self invite * action button styling in settings and toast on copy * add ability to remove member from org * move stripe product id to env var * add await for blocking loop in backend * add subscription info to billing page * handle trial case in billing info page * add trial duration indicator to nav bar * check if domain starts or ends with dash * remove unused no org component * Generate AUTH_SECRET if not provided (#189) * remove package lock file and fix prisma dep version * revert dep version updates * fix yarn.lock * add auth and membership check to fetchSubscription * properly handle invite redeem with no valid subscription case * change back fetch subscription to not require org membership * add back subscription check in invite redeem page * Add stripe billing logic (#190) * add side bar nav in settings page * improve styling of members page * wip adding stripe checkout button * wip onboarding flow * add stripe subscription id to org * save stripe session id and add manage subscription button in settings * properly block access to pages if user isn't in an org * wip add paywall * Domain support * add back paywall and also add support for incrememnting seat count on invite redemption * prevent self invite * action button styling in settings and toast on copy * add ability to remove member from org * move stripe product id to env var * add await for blocking loop in backend * add subscription info to billing page * handle trial case in billing info page * add trial duration indicator to nav bar * check if domain starts or ends with dash * remove unused no org component * remove package lock file and fix prisma dep version * revert dep version updates * fix yarn.lock * add auth and membership check to fetchSubscription * properly handle invite redeem with no valid subscription case * change back fetch subscription to not require org membership * add back subscription check in invite redeem page --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * fix nits * remove providers check * fix more nits * change stripe init to be behind function * fix publishible stripe key handling in docker container * enforce owner perms (#191) * add make owner logic, and owner perms for removal, invite, and manage subscription * add change billing email card to billing settings * enforce owner role in action level * remove unused hover card component * cleanup * add back gitlab, gitea, and gerrit support (#184) * add non github config definitions * refactor github config compilation to seperate file * add gitlab config compilation * Connection management (#183) * wip gitlab repo sync support * fix gitlab zoekt metadata * add gitea support * add gerrit support * Connection management (#183) * add gerrit config compilation * Connection management (#183) --------- Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com> * fix apos usage in redeem page * change csrf cookie to secure not host * Credentials provider (#192) * email password functionality * feedback * cleanup org's repos and shards if it's inactive (#194) * add stripe subscription status and webhook * add inactive org repo cleanup logic * mark reactivated org connections for sync * connections qol improvements (#195) * add client side polling to connections list * properly fetch repo image url * add client polling to connection management page, and add ability to sync failed connections * Fix build with suspense boundary * improved fix * add retries for 429 issues (#196) * add connection compile retry and hard repo limit * add more retry checks * cleanup unused change * address feedback * fix build errors and add index concurrency env var * add config upsert timeout env var * Membership settings rework (#198) * Add refined members list * futher progress on members settings polish * Remove old components * feedback * Magic links (#199) * wip on magic link support * Switch to nodemailer / resend for transactional mail * Further cleanup * Add stylized email using react-email * fix * Fix build * db performance improvements and job resilience (#200) * replace upsert with seperate create many and raw update many calls * add bulk repo status update and queue addition with priority * add support for managed redis * add note for changing raw sql on schema change * remove non secret token options * fix token examples in schema * add better visualization for connection/repo errors and warnings (#201) * replace upsert with seperate create many and raw update many calls * add bulk repo status update and queue addition with priority * add support for managed redis * add note for changing raw sql on schema change * add error package and use BackendException in connection manager * handle connection failure display on web app * add warning banner for not found orgs/repos/users * add failure handling for gerrit * add gitea notfound warning support * add warning icon in connections list * style nits * add failed repo vis in connections list * added retry failed repo index buttons * move nav indicators to client with polling * fix indicator flash issue and truncate large list results * display error nav better * truncate failed repo list in connection list item * fix merge error * fix merge bug * add connection util file [wip] * refactor notfound fetch logic and add missing error package to dockerfile * move repeated logic to function and add zod schema for syncStatusMetadata * add orgid unique constraint to repo * revert repo compile update logic to upsert loop * log upsert stats * [temp] disable polling everywhere (#205) * add health check endpoint * Refined onboarding flow (#202) * Redeem UX pass (#204) * add log for health check * fix new connection complete callback route * add cpu split logic and only wait for postgres if we're going to connec to it * Inline secret creation (#207) * use docker scopes to try and improve caching * Dummy change * remove cpu split logic * Add some instrumentation to web * add posthog events on various user actions (#208) * add page view event support * add posthog events * nit: remove unused import * feedback * fix merge error * use staging posthog papik when building staging image * fix other merge error and build warnings * Add invite email (#209) * wrap posthog provider in suspense to fix build error * add grafana alloy config and setup (#210) * add grafana alloy config and setup * add basic repo prom metrics * nits in dockerfile * remove invalid characters when auto filling domain * add login posthog events * remove hard coded sourcebot.app references * make repo garbage collection async (#211) * add gc queue logic * fix missing switch cases for gc status * style org create form better with new staging domain * change repo rm logic to be async * simplify repo for inactive org query * add grace period for garbage collecting repos * make prom scrape interval 500ms * fix typo in trial card * onboarding tweaks * rename some prom metrics and cleanup unused * wipe existing repo if we've picked up a killed job to ensure good state * Connections UX pass + query optimizations (#212) * remove git & local schemas (#213) * skip stripe checkout for trial + fix indexing in progress UI + additional schema validation (#214) * add additional config validation * wip bypass stripe checkout for trial * fix stripe trial checkout bypass * fix indexing in progress ui on home page * add subscription checks, more schema validation, and fix issue with complete page * dont display if no indexed repos * fix skipping onboard complete check * fix build error * add back button in onboard connection creation flow * Add back revision support (#215) * fix build * Fix bug with repository snapshot * fix share links * fix repo rm issue, 502 page, condition on test clock * Make login and onboarding mobile friendly * fix ordering of quick actions * remove error msg dump on failed repo index job, and update indexedAt field * Add mobile unsupported splash screne * cherry pick fix for file links * [Cherry Pick] Syntax reference guide (#169) (#216) * Add .env to db gitignore * fix case where we have repos but they're all failed for repo snapshot * /settings/secrets page (#217) * display domain properly in org create form * Quick action tweaks (#218) * revamp repo page (#220) * wip repo table * new repo page * add indicator for when feedback is applied in repo page * add repo button * fetch connection data in one query * fix styling * fix (#219) * remove / keyboard shortcut hint in search bar * prevent switching to first page on data update and truncate long repo names in repo list * General settings + cleanup (#221) * General settings * Add alert to org domain change * First attempt at sending logs to grafana * logs wip * add alloy logs * wip * [temp] comment out loki for now * update trial card content and add events for code host selection on onboard * reduce scraping interval to 15s * Add prometheus metric for pending repo indexing jobs * switch magic link to invite code (#222) * wip magic link codes * pipe email to email provider properly * remove magic link data cookie after sign in * clean up unused imports * dont remove cookie before we use it * rm package-lock.json * revert yarn files to v3 state * switch email passing from cookie to search param * add comment for settings dropdown auth update * remove unused middleware file * fix build error and warnings * fix build error with useSearchParam not wrapped in suspense * add sentry support to backend and webapp (#223) * add sentry to web app * set sentry environemnt from env var * add sentry env replace logic in docker container * wip add backend sentry * add sentry to backend * move dns to env var * remove test exception * Fix root domain issue on onboarding * add setup sentry cli step to github action * login to sentry * fix sentry login in action * Update grafana loki endpoint * switch source map publish to runtime in entrypoint * catch and rethrow simplegit exceptions * alloy nits * fix alloy * backend logging (#224) * revert grafana loki config * fix login ui nits * fix quick actions * fix typo in secret creation * fix private repo clone issue for gitlab * add repo index timeout logic * add posthog identify call after registeration * various changes to add terms and security info (#225) * add terms and security to footer * add security card * add demo card * fix build error * nit fix: center 'get in touch' on security card * Dark theme improvements (#226) * (fix) Fixed bug with gitlab and gitea not including hostname in the repoName * Switch to using t3-env for env-var management (#230) * Add missing env var * fix build * Centralize to using a single .env.development for development workflows (#231) * Make billing optional (#232) * Massage environment variables from strings to numbers (#234) * Single tenancy & auth modes (#233) * Add docs to this repo * dummy change * Declarative connection configuration (#235) * fix build * upgrade to next 14.2.25 * Improved database DX * migrate to yarn v4 * Use origin from header for baseUrl of emails (instead of AUTH_URL). Also removed reference to hide scrollbars * Remove SOURCEBOT_ENCRYPTION_KEY from build arg * Fix issue with linking default user to org in single tenant + no-auth mode * Fix fallback tokens (#242) * add SECURITY_CARD_ENABLED flag * Add repository weburl (#243) * Random fixes and improvements (#244) * add zoekt max wall time env var * remove empty warning in docs * fix reference in sh docs * add connection manager upsert timeout env var * Declarative connection cleanup + improvements (#245) * change contact us footer in app to point to main contact form * PostHog event pass (#246) * fix typo * Add sourcebot cloud environment prop to staging workflow * Update generated files * remove AUTH_URL since it unused and (likely) unnecessary * Revert "remove AUTH_URL since it unused and (likely) unnecessary" This reverts commit 1f4a5aed22fa94bace899262e8576427fc852f61. * cleanup GitHub action releases (#252) * remove alloy, change auth defaul to disabled, add settings page in me dropdown * enforce connection management perms to owner (#253) * enforce conneciton management perms to owner * fix formatting * more formatting * naming nits * fix var name error * change empty repo set copy if auth is disabled * add CONTRIBUTING.md file * hide settings in dropdown with auth isnt enabled * handle case where gerrit weburl is just gitiles path * Docs overhall (#251) * remove nocheckin * fix build error * remove v3 trigger from deploy staging * fix build errors round 2 * another error fix --------- Co-authored-by: msukkari <michael.sukkarieh@mail.mcgill.ca>
2025-04-01 05:34:42 +00:00
export const getConnectionInfo = async (connectionId: number, domain: string) => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const connection = await prisma.connection.findUnique({
where: {
id: connectionId,
orgId,
},
include: {
repos: true,
}
});
if (!connection) {
return notFound();
}
return {
id: connection.id,
name: connection.name,
syncStatus: connection.syncStatus,
syncStatusMetadata: connection.syncStatusMetadata,
connectionType: connection.connectionType,
updatedAt: connection.updatedAt,
syncedAt: connection.syncedAt ?? undefined,
numLinkedRepos: connection.repos.length,
}
})));
export const getRepos = async (domain: string, filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const repos = await prisma.repo.findMany({
where: {
orgId,
...(filter.status ? {
repoIndexingStatus: { in: filter.status }
} : {}),
...(filter.connectionId ? {
connections: {
some: {
connectionId: filter.connectionId
}
}
} : {}),
},
include: {
connections: {
include: {
connection: true,
}
}
}
});
return repos.map((repo) => repositoryQuerySchema.parse({
codeHostType: repo.external_codeHostType,
repoId: repo.id,
repoName: repo.name,
2025-04-03 00:50:48 +00:00
repoDisplayName: repo.displayName ?? undefined,
v3 effort (#158) * SQL Database (#157) * point zoekt to v3 branch * bump zoekt version * Add tenant ID concept into web app and backend (#160) * hacked together a example of using zoekt grpc api * provide tenant id to zoekt git indexer * update zoekt version to point to multitenant branch * pipe tenant id through header to zoekt * remove incorrect submodule reference and settings typo * update zoekt commit * remove unused yarn script * remove unused grpc client in web server * remove unneeded deps and improve tenant id log * pass tenant id when creating repo in db * add mt yarn script * add nocheckin comment to tenant id in v2 schema --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * bump zoekt version * parallelize repo indexing (#163) * hacked together a example of using zoekt grpc api * provide tenant id to zoekt git indexer * update zoekt version to point to multitenant branch * pipe tenant id through header to zoekt * remove incorrect submodule reference and settings typo * update zoekt commit * remove unused yarn script * remove unused grpc client in web server * remove unneeded deps and improve tenant id log * pass tenant id when creating repo in db * add mt yarn script * add pol of bullmq into backend * add better error handling and concurrency setting * spin up redis instance in dockerfile * cleanup transaction logic when adding repos to index queue * add NEW index status fetch condition * move bullmq deps to backend --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * Authentication (#164) * Add Org table (#167) * Move logout button & profile picture into settings dropdown (#172) * Multi tenancy support in config syncer (#171) * [wip] initial mt support in config syncer * Move logout button & profile picture into settings dropdown (#172) * update sync status properly and fix bug with multiple config in db case * make config path required in single tenant mode NOTE: deleting config/repos is currently not supported in multi tenancy case. Support for this will be added in a future PR --------- Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com> * add tenant mode support in docker container: * Organization switching & active org management (#173) * updated syncedAt date after config sync: * Migrate to postgres (#174) * spin up postgres in docker container * get initial pol of postgres db working in docker image * spin up postgres server in dev case * updated syncedAt date after config sync: * remove unnecessary port expose in docker file * Connection creation form (#175) * fix issue with yarn dev startup * init (#176) * Add `@sourcebot/schemas` package (#177) * Connection management (#178) * add concept of secrets (#180) * add @sourcebot/schemas package * migrate things to use the schemas package * Dockerfile support * add secret table to schema * Add concept of connection manager * Rename Config->Connection * Handle job failures * Add join table between repo and connection * nits * create first version of crypto package * add crypto package as deps to others * forgot to add package changes * add server action for adding and listing secrets, create test page for it * add secrets page to nav menu * add secret to config and support fetching it in backend * reset secret form on successful submission * add toast feedback for secrets form * add instructions for adding encryption key to dev instructions * add encryption key support in docker file * add delete secret button * fix nits from pr review --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * bump zoekt version * enforce tenancy on search and repo listing endpoints (#181) * enforce tenancy on search and repo listing * remove orgId from request schemas * adds garbage collection for repos (#182) * refactor repo indexing logic into RepoManager * wip cleanup stale repos * add rest of gc logic * set status to indexing properly * add initial logic for staging environment * try to move encryption key env decleration in docker file to fix build issues * switch encryption key as build arg to se if that fixes build issues * add deployment action for staging image * try using mac github action runners instead * switch to using arm64 runners on arm64 build * change workflow names to fix trigger issue * trigger staging actions to see if it works * fix working directory typo and pray it doesnt push to prod * checkout v3 when deploying staging * try to change into the staging dir manuall * dummy commit to trigger v3 workflows to test * update staging deploy script to match new version in main * reference proper image:tag in staging fly config * update staging fly config to point to ghcr * Connection management (#183) * add invite system and google oauth provider (#185) * add settings page with members list * add invite to schema and basic create form * add invite table * add basic invite link copy button * add auth invite accept case * add non auth logic * add google oauth provider * fix reference to header component in connections * add google logo to google oauth * fix web build errors * bump staging resources * change staging cpu to perf * add side bar nav in settings page * improve styling of members page * wip adding stripe checkout button * wip onboarding flow * add stripe subscription id to org * save stripe session id and add manage subscription button in settings * properly block access to pages if user isn't in an org * wip add paywall * Domain support * Domain support (#188) * Update Makefile to include crypto package when doing a make clean * Add default for AUTH_URL in attempt to fix build * attempt 2 * fix attempt #3: Do not require a encrpytion key at build time * Fix generate script race condition * Attempt #4 * add back paywall and also add support for incrememnting seat count on invite redemption * prevent self invite * action button styling in settings and toast on copy * add ability to remove member from org * move stripe product id to env var * add await for blocking loop in backend * add subscription info to billing page * handle trial case in billing info page * add trial duration indicator to nav bar * check if domain starts or ends with dash * remove unused no org component * Generate AUTH_SECRET if not provided (#189) * remove package lock file and fix prisma dep version * revert dep version updates * fix yarn.lock * add auth and membership check to fetchSubscription * properly handle invite redeem with no valid subscription case * change back fetch subscription to not require org membership * add back subscription check in invite redeem page * Add stripe billing logic (#190) * add side bar nav in settings page * improve styling of members page * wip adding stripe checkout button * wip onboarding flow * add stripe subscription id to org * save stripe session id and add manage subscription button in settings * properly block access to pages if user isn't in an org * wip add paywall * Domain support * add back paywall and also add support for incrememnting seat count on invite redemption * prevent self invite * action button styling in settings and toast on copy * add ability to remove member from org * move stripe product id to env var * add await for blocking loop in backend * add subscription info to billing page * handle trial case in billing info page * add trial duration indicator to nav bar * check if domain starts or ends with dash * remove unused no org component * remove package lock file and fix prisma dep version * revert dep version updates * fix yarn.lock * add auth and membership check to fetchSubscription * properly handle invite redeem with no valid subscription case * change back fetch subscription to not require org membership * add back subscription check in invite redeem page --------- Co-authored-by: bkellam <bshizzle1234@gmail.com> * fix nits * remove providers check * fix more nits * change stripe init to be behind function * fix publishible stripe key handling in docker container * enforce owner perms (#191) * add make owner logic, and owner perms for removal, invite, and manage subscription * add change billing email card to billing settings * enforce owner role in action level * remove unused hover card component * cleanup * add back gitlab, gitea, and gerrit support (#184) * add non github config definitions * refactor github config compilation to seperate file * add gitlab config compilation * Connection management (#183) * wip gitlab repo sync support * fix gitlab zoekt metadata * add gitea support * add gerrit support * Connection management (#183) * add gerrit config compilation * Connection management (#183) --------- Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com> * fix apos usage in redeem page * change csrf cookie to secure not host * Credentials provider (#192) * email password functionality * feedback * cleanup org's repos and shards if it's inactive (#194) * add stripe subscription status and webhook * add inactive org repo cleanup logic * mark reactivated org connections for sync * connections qol improvements (#195) * add client side polling to connections list * properly fetch repo image url * add client polling to connection management page, and add ability to sync failed connections * Fix build with suspense boundary * improved fix * add retries for 429 issues (#196) * add connection compile retry and hard repo limit * add more retry checks * cleanup unused change * address feedback * fix build errors and add index concurrency env var * add config upsert timeout env var * Membership settings rework (#198) * Add refined members list * futher progress on members settings polish * Remove old components * feedback * Magic links (#199) * wip on magic link support * Switch to nodemailer / resend for transactional mail * Further cleanup * Add stylized email using react-email * fix * Fix build * db performance improvements and job resilience (#200) * replace upsert with seperate create many and raw update many calls * add bulk repo status update and queue addition with priority * add support for managed redis * add note for changing raw sql on schema change * remove non secret token options * fix token examples in schema * add better visualization for connection/repo errors and warnings (#201) * replace upsert with seperate create many and raw update many calls * add bulk repo status update and queue addition with priority * add support for managed redis * add note for changing raw sql on schema change * add error package and use BackendException in connection manager * handle connection failure display on web app * add warning banner for not found orgs/repos/users * add failure handling for gerrit * add gitea notfound warning support * add warning icon in connections list * style nits * add failed repo vis in connections list * added retry failed repo index buttons * move nav indicators to client with polling * fix indicator flash issue and truncate large list results * display error nav better * truncate failed repo list in connection list item * fix merge error * fix merge bug * add connection util file [wip] * refactor notfound fetch logic and add missing error package to dockerfile * move repeated logic to function and add zod schema for syncStatusMetadata * add orgid unique constraint to repo * revert repo compile update logic to upsert loop * log upsert stats * [temp] disable polling everywhere (#205) * add health check endpoint * Refined onboarding flow (#202) * Redeem UX pass (#204) * add log for health check * fix new connection complete callback route * add cpu split logic and only wait for postgres if we're going to connec to it * Inline secret creation (#207) * use docker scopes to try and improve caching * Dummy change * remove cpu split logic * Add some instrumentation to web * add posthog events on various user actions (#208) * add page view event support * add posthog events * nit: remove unused import * feedback * fix merge error * use staging posthog papik when building staging image * fix other merge error and build warnings * Add invite email (#209) * wrap posthog provider in suspense to fix build error * add grafana alloy config and setup (#210) * add grafana alloy config and setup * add basic repo prom metrics * nits in dockerfile * remove invalid characters when auto filling domain * add login posthog events * remove hard coded sourcebot.app references * make repo garbage collection async (#211) * add gc queue logic * fix missing switch cases for gc status * style org create form better with new staging domain * change repo rm logic to be async * simplify repo for inactive org query * add grace period for garbage collecting repos * make prom scrape interval 500ms * fix typo in trial card * onboarding tweaks * rename some prom metrics and cleanup unused * wipe existing repo if we've picked up a killed job to ensure good state * Connections UX pass + query optimizations (#212) * remove git & local schemas (#213) * skip stripe checkout for trial + fix indexing in progress UI + additional schema validation (#214) * add additional config validation * wip bypass stripe checkout for trial * fix stripe trial checkout bypass * fix indexing in progress ui on home page * add subscription checks, more schema validation, and fix issue with complete page * dont display if no indexed repos * fix skipping onboard complete check * fix build error * add back button in onboard connection creation flow * Add back revision support (#215) * fix build * Fix bug with repository snapshot * fix share links * fix repo rm issue, 502 page, condition on test clock * Make login and onboarding mobile friendly * fix ordering of quick actions * remove error msg dump on failed repo index job, and update indexedAt field * Add mobile unsupported splash screne * cherry pick fix for file links * [Cherry Pick] Syntax reference guide (#169) (#216) * Add .env to db gitignore * fix case where we have repos but they're all failed for repo snapshot * /settings/secrets page (#217) * display domain properly in org create form * Quick action tweaks (#218) * revamp repo page (#220) * wip repo table * new repo page * add indicator for when feedback is applied in repo page * add repo button * fetch connection data in one query * fix styling * fix (#219) * remove / keyboard shortcut hint in search bar * prevent switching to first page on data update and truncate long repo names in repo list * General settings + cleanup (#221) * General settings * Add alert to org domain change * First attempt at sending logs to grafana * logs wip * add alloy logs * wip * [temp] comment out loki for now * update trial card content and add events for code host selection on onboard * reduce scraping interval to 15s * Add prometheus metric for pending repo indexing jobs * switch magic link to invite code (#222) * wip magic link codes * pipe email to email provider properly * remove magic link data cookie after sign in * clean up unused imports * dont remove cookie before we use it * rm package-lock.json * revert yarn files to v3 state * switch email passing from cookie to search param * add comment for settings dropdown auth update * remove unused middleware file * fix build error and warnings * fix build error with useSearchParam not wrapped in suspense * add sentry support to backend and webapp (#223) * add sentry to web app * set sentry environemnt from env var * add sentry env replace logic in docker container * wip add backend sentry * add sentry to backend * move dns to env var * remove test exception * Fix root domain issue on onboarding * add setup sentry cli step to github action * login to sentry * fix sentry login in action * Update grafana loki endpoint * switch source map publish to runtime in entrypoint * catch and rethrow simplegit exceptions * alloy nits * fix alloy * backend logging (#224) * revert grafana loki config * fix login ui nits * fix quick actions * fix typo in secret creation * fix private repo clone issue for gitlab * add repo index timeout logic * add posthog identify call after registeration * various changes to add terms and security info (#225) * add terms and security to footer * add security card * add demo card * fix build error * nit fix: center 'get in touch' on security card * Dark theme improvements (#226) * (fix) Fixed bug with gitlab and gitea not including hostname in the repoName * Switch to using t3-env for env-var management (#230) * Add missing env var * fix build * Centralize to using a single .env.development for development workflows (#231) * Make billing optional (#232) * Massage environment variables from strings to numbers (#234) * Single tenancy & auth modes (#233) * Add docs to this repo * dummy change * Declarative connection configuration (#235) * fix build * upgrade to next 14.2.25 * Improved database DX * migrate to yarn v4 * Use origin from header for baseUrl of emails (instead of AUTH_URL). Also removed reference to hide scrollbars * Remove SOURCEBOT_ENCRYPTION_KEY from build arg * Fix issue with linking default user to org in single tenant + no-auth mode * Fix fallback tokens (#242) * add SECURITY_CARD_ENABLED flag * Add repository weburl (#243) * Random fixes and improvements (#244) * add zoekt max wall time env var * remove empty warning in docs * fix reference in sh docs * add connection manager upsert timeout env var * Declarative connection cleanup + improvements (#245) * change contact us footer in app to point to main contact form * PostHog event pass (#246) * fix typo * Add sourcebot cloud environment prop to staging workflow * Update generated files * remove AUTH_URL since it unused and (likely) unnecessary * Revert "remove AUTH_URL since it unused and (likely) unnecessary" This reverts commit 1f4a5aed22fa94bace899262e8576427fc852f61. * cleanup GitHub action releases (#252) * remove alloy, change auth defaul to disabled, add settings page in me dropdown * enforce connection management perms to owner (#253) * enforce conneciton management perms to owner * fix formatting * more formatting * naming nits * fix var name error * change empty repo set copy if auth is disabled * add CONTRIBUTING.md file * hide settings in dropdown with auth isnt enabled * handle case where gerrit weburl is just gitiles path * Docs overhall (#251) * remove nocheckin * fix build error * remove v3 trigger from deploy staging * fix build errors round 2 * another error fix --------- Co-authored-by: msukkari <michael.sukkarieh@mail.mcgill.ca>
2025-04-01 05:34:42 +00:00
repoCloneUrl: repo.cloneUrl,
webUrl: repo.webUrl ?? undefined,
linkedConnections: repo.connections.map(({ connection }) => ({
id: connection.id,
name: connection.name,
})),
imageUrl: repo.imageUrl ?? undefined,
indexedAt: repo.indexedAt ?? undefined,
repoIndexingStatus: repo.repoIndexingStatus,
}));
}
), /* allowSingleTenantUnauthedAccess = */ true));
export const createConnection = async (name: string, type: string, connectionConfig: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const parsedConfig = parseConnectionConfig(type, connectionConfig);
if (isServiceError(parsedConfig)) {
return parsedConfig;
}
const existingConnectionWithName = await prisma.connection.findUnique({
where: {
name_orgId: {
orgId,
name,
}
}
});
if (existingConnectionWithName) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.CONNECTION_ALREADY_EXISTS,
message: "A connection with this name already exists.",
} satisfies ServiceError;
}
const connection = await prisma.connection.create({
data: {
orgId,
name,
config: parsedConfig as unknown as Prisma.InputJsonValue,
connectionType: type,
}
});
return {
id: connection.id,
}
}, OrgRole.OWNER)
));
export const updateConnectionDisplayName = async (connectionId: number, name: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const connection = await getConnection(connectionId, orgId);
if (!connection) {
return notFound();
}
const existingConnectionWithName = await prisma.connection.findUnique({
where: {
name_orgId: {
orgId,
name,
}
}
});
if (existingConnectionWithName) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.CONNECTION_ALREADY_EXISTS,
message: "A connection with this name already exists.",
} satisfies ServiceError;
}
await prisma.connection.update({
where: {
id: connectionId,
orgId,
},
data: {
name,
}
});
return {
success: true,
}
}, OrgRole.OWNER)
));
export const updateConnectionConfigAndScheduleSync = async (connectionId: number, config: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const connection = await getConnection(connectionId, orgId);
if (!connection) {
return notFound();
}
const parsedConfig = parseConnectionConfig(connection.connectionType, config);
if (isServiceError(parsedConfig)) {
return parsedConfig;
}
if (connection.syncStatus === "SYNC_NEEDED" ||
connection.syncStatus === "IN_SYNC_QUEUE" ||
connection.syncStatus === "SYNCING") {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.CONNECTION_SYNC_ALREADY_SCHEDULED,
message: "Connection is already syncing. Please wait for the sync to complete before updating the connection.",
} satisfies ServiceError;
}
await prisma.connection.update({
where: {
id: connectionId,
orgId,
},
data: {
config: parsedConfig as unknown as Prisma.InputJsonValue,
syncStatus: "SYNC_NEEDED",
}
});
return {
success: true,
}
}, OrgRole.OWNER)
));
export const flagConnectionForSync = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const connection = await getConnection(connectionId, orgId);
if (!connection || connection.orgId !== orgId) {
return notFound();
}
await prisma.connection.update({
where: {
id: connection.id,
},
data: {
syncStatus: "SYNC_NEEDED",
}
});
return {
success: true,
}
})
));
export const flagReposForIndex = async (repoIds: number[], domain: string) => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
await prisma.repo.updateMany({
where: {
id: { in: repoIds },
orgId,
},
data: {
repoIndexingStatus: RepoIndexingStatus.NEW,
}
});
return {
success: true,
}
})
));
export const deleteConnection = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const connection = await getConnection(connectionId, orgId);
if (!connection) {
return notFound();
}
await prisma.connection.delete({
where: {
id: connectionId,
orgId,
}
});
return {
success: true,
}
}, OrgRole.OWNER)
));
export const getCurrentUserRole = async (domain: string): Promise<OrgRole | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ userRole }) => {
return userRole;
})
));
export const createInvites = async (emails: string[], domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
// Check for existing invites
const existingInvites = await prisma.invite.findMany({
where: {
recipientEmail: {
in: emails
},
orgId,
}
});
if (existingInvites.length > 0) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_INVITE,
message: `A pending invite already exists for one or more of the provided emails.`,
} satisfies ServiceError;
}
// Check for members that are already in the org
const existingMembers = await prisma.userToOrg.findMany({
where: {
user: {
email: {
in: emails,
}
},
orgId,
},
});
if (existingMembers.length > 0) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_INVITE,
message: `One or more of the provided emails are already members of this org.`,
} satisfies ServiceError;
}
await prisma.invite.createMany({
data: emails.map((email) => ({
recipientEmail: email,
hostUserId: session.user.id,
orgId,
})),
skipDuplicates: true,
});
// Send invites to recipients
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS) {
const origin = (await headers()).get('origin')!;
await Promise.all(emails.map(async (email) => {
const invite = await prisma.invite.findUnique({
where: {
recipientEmail_orgId: {
recipientEmail: email,
orgId,
},
},
include: {
org: true,
}
});
if (!invite) {
return;
}
const recipient = await prisma.user.findUnique({
where: {
email,
},
});
const inviteLink = `${origin}/redeem?invite_id=${invite.id}`;
const transport = createTransport(env.SMTP_CONNECTION_URL);
const html = await render(InviteUserEmail({
baseUrl: origin,
host: {
name: session.user.name ?? undefined,
email: session.user.email!,
avatarUrl: session.user.image ?? undefined,
},
recipient: {
name: recipient?.name ?? undefined,
},
orgName: invite.org.name,
orgImageUrl: invite.org.imageUrl ?? undefined,
inviteLink,
}));
const result = await transport.sendMail({
to: email,
from: env.EMAIL_FROM_ADDRESS,
subject: `Join ${invite.org.name} on Sourcebot`,
html,
text: `Join ${invite.org.name} on Sourcebot by clicking here: ${inviteLink}`,
});
const failed = result.rejected.concat(result.pending).filter(Boolean);
if (failed.length > 0) {
console.error(`Failed to send invite email to ${email}: ${failed}`);
}
}));
}
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
export const cancelInvite = async (inviteId: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
orgId,
},
});
if (!invite) {
return notFound();
}
await prisma.invite.delete({
where: {
id: inviteId,
},
});
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
export const getMe = async () => sew(() =>
withAuth(async (session) => {
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
include: {
orgs: {
include: {
org: true,
}
},
}
});
if (!user) {
return notFound();
}
return {
id: user.id,
email: user.email,
name: user.name,
memberships: user.orgs.map((org) => ({
id: org.orgId,
role: org.role,
domain: org.org.domain,
name: org.org.name,
}))
}
}));
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth(async () => {
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
},
include: {
org: true,
}
});
if (!invite) {
return notFound();
}
const user = await getMe();
if (isServiceError(user)) {
return user;
}
// Check if the user is the recipient of the invite
if (user.email !== invite.recipientEmail) {
return notFound();
}
const res = await prisma.$transaction(async (tx) => {
if (IS_BILLING_ENABLED) {
// @note: we need to use the internal subscription fetch here since we would otherwise fail the `withOrgMembership` check.
const subscription = await _fetchSubscriptionForOrg(invite.orgId, tx);
if (isServiceError(subscription)) {
return subscription;
}
const existingSeatCount = subscription.items.data[0].quantity;
const newSeatCount = (existingSeatCount || 1) + 1
await stripeClient?.subscriptionItems.update(
subscription.items.data[0].id,
{
quantity: newSeatCount,
proration_behavior: 'create_prorations',
}
)
}
await tx.userToOrg.create({
data: {
userId: user.id,
orgId: invite.orgId,
role: "MEMBER",
}
});
await tx.invite.delete({
where: {
id: invite.id,
}
});
});
if (isServiceError(res)) {
return res;
}
return {
success: true,
}
}));
export const getInviteInfo = async (inviteId: string) => sew(() =>
withAuth(async () => {
const user = await getMe();
if (isServiceError(user)) {
return user;
}
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
},
include: {
org: true,
host: true,
}
});
if (!invite) {
return notFound();
}
if (invite.recipientEmail !== user.email) {
return notFound();
}
return {
id: invite.id,
orgName: invite.org.name,
orgImageUrl: invite.org.imageUrl ?? undefined,
orgDomain: invite.org.domain,
host: {
name: invite.host.name ?? undefined,
email: invite.host.email!,
avatarUrl: invite.host.image ?? undefined,
},
recipient: {
name: user.name ?? undefined,
email: user.email!,
}
}
}));
export const transferOwnership = async (newOwnerId: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const currentUserId = session.user.id;
if (newOwnerId === currentUserId) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "You're already the owner of this org",
} satisfies ServiceError;
}
const newOwner = await prisma.userToOrg.findUnique({
where: {
orgId_userId: {
userId: newOwnerId,
orgId,
},
},
});
if (!newOwner) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "The user you're trying to make the owner doesn't exist",
} satisfies ServiceError;
}
await prisma.$transaction([
prisma.userToOrg.update({
where: {
orgId_userId: {
userId: newOwnerId,
orgId,
},
},
data: {
role: "OWNER",
}
}),
prisma.userToOrg.update({
where: {
orgId_userId: {
userId: currentUserId,
orgId,
},
},
data: {
role: "MEMBER",
}
})
]);
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
export const createOnboardingSubscription = async (domain: string) => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const org = await prisma.org.findUnique({
where: {
id: orgId,
},
});
if (!org) {
return notFound();
}
const user = await getMe();
if (isServiceError(user)) {
return user;
}
if (!stripeClient) {
return stripeClientNotInitialized();
}
const test_clock = env.STRIPE_ENABLE_TEST_CLOCKS === 'true' ? await stripeClient.testHelpers.testClocks.create({
frozen_time: Math.floor(Date.now() / 1000)
}) : null;
// Use the existing customer if it exists, otherwise create a new one.
const customerId = await (async () => {
if (org.stripeCustomerId) {
return org.stripeCustomerId;
}
const customer = await stripeClient.customers.create({
name: org.name,
email: user.email ?? undefined,
test_clock: test_clock?.id,
description: `Created by ${user.email} on ${domain} (id: ${org.id})`,
});
await prisma.org.update({
where: {
id: org.id,
},
data: {
stripeCustomerId: customer.id,
}
});
return customer.id;
})();
const existingSubscription = await fetchSubscription(domain);
if (!isServiceError(existingSubscription)) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.SUBSCRIPTION_ALREADY_EXISTS,
message: "Attemped to create a trial subscription for an organization that already has an active subscription",
} satisfies ServiceError;
}
const prices = await stripeClient.prices.list({
product: env.STRIPE_PRODUCT_ID,
expand: ['data.product'],
});
try {
const subscription = await stripeClient.subscriptions.create({
customer: customerId,
items: [{
price: prices.data[0].id,
}],
trial_period_days: 14,
trial_settings: {
end_behavior: {
missing_payment_method: 'cancel',
},
},
payment_settings: {
save_default_payment_method: 'on_subscription',
},
});
if (!subscription) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR,
message: "Failed to create subscription",
} satisfies ServiceError;
}
return {
subscriptionId: subscription.id,
}
} catch (e) {
console.error(e);
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR,
message: "Failed to create subscription",
} satisfies ServiceError;
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
export const createStripeCheckoutSession = async (domain: string) => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const org = await prisma.org.findUnique({
where: {
id: orgId,
},
});
if (!org || !org.stripeCustomerId) {
return notFound();
}
if (!stripeClient) {
return stripeClientNotInitialized();
}
const orgMembers = await prisma.userToOrg.findMany({
where: {
orgId,
},
select: {
userId: true,
}
});
const numOrgMembers = orgMembers.length;
const origin = (await headers()).get('origin')!;
const prices = await stripeClient.prices.list({
product: env.STRIPE_PRODUCT_ID,
expand: ['data.product'],
});
const stripeSession = await stripeClient.checkout.sessions.create({
customer: org.stripeCustomerId as string,
payment_method_types: ['card'],
line_items: [
{
price: prices.data[0].id,
quantity: numOrgMembers
}
],
mode: 'subscription',
payment_method_collection: 'always',
success_url: `${origin}/${domain}/settings/billing`,
cancel_url: `${origin}/${domain}`,
});
if (!stripeSession.url) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR,
message: "Failed to create checkout session",
} satisfies ServiceError;
}
return {
url: stripeSession.url,
}
})
));
export const getCustomerPortalSessionLink = async (domain: string): Promise<string | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const org = await prisma.org.findUnique({
where: {
id: orgId,
},
});
if (!org || !org.stripeCustomerId) {
return notFound();
}
if (!stripeClient) {
return stripeClientNotInitialized();
}
const origin = (await headers()).get('origin')!;
const portalSession = await stripeClient.billingPortal.sessions.create({
customer: org.stripeCustomerId as string,
return_url: `${origin}/${domain}/settings/billing`,
});
return portalSession.url;
}, /* minRequiredRole = */ OrgRole.OWNER)
));
export const fetchSubscription = (domain: string): Promise<Stripe.Subscription | ServiceError> => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
return _fetchSubscriptionForOrg(orgId, prisma);
})
));
export const getSubscriptionBillingEmail = async (domain: string): Promise<string | ServiceError> => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const org = await prisma.org.findUnique({
where: {
id: orgId,
},
});
if (!org || !org.stripeCustomerId) {
return notFound();
}
if (!stripeClient) {
return stripeClientNotInitialized();
}
const customer = await stripeClient.customers.retrieve(org.stripeCustomerId);
if (!('email' in customer) || customer.deleted) {
return notFound();
}
return customer.email!;
})
));
export const changeSubscriptionBillingEmail = async (domain: string, newEmail: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const org = await prisma.org.findUnique({
where: {
id: orgId,
},
});
if (!org || !org.stripeCustomerId) {
return notFound();
}
if (!stripeClient) {
return stripeClientNotInitialized();
}
await stripeClient.customers.update(org.stripeCustomerId, {
email: newEmail,
});
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
export const checkIfOrgDomainExists = async (domain: string): Promise<boolean | ServiceError> => sew(() =>
withAuth(async () => {
const org = await prisma.org.findFirst({
where: {
domain,
}
});
return !!org;
}));
export const removeMemberFromOrg = async (memberId: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const targetMember = await prisma.userToOrg.findUnique({
where: {
orgId_userId: {
orgId,
userId: memberId,
}
}
});
if (!targetMember) {
return notFound();
}
const org = await prisma.org.findUnique({
where: {
id: orgId,
},
});
if (!org) {
return notFound();
}
if (IS_BILLING_ENABLED) {
const subscription = await fetchSubscription(domain);
if (isServiceError(subscription)) {
return subscription;
}
const existingSeatCount = subscription.items.data[0].quantity;
const newSeatCount = (existingSeatCount || 1) - 1;
await stripeClient?.subscriptionItems.update(
subscription.items.data[0].id,
{
quantity: newSeatCount,
proration_behavior: 'create_prorations',
}
)
}
await prisma.userToOrg.delete({
where: {
orgId_userId: {
orgId,
userId: memberId,
}
}
});
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
export const leaveOrg = async (domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId, userRole }) => {
if (userRole === OrgRole.OWNER) {
return {
statusCode: StatusCodes.FORBIDDEN,
errorCode: ErrorCode.OWNER_CANNOT_LEAVE_ORG,
message: "Organization owners cannot leave their own organization",
} satisfies ServiceError;
}
const org = await prisma.org.findUnique({
where: {
id: orgId,
},
});
if (!org) {
return notFound();
}
if (IS_BILLING_ENABLED) {
const subscription = await fetchSubscription(domain);
if (isServiceError(subscription)) {
return subscription;
}
const existingSeatCount = subscription.items.data[0].quantity;
const newSeatCount = (existingSeatCount || 1) - 1;
await stripeClient?.subscriptionItems.update(
subscription.items.data[0].id,
{
quantity: newSeatCount,
proration_behavior: 'create_prorations',
}
)
}
await prisma.userToOrg.delete({
where: {
orgId_userId: {
orgId,
userId: session.user.id,
}
}
});
return {
success: true,
}
})
));
export const getSubscriptionData = async (domain: string) => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async () => {
const subscription = await fetchSubscription(domain);
if (isServiceError(subscription)) {
return subscription;
}
if (!subscription) {
return null;
}
return {
plan: "Team",
seats: subscription.items.data[0].quantity!,
perSeatPrice: subscription.items.data[0].price.unit_amount! / 100,
nextBillingDate: subscription.current_period_end!,
status: subscription.status,
}
})
));
export const getOrgMembership = async (domain: string) => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const membership = await prisma.userToOrg.findUnique({
where: {
orgId_userId: {
orgId,
userId: session.user.id,
}
}
});
if (!membership) {
return notFound();
}
return membership;
})
));
export const getOrgMembers = async (domain: string) => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const members = await prisma.userToOrg.findMany({
where: {
orgId,
},
include: {
user: true,
},
});
return members.map((member) => ({
id: member.userId,
email: member.user.email!,
name: member.user.name ?? undefined,
avatarUrl: member.user.image ?? undefined,
role: member.role,
joinedAt: member.joinedAt,
}));
})
));
export const getOrgInvites = async (domain: string) => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const invites = await prisma.invite.findMany({
where: {
orgId,
},
});
return invites.map((invite) => ({
id: invite.id,
email: invite.recipientEmail,
createdAt: invite.createdAt,
}));
})
));
export const dismissMobileUnsupportedSplashScreen = async () => sew(async () => {
await cookies().set(MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, 'true');
return true;
});
////// Helpers ///////
const _fetchSubscriptionForOrg = async (orgId: number, prisma: Prisma.TransactionClient): Promise<Stripe.Subscription | ServiceError> => {
const org = await prisma.org.findUnique({
where: {
id: orgId,
},
});
if (!org) {
return notFound();
}
if (!org.stripeCustomerId) {
return notFound();
}
if (!stripeClient) {
return stripeClientNotInitialized();
}
const subscriptions = await stripeClient.subscriptions.list({
customer: org.stripeCustomerId
});
if (subscriptions.data.length === 0) {
return orgInvalidSubscription();
}
return subscriptions.data[0];
}
const parseConnectionConfig = (connectionType: string, config: string) => {
let parsedConfig: ConnectionConfig;
try {
parsedConfig = JSON.parse(config);
} catch (_e) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "config must be a valid JSON object."
} satisfies ServiceError;
}
const schema = (() => {
switch (connectionType) {
case "github":
return githubSchema;
case "gitlab":
return gitlabSchema;
case 'gitea':
return giteaSchema;
case 'gerrit':
return gerritSchema;
}
})();
if (!schema) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "invalid connection type",
} satisfies ServiceError;
}
const isValidConfig = ajv.validate(schema, parsedConfig);
if (!isValidConfig) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: `config schema validation failed with errors: ${ajv.errorsText(ajv.errors)}`,
} satisfies ServiceError;
}
if ('token' in parsedConfig && parsedConfig.token && 'env' in parsedConfig.token) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "Environment variables are not supported for connections created in the web UI. Please use a secret instead.",
} satisfies ServiceError;
}
const { numRepos, hasToken } = (() => {
switch (parsedConfig.type) {
case "gitea":
case "github": {
return {
numRepos: parsedConfig.repos?.length,
hasToken: !!parsedConfig.token,
}
}
case "gitlab": {
return {
numRepos: parsedConfig.projects?.length,
hasToken: !!parsedConfig.token,
}
}
case "gerrit": {
return {
numRepos: parsedConfig.projects?.length,
hasToken: true, // gerrit doesn't use a token atm
}
}
}
})();
if (!hasToken && numRepos && numRepos > env.CONFIG_MAX_REPOS_NO_TOKEN) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: `You must provide a token to sync more than ${env.CONFIG_MAX_REPOS_NO_TOKEN} repositories.`,
} satisfies ServiceError;
}
return parsedConfig;
}
export const encryptValue = async (value: string) => {
return encrypt(value);
}
export const decryptValue = async (iv: string, encryptedValue: string) => {
return decrypt(iv, encryptedValue);
}