mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-16 14:25:22 +00:00
## Problem If a repository is added **after** a search context (e.g., a new repository is synced from the code host), then it will never be added to the context even if it should be included. The workaround is to restart the instance. ## Solution This PR adds a call to re-sync all search contexts whenever a connection is successfully synced. This PR adds the `@sourcebot/shared` package that contains `syncSearchContexts.ts` (previously in web) and it's dependencies (namely the entitlements system). ## Why another package? Because the `syncSearchContexts` call is now called from: 1. `initialize.ts` in **web** - handles syncing search contexts on startup and whenever the config is modified in watch mode. This is the same as before. 2. `connectionManager.ts` in **backend** - syncs the search contexts whenever a connection is successfully synced. ## Follow-up devex work Two things: 1. We have several very thin shared packages (i.e., `crypto`, `error`, and `logger`) that we can probably fold into this "general" shared package. `schemas` and `db` _feels_ like they should remain separate (mostly because they are "code-gen" packages). 2. When running `yarn dev`, any changes made to the shared package will only get picked if you `ctrl+c` and restart the instance. Would be nice if we have watch mode work across package dependencies in the monorepo.
168 lines
5.5 KiB
TypeScript
168 lines
5.5 KiB
TypeScript
import 'next-auth/jwt';
|
|
import NextAuth, { DefaultSession, User as AuthJsUser } from "next-auth"
|
|
import Credentials from "next-auth/providers/credentials"
|
|
import EmailProvider from "next-auth/providers/nodemailer";
|
|
import { PrismaAdapter } from "@auth/prisma-adapter"
|
|
import { prisma } from "@/prisma";
|
|
import { env } from "@/env.mjs";
|
|
import { User } from '@sourcebot/db';
|
|
import 'next-auth/jwt';
|
|
import type { Provider } from "next-auth/providers";
|
|
import { verifyCredentialsRequestSchema } from './lib/schemas';
|
|
import { createTransport } from 'nodemailer';
|
|
import { render } from '@react-email/render';
|
|
import MagicLinkEmail from './emails/magicLinkEmail';
|
|
import bcrypt from 'bcryptjs';
|
|
import { getSSOProviders } from '@/ee/sso/sso';
|
|
import { hasEntitlement } from '@sourcebot/shared';
|
|
import { onCreateUser } from '@/lib/authUtils';
|
|
|
|
export const runtime = 'nodejs';
|
|
|
|
declare module 'next-auth' {
|
|
interface Session {
|
|
user: {
|
|
id: string;
|
|
} & DefaultSession['user'];
|
|
}
|
|
}
|
|
|
|
declare module 'next-auth/jwt' {
|
|
interface JWT {
|
|
userId: string
|
|
}
|
|
}
|
|
|
|
export const getProviders = () => {
|
|
const providers: Provider[] = [];
|
|
|
|
if (hasEntitlement("sso")) {
|
|
providers.push(...getSSOProviders());
|
|
}
|
|
|
|
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS && env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true') {
|
|
providers.push(EmailProvider({
|
|
server: env.SMTP_CONNECTION_URL,
|
|
from: env.EMAIL_FROM_ADDRESS,
|
|
maxAge: 60 * 10,
|
|
generateVerificationToken: async () => {
|
|
const token = String(Math.floor(100000 + Math.random() * 900000));
|
|
return token;
|
|
},
|
|
sendVerificationRequest: async ({ identifier, provider, token }) => {
|
|
const transport = createTransport(provider.server);
|
|
const html = await render(MagicLinkEmail({ token: token }));
|
|
const result = await transport.sendMail({
|
|
to: identifier,
|
|
from: provider.from,
|
|
subject: 'Log in to Sourcebot',
|
|
html,
|
|
text: `Log in to Sourcebot using this code: ${token}`
|
|
});
|
|
|
|
const failed = result.rejected.concat(result.pending).filter(Boolean);
|
|
if (failed.length) {
|
|
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
if (env.AUTH_CREDENTIALS_LOGIN_ENABLED === 'true') {
|
|
providers.push(Credentials({
|
|
credentials: {
|
|
email: {},
|
|
password: {}
|
|
},
|
|
type: "credentials",
|
|
authorize: async (credentials) => {
|
|
const body = verifyCredentialsRequestSchema.safeParse(credentials);
|
|
if (!body.success) {
|
|
return null;
|
|
}
|
|
const { email, password } = body.data;
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { email }
|
|
});
|
|
|
|
// The user doesn't exist, so create a new one.
|
|
if (!user) {
|
|
const hashedPassword = bcrypt.hashSync(password, 10);
|
|
const newUser = await prisma.user.create({
|
|
data: {
|
|
email,
|
|
hashedPassword,
|
|
}
|
|
});
|
|
|
|
const authJsUser: AuthJsUser = {
|
|
id: newUser.id,
|
|
email: newUser.email,
|
|
}
|
|
|
|
onCreateUser({ user: authJsUser });
|
|
return authJsUser;
|
|
|
|
// Otherwise, the user exists, so verify the password.
|
|
} else {
|
|
if (!user.hashedPassword) {
|
|
return null;
|
|
}
|
|
|
|
if (!bcrypt.compareSync(password, user.hashedPassword)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name ?? undefined,
|
|
image: user.image ?? undefined,
|
|
};
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
return providers;
|
|
}
|
|
|
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
secret: env.AUTH_SECRET,
|
|
adapter: PrismaAdapter(prisma),
|
|
session: {
|
|
strategy: "jwt",
|
|
},
|
|
trustHost: true,
|
|
events: {
|
|
createUser: onCreateUser,
|
|
},
|
|
callbacks: {
|
|
async jwt({ token, user: _user }) {
|
|
const user = _user as User | undefined;
|
|
// @note: `user` will be available on signUp or signIn triggers.
|
|
// Cache the userId in the JWT for later use.
|
|
if (user) {
|
|
token.userId = user.id;
|
|
}
|
|
return token;
|
|
},
|
|
async session({ session, token }) {
|
|
// @WARNING: Anything stored in the session will be sent over
|
|
// to the client.
|
|
session.user = {
|
|
...session.user,
|
|
// Propogate the userId to the session.
|
|
id: token.userId,
|
|
}
|
|
return session;
|
|
},
|
|
},
|
|
providers: getProviders(),
|
|
pages: {
|
|
signIn: "/login",
|
|
// We set redirect to false in signInOptions so we can pass the email is as a param
|
|
// verifyRequest: "/login/verify",
|
|
}
|
|
});
|