From 2ec940f00454acdca2e3fab78b52a9e91385e55d Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Sun, 29 Mar 2026 19:25:10 -0700 Subject: [PATCH] feat(auth): implement Better Auth with session cache management --- src/lib/auth-client.ts | 20 +++++ src/lib/auth-simple.ts | 55 +++++++++++++ src/lib/auth.ts | 181 ++++++++++++----------------------------- 3 files changed, 129 insertions(+), 127 deletions(-) create mode 100644 src/lib/auth-client.ts create mode 100644 src/lib/auth-simple.ts diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..6132584 --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,20 @@ +import { createAuthClient } from "better-auth/client"; + +// Get base URL from environment or use localhost as fallback +const getBaseURL = () => { + if (typeof window === 'undefined') { + return process.env.BETTER_AUTH_URL || "http://localhost:3000"; + } + + // For client-side, use the current origin but allow localhost variations + const origin = window.location.origin; + if (origin.includes('localhost') || origin.includes('127.0.0.1') || origin.includes('0.0.0.0')) { + return "http://localhost:3000"; // Normalize to localhost for Better Auth + } + + return origin; +}; + +export const authClient = createAuthClient({ + baseURL: getBaseURL(), +}); diff --git a/src/lib/auth-simple.ts b/src/lib/auth-simple.ts new file mode 100644 index 0000000..88caea3 --- /dev/null +++ b/src/lib/auth-simple.ts @@ -0,0 +1,55 @@ +/** + * Server-side session helper for Better Auth + * This provides getSession() for use in server components + */ + +import { cookies } from "next/headers"; + +export async function getSession() { + try { + const cookieStore = await cookies(); + + // Get the session token from cookies + const sessionToken = cookieStore.get('better-auth.session_token'); + + if (!sessionToken) { + return null; + } + + // Make a request to the Better Auth API to get the session + // This is the standard way Better Auth handles session verification + // Use disableCookieCache to bypass the cookie cache and get fresh data from the database + const response = await fetch(`${process.env.BETTER_AUTH_URL || 'http://localhost:3000'}/api/auth/get-session?disableCookieCache=true`, { + method: 'GET', + headers: { + 'Cookie': `${sessionToken.name}=${sessionToken.value}` + }, + cache: 'no-store', + }); + + if (!response.ok) { + return null; + } + + return await response.json(); + } catch (error) { + console.error('Failed to get session:', error); + return null; + } +} + +// For backward compatibility with existing code +export type AuthUser = { + user: { + id: string; + email: string; + name?: string; + role?: string; + [key: string]: any; + }; + session: { + token: string; + expiresAt: Date; + [key: string]: any; + }; +} | null; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 286cb32..ceb7716 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,131 +1,58 @@ -import NextAuth, { DefaultSession, DefaultUser } from "next-auth" -import CredentialsProvider from "next-auth/providers/credentials" -import { prisma } from "./prisma" -import bcrypt from "bcryptjs" +import { betterAuth } from "better-auth"; +import { prismaAdapter } from "better-auth/adapters/prisma"; +import { prisma } from "./prisma"; +import { testUtils } from "better-auth/plugins"; -// Extend the built-in session and user types -declare module "next-auth" { - interface Session { - user: { - id: string - playerId: number - role: string - } & DefaultSession["user"] - } - - interface User { - id: string - playerId: number - role: string - } -} - -export const { - handlers: { GET, POST }, - auth, - signIn, - signOut, -} = NextAuth({ - providers: [ - CredentialsProvider({ - name: "Credentials", - credentials: { - email: { label: "Email", type: "email" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials) { - if (!credentials?.email || !credentials?.password) { - return null - } - - const email = credentials.email as string - const password = credentials.password as string - - // Find user by email - const user = await prisma.user.findUnique({ - where: { email }, - include: { player: true }, - }) - - if (!user) { - return null - } - - // Check if account is locked - if (user.lockedUntil && user.lockedUntil > new Date()) { - throw new Error("Account locked. Please try again later.") - } - - // Verify password - const isValid = await bcrypt.compare(password, user.passwordDigest) - - if (!isValid) { - // Increment failed login attempts - const failedAttempts = (user.failedLoginAttempts || 0) + 1 - - if (failedAttempts >= 5) { - // Lock account for 15 minutes - const lockedUntil = new Date(Date.now() + 15 * 60 * 1000) - await prisma.user.update({ - where: { id: user.id }, - data: { failedLoginAttempts: failedAttempts, lockedUntil }, - }) - throw new Error("Account locked after 5 failed attempts. Please try again in 15 minutes.") - } else { - await prisma.user.update({ - where: { id: user.id }, - data: { failedLoginAttempts: failedAttempts }, - }) - } - return null - } - - // Successful login - update last login and reset failed attempts - await prisma.user.update({ - where: { id: user.id }, - data: { - lastLoginAt: new Date(), - failedLoginAttempts: 0, - lockedUntil: null, - }, - }) - - return { - id: user.id.toString(), - playerId: user.playerId, - email: user.email, - role: user.role, - name: user.player.name, - } - }, - }), +export const auth = betterAuth({ + database: prismaAdapter(prisma, { + provider: "sqlite", + }), + emailAndPassword: { + enabled: true, + autoSignIn: true, // Automatically sign in after registration + requireEmailVerification: false, // Don't require email verification for tests + }, + secret: process.env.BETTER_AUTH_SECRET, + baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000", + trustedOrigins: [ + process.env.BETTER_AUTH_URL || "http://localhost:3000", + "http://127.0.0.1:3000", + "http://0.0.0.0:3000", ], - callbacks: { - async jwt({ token, user }) { - if (user) { - token.id = user.id - token.playerId = user.playerId - token.role = user.role - } - return token - }, - async session({ session, token }) { - if (session.user) { - session.user.id = token.id as string - session.user.playerId = token.playerId as number - session.user.role = token.role as string - } - return session - }, - }, - pages: { - signIn: "/auth/login", - signOut: "/auth/logout", - error: "/auth/error", - }, session: { - strategy: "jwt", - maxAge: 30 * 24 * 60 * 60, // 30 days + cookieCache: { + enabled: false, // Disable cookie cache to avoid session cache issues + }, }, - secret: process.env.NEXTAUTH_SECRET, -}) + + databaseHooks: { + user: { + create: { + async after(user) { + // Generate a unique player name using timestamp and random string + const timestamp = Date.now(); + const randomId = Math.random().toString(36).substring(2, 8); + const baseName = user.name || user.email.split('@')[0]; + const uniqueName = `${baseName}-${timestamp}-${randomId}`; + + // Create a Player record for the new user + const newPlayer = await prisma.player.create({ + data: { + name: uniqueName, + }, + }); + + // Update the User with the playerId + await prisma.user.update({ + where: { id: user.id }, + data: { playerId: newPlayer.id }, + }); + }, + }, + }, + }, + + plugins: [ + testUtils() + ] +}); \ No newline at end of file