sourcebot/packages/web/src/ee/sso/sso.tsx

169 lines
5.3 KiB
TypeScript
Raw Normal View History

V4 (#311) Sourcebot V4 introduces authentication, performance improvements and code navigation. Checkout the [migration guide](https://docs.sourcebot.dev/self-hosting/upgrade/v3-to-v4-guide) for information on upgrading your instance to v4. ### Changed - [**Breaking Change**] Authentication is now required by default. Notes: - When setting up your instance, email / password login will be the default authentication provider. - The first user that logs into the instance is given the `owner` role. ([docs](https://docs.sourcebot.dev/docs/more/roles-and-permissions)). - Subsequent users can request to join the instance. The `owner` can approve / deny requests to join the instance via `Settings` > `Members` > `Pending Requests`. - If a user is approved to join the instance, they are given the `member` role. - Additional login providers, including email links and SSO, can be configured with additional environment variables. ([docs](https://docs.sourcebot.dev/self-hosting/configuration/authentication)). - Clicking on a search result now takes you to the `/browse` view. Files can still be previewed by clicking the "Preview" button or holding `Cmd` / `Ctrl` when clicking on a search result. [#315](https://github.com/sourcebot-dev/sourcebot/pull/315) ### Added - [Sourcebot EE] Added search-based code navigation, allowing you to jump between symbol definition and references when viewing source files. [Read the documentation](https://docs.sourcebot.dev/docs/search/code-navigation). [#315](https://github.com/sourcebot-dev/sourcebot/pull/315) - Added collapsible filter panel. [#315](https://github.com/sourcebot-dev/sourcebot/pull/315) ### Fixed - Improved scroll performance for large numbers of search results. [#315](https://github.com/sourcebot-dev/sourcebot/pull/315)
2025-05-28 23:08:42 +00:00
import type { Provider } from "next-auth/providers";
import { env } from "@/env.mjs";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import Okta from "next-auth/providers/okta";
import Keycloak from "next-auth/providers/keycloak";
import Gitlab from "next-auth/providers/gitlab";
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id";
import { prisma } from "@/prisma";
import { notFound, ServiceError } from "@/lib/serviceError";
import { OrgRole } from "@sourcebot/db";
import { getSeats, SOURCEBOT_UNLIMITED_SEATS } from "@/features/entitlements/server";
import { StatusCodes } from "http-status-codes";
import { ErrorCode } from "@/lib/errorCodes";
import { sew } from "@/actions";
export const getSSOProviders = (): Provider[] => {
const providers: Provider[] = [];
if (env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) {
const baseUrl = env.AUTH_EE_GITHUB_BASE_URL ?? "https://github.com";
const apiUrl = env.AUTH_EE_GITHUB_BASE_URL ? `${env.AUTH_EE_GITHUB_BASE_URL}/api/v3` : "https://api.github.com";
providers.push(GitHub({
clientId: env.AUTH_EE_GITHUB_CLIENT_ID,
clientSecret: env.AUTH_EE_GITHUB_CLIENT_SECRET,
authorization: {
url: `${baseUrl}/login/oauth/authorize`,
params: {
scope: "read:user user:email",
},
},
token: {
url: `${baseUrl}/login/oauth/access_token`,
},
userinfo: {
url: `${apiUrl}/user`,
},
}));
}
if (env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) {
providers.push(Gitlab({
clientId: env.AUTH_EE_GITLAB_CLIENT_ID,
clientSecret: env.AUTH_EE_GITLAB_CLIENT_SECRET,
authorization: {
url: `${env.AUTH_EE_GITLAB_BASE_URL}/oauth/authorize`,
params: {
scope: "read_user",
},
},
token: {
url: `${env.AUTH_EE_GITLAB_BASE_URL}/oauth/token`,
},
userinfo: {
url: `${env.AUTH_EE_GITLAB_BASE_URL}/api/v4/user`,
},
}));
}
if (env.AUTH_EE_GOOGLE_CLIENT_ID && env.AUTH_EE_GOOGLE_CLIENT_SECRET) {
providers.push(Google({
clientId: env.AUTH_EE_GOOGLE_CLIENT_ID,
clientSecret: env.AUTH_EE_GOOGLE_CLIENT_SECRET,
}));
}
if (env.AUTH_EE_OKTA_CLIENT_ID && env.AUTH_EE_OKTA_CLIENT_SECRET && env.AUTH_EE_OKTA_ISSUER) {
providers.push(Okta({
clientId: env.AUTH_EE_OKTA_CLIENT_ID,
clientSecret: env.AUTH_EE_OKTA_CLIENT_SECRET,
issuer: env.AUTH_EE_OKTA_ISSUER,
}));
}
if (env.AUTH_EE_KEYCLOAK_CLIENT_ID && env.AUTH_EE_KEYCLOAK_CLIENT_SECRET && env.AUTH_EE_KEYCLOAK_ISSUER) {
providers.push(Keycloak({
clientId: env.AUTH_EE_KEYCLOAK_CLIENT_ID,
clientSecret: env.AUTH_EE_KEYCLOAK_CLIENT_SECRET,
issuer: env.AUTH_EE_KEYCLOAK_ISSUER,
}));
}
if (env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID && env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET && env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER) {
providers.push(MicrosoftEntraID({
clientId: env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID,
clientSecret: env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET,
issuer: env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER,
}));
}
return providers;
}
export const handleJITProvisioning = async (userId: string, domain: string): Promise<ServiceError | boolean> => sew(async () => {
const org = await prisma.org.findUnique({
where: {
domain,
},
include: {
members: {
where: {
role: {
not: OrgRole.GUEST,
}
}
}
}
});
if (!org) {
return notFound(`Org ${domain} not found`);
}
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (!user) {
return notFound(`User ${userId} not found`);
}
const userToOrg = await prisma.userToOrg.findFirst({
where: {
userId,
orgId: org.id,
}
});
if (userToOrg) {
console.warn(`JIT provisioning skipped for user ${userId} since they're already a member of org ${domain}`);
return true;
}
const seats = await getSeats();
const memberCount = org.members.length;
if (seats != SOURCEBOT_UNLIMITED_SEATS && memberCount >= seats) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.ORG_SEAT_COUNT_REACHED,
message: "Failed to provision user since the organization is at max capacity",
} satisfies ServiceError;
}
await prisma.$transaction(async (tx) => {
await tx.user.update({
where: {
id: userId,
},
data: {
pendingApproval: false,
},
});
await tx.userToOrg.create({
data: {
userId,
orgId: org.id,
role: OrgRole.MEMBER,
},
});
});
return true;
});