Spaces:
Sleeping
Sleeping
| // Base auth instance without "server-only" - can be used in seed scripts | |
| import { betterAuth, type BetterAuthOptions } from "better-auth"; | |
| import { drizzleAdapter } from "better-auth/adapters/drizzle"; | |
| import { nextCookies } from "better-auth/next-js"; | |
| import { admin as adminPlugin } from "better-auth/plugins"; | |
| import { pgDb } from "lib/db/pg/db.pg"; | |
| import { headers } from "next/headers"; | |
| import { | |
| AccountTable, | |
| SessionTable, | |
| UserTable, | |
| VerificationTable, | |
| } from "lib/db/pg/schema.pg"; | |
| import { getAuthConfig } from "./config"; | |
| import logger from "logger"; | |
| import { userRepository } from "lib/db/repository"; | |
| import { DEFAULT_USER_ROLE, USER_ROLES } from "app-types/roles"; | |
| import { admin, editor, user, ac } from "./roles"; | |
| const { | |
| emailAndPasswordEnabled, | |
| signUpEnabled, | |
| socialAuthenticationProviders, | |
| } = getAuthConfig(); | |
| const options = { | |
| secret: process.env.BETTER_AUTH_SECRET!, | |
| plugins: [ | |
| adminPlugin({ | |
| defaultRole: DEFAULT_USER_ROLE, | |
| adminRoles: [USER_ROLES.ADMIN], | |
| ac, | |
| roles: { | |
| admin, | |
| editor, | |
| user, | |
| }, | |
| }), | |
| nextCookies(), | |
| ], | |
| baseURL: process.env.BETTER_AUTH_URL || process.env.NEXT_PUBLIC_BASE_URL, | |
| user: { | |
| changeEmail: { | |
| enabled: true, | |
| }, | |
| deleteUser: { | |
| enabled: true, | |
| }, | |
| }, | |
| database: drizzleAdapter(pgDb, { | |
| provider: "pg", | |
| schema: { | |
| user: UserTable, | |
| session: SessionTable, | |
| account: AccountTable, | |
| verification: VerificationTable, | |
| }, | |
| }), | |
| databaseHooks: { | |
| user: { | |
| create: { | |
| before: async (user) => { | |
| // This hook ONLY runs during user creation (sign-up), not on sign-in | |
| // Use our optimized getIsFirstUser function with caching | |
| const isFirstUser = await getIsFirstUser(); | |
| // Set role based on whether this is the first user | |
| const role = isFirstUser ? USER_ROLES.ADMIN : DEFAULT_USER_ROLE; | |
| logger.info( | |
| `User creation hook: ${user.email} will get role: ${role} (isFirstUser: ${isFirstUser})`, | |
| ); | |
| return { | |
| data: { | |
| ...user, | |
| role, | |
| }, | |
| }; | |
| }, | |
| }, | |
| }, | |
| }, | |
| emailAndPassword: { | |
| enabled: emailAndPasswordEnabled, | |
| disableSignUp: !signUpEnabled, | |
| }, | |
| session: { | |
| cookieCache: { | |
| enabled: true, | |
| maxAge: 60 * 60, | |
| }, | |
| expiresIn: 60 * 60 * 24 * 7, // 7 days | |
| updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated) | |
| }, | |
| advanced: { | |
| useSecureCookies: | |
| process.env.NO_HTTPS == "1" | |
| ? false | |
| : process.env.NODE_ENV === "production", | |
| database: { | |
| generateId: false, | |
| }, | |
| }, | |
| account: { | |
| accountLinking: { | |
| trustedProviders: ( | |
| Object.keys( | |
| socialAuthenticationProviders, | |
| ) as (keyof typeof socialAuthenticationProviders)[] | |
| ).filter((key) => socialAuthenticationProviders[key]), | |
| }, | |
| }, | |
| socialProviders: socialAuthenticationProviders, | |
| } satisfies BetterAuthOptions; | |
| export const auth = betterAuth({ | |
| ...options, | |
| plugins: [...(options.plugins ?? [])], | |
| }); | |
| export const getSession = async () => { | |
| const reqHeaders = await headers(); | |
| try { | |
| const session = await auth.api.getSession({ | |
| headers: reqHeaders, | |
| }); | |
| return session ?? null; | |
| } catch (error) { | |
| logger.error("Error getting session:", error); | |
| return null; | |
| } | |
| }; | |
| // Cache the first user check to avoid repeated DB queries | |
| let isFirstUserCache: boolean | null = null; | |
| export const getIsFirstUser = async () => { | |
| // If we already know there's at least one user, return false immediately | |
| // This in-memory cache prevents any DB calls once we know users exist | |
| if (isFirstUserCache === false) { | |
| return false; | |
| } | |
| try { | |
| // Direct database query - simple and reliable | |
| const userCount = await userRepository.getUserCount(); | |
| const isFirstUser = userCount === 0; | |
| // Once we have at least one user, cache it permanently in memory | |
| if (!isFirstUser) { | |
| isFirstUserCache = false; | |
| } | |
| return isFirstUser; | |
| } catch (error) { | |
| logger.error("Error checking if first user:", error); | |
| // Cache as false on error to prevent repeated attempts | |
| isFirstUserCache = false; | |
| return false; | |
| } | |
| }; | |