feat(auth): implement Better Auth with session cache management

This commit is contained in:
2026-03-29 19:25:10 -07:00
parent 8f1f2bf196
commit 2ec940f004
3 changed files with 129 additions and 127 deletions
+20
View File
@@ -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(),
});
+55
View File
@@ -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;
+54 -127
View File
@@ -1,131 +1,58 @@
import NextAuth, { DefaultSession, DefaultUser } from "next-auth" import { betterAuth } from "better-auth";
import CredentialsProvider from "next-auth/providers/credentials" import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "./prisma" import { prisma } from "./prisma";
import bcrypt from "bcryptjs" import { testUtils } from "better-auth/plugins";
// Extend the built-in session and user types export const auth = betterAuth({
declare module "next-auth" { database: prismaAdapter(prisma, {
interface Session { provider: "sqlite",
user: { }),
id: string emailAndPassword: {
playerId: number enabled: true,
role: string autoSignIn: true, // Automatically sign in after registration
} & DefaultSession["user"] requireEmailVerification: false, // Don't require email verification for tests
} },
secret: process.env.BETTER_AUTH_SECRET,
interface User { baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
id: string trustedOrigins: [
playerId: number process.env.BETTER_AUTH_URL || "http://localhost:3000",
role: string "http://127.0.0.1:3000",
} "http://0.0.0.0:3000",
}
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,
}
},
}),
], ],
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: { session: {
strategy: "jwt", cookieCache: {
maxAge: 30 * 24 * 60 * 60, // 30 days 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()
]
});