import { config } from "@repo/config"; import { db } from "@repo/database"; import type { Locale } from "@repo/i18n"; import { logger } from "@repo/logs"; import { sendEmail } from "@repo/mail"; import { getBaseUrl } from "@repo/utils"; import { betterAuth } from "better-auth"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import { admin, createAuthMiddleware, magicLink, openAPI, organization, } from "better-auth/plugins"; import { passkey } from "better-auth/plugins/passkey"; import { parse as parseCookies } from "cookie"; import { MongoClient } from "mongodb"; import { updateSeatsInOrganizationSubscription } from "./lib/organization"; import { getUserByEmail } from "./lib/user"; import { invitationOnlyPlugin } from "./plugins/invitation-only"; const getLocaleFromRequest = (request?: Request) => { const cookies = parseCookies(request?.headers.get("cookie") ?? ""); return ( (cookies[config.i18n.localeCookieName] as Locale) ?? config.i18n.defaultLocale ); }; const client = new MongoClient(process.env.DATABASE_URL || ""); const appUrl = getBaseUrl(); export const auth = betterAuth({ baseURL: appUrl, trustedOrigins: [appUrl], database: mongodbAdapter(client.db("dev")), session: { expiresIn: config.auth.sessionCookieMaxAge, freshAge: 0, }, account: { accountLinking: { enabled: true, trustedProviders: [ "google", ], }, }, hooks: { after: createAuthMiddleware(async (ctx) => { if (ctx.path.startsWith("/organization/accept-invitation")) { const { invitationId } = ctx.body; if (!invitationId) { return; } const invitation = await db.invitation.findUnique({ where: { id: invitationId }, }); if (!invitation) { return; } await updateSeatsInOrganizationSubscription( invitation.organizationId, ); } else if (ctx.path.startsWith("/organization/remove-member")) { const { organizationId } = ctx.body; if (!organizationId) { return; } await updateSeatsInOrganizationSubscription(organizationId); } }), }, user: { additionalFields: { onboardingComplete: { type: "boolean", required: false, }, locale: { type: "string", required: false, }, }, deleteUser: { enabled: true, }, changeEmail: { enabled: true, sendChangeEmailVerification: async ( { user: { email, name }, url }, request, ) => { const locale = getLocaleFromRequest(request); await sendEmail({ to: email, templateId: "emailVerification", context: { url, name, }, locale, }); }, }, }, emailAndPassword: { enabled: true, autoSignIn: !config.auth.enableSignup, requireEmailVerification: config.auth.enableSignup, sendResetPassword: async ({ user, url }, request) => { const locale = getLocaleFromRequest(request); await sendEmail({ to: user.email, templateId: "forgotPassword", context: { url, name: user.name, }, locale, }); }, }, emailVerification: { sendOnSignUp: config.auth.enableSignup, sendVerificationEmail: async ( { user: { email, name }, url }, request, ) => { const locale = getLocaleFromRequest(request); await sendEmail({ to: email, templateId: "emailVerification", context: { url, name, }, locale, }); }, }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID! as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET! as string, scope: ["email", "profile"], }, }, plugins: [ admin(), passkey(), magicLink({ disableSignUp: true, sendMagicLink: async ({ email, url }, request) => { const locale = getLocaleFromRequest(request); await sendEmail({ to: email, templateId: "magicLink", context: { url, }, locale, }); }, }), organization({ sendInvitationEmail: async ( { email, id, organization }, request, ) => { const locale = getLocaleFromRequest(request); const existingUser = await getUserByEmail(email); const url = new URL( existingUser ? "/auth/login" : "/auth/signup", getBaseUrl(), ); url.searchParams.set("invitationId", id); url.searchParams.set("email", email); await sendEmail({ to: email, templateId: "organizationInvitation", locale, context: { organizationName: organization.name, url: url.toString(), }, }); }, }), openAPI(), invitationOnlyPlugin(), ], onAPIError: { onError(error, ctx) { logger.error(error, { ctx }); }, }, }); export * from "./lib/organization"; export type Session = typeof auth.$Infer.Session; export type ActiveOrganization = typeof auth.$Infer.ActiveOrganization; export type Organization = typeof auth.$Infer.Organization; export type OrganizationMemberRole = typeof auth.$Infer.Member.role; export type OrganizationInvitationStatus = typeof auth.$Infer.Invitation.status; export type OrganizationMetadata = Record | undefined;