Skip to content

Commit 1b53bb5

Browse files
committed
Refine session caching and auth middleware
1 parent 5aaec29 commit 1b53bb5

7 files changed

Lines changed: 85 additions & 57 deletions

File tree

apps/api/src/auth.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ function createAuth() {
2121
'https://sandchest.com',
2222
'https://*.sandchest.com',
2323
],
24+
session: {
25+
cookieCache: {
26+
enabled: true,
27+
maxAge: 5 * 60, // 5 minutes — avoids DB read on every getSession()
28+
},
29+
},
2430
advanced: {
2531
crossSubDomainCookies: {
2632
enabled: true,

apps/web/src/app/login/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import type { Metadata } from 'next'
2+
import { redirect } from 'next/navigation'
3+
import { getSession } from '@/lib/server-auth'
24
import AuthLayout from '@/components/AuthLayout'
35
import EmailForm from '@/components/auth/EmailForm'
46

57
export const metadata: Metadata = {
68
title: 'Log in — Sandchest',
79
}
810

9-
export default function LoginPage() {
11+
export default async function LoginPage() {
12+
const session = await getSession()
13+
if (session) redirect('/dashboard')
14+
1015
return (
1116
<AuthLayout>
1217
<EmailForm

apps/web/src/app/signup/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import type { Metadata } from 'next'
2+
import { redirect } from 'next/navigation'
3+
import { getSession } from '@/lib/server-auth'
24
import AuthLayout from '@/components/AuthLayout'
35
import EmailForm from '@/components/auth/EmailForm'
46

57
export const metadata: Metadata = {
68
title: 'Sign up — Sandchest',
79
}
810

9-
export default function SignupPage() {
11+
export default async function SignupPage() {
12+
const session = await getSession()
13+
if (session) redirect('/dashboard')
14+
1015
return (
1116
<AuthLayout>
1217
<EmailForm

apps/web/src/components/DashboardSessionProvider.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,42 @@ export default function DashboardSessionProvider({
3636
const router = useRouter()
3737

3838
// Check session validity every 5 minutes — external system sync (valid useEffect)
39+
// Pauses when tab is hidden to avoid unnecessary API+DB calls.
3940
useEffect(() => {
40-
const interval = setInterval(async () => {
41-
const { data } = await authClient.getSession()
42-
if (!data) {
43-
window.location.href = '/login'
41+
let interval: ReturnType<typeof setInterval> | null = null
42+
43+
function startPolling() {
44+
if (interval) return
45+
interval = setInterval(async () => {
46+
const { data } = await authClient.getSession()
47+
if (!data) {
48+
window.location.href = '/login'
49+
}
50+
}, 5 * 60 * 1000)
51+
}
52+
53+
function stopPolling() {
54+
if (interval) {
55+
clearInterval(interval)
56+
interval = null
57+
}
58+
}
59+
60+
function handleVisibilityChange() {
61+
if (document.hidden) {
62+
stopPolling()
63+
} else {
64+
startPolling()
4465
}
45-
}, 5 * 60 * 1000)
46-
return () => clearInterval(interval)
66+
}
67+
68+
startPolling()
69+
document.addEventListener('visibilitychange', handleVisibilityChange)
70+
71+
return () => {
72+
stopPolling()
73+
document.removeEventListener('visibilitychange', handleVisibilityChange)
74+
}
4775
}, [])
4876

4977
return (

apps/web/src/lib/server-auth.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import 'server-only'
22

3+
import { cache } from 'react'
34
import { cookies } from 'next/headers'
45
import { redirect } from 'next/navigation'
56

67
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001'
78

9+
const AUTH_COOKIE = 'better-auth.session_token'
10+
const SECURE_AUTH_COOKIE = '__Secure-better-auth.session_token'
11+
812
export interface ServerSession {
913
session: {
1014
id: string
@@ -27,6 +31,11 @@ export interface ServerOrg {
2731
slug: string
2832
}
2933

34+
async function hasSessionCookie(): Promise<boolean> {
35+
const cookieStore = await cookies()
36+
return cookieStore.has(AUTH_COOKIE) || cookieStore.has(SECURE_AUTH_COOKIE)
37+
}
38+
3039
async function authFetch<T>(path: string, init?: RequestInit): Promise<T | null> {
3140
const cookieStore = await cookies()
3241
const cookieHeader = cookieStore
@@ -48,14 +57,16 @@ async function authFetch<T>(path: string, init?: RequestInit): Promise<T | null>
4857
return res.json() as Promise<T>
4958
}
5059

51-
export async function getSession(): Promise<ServerSession | null> {
60+
export const getSession = cache(async (): Promise<ServerSession | null> => {
61+
if (!(await hasSessionCookie())) return null
5262
return authFetch<ServerSession>('/api/auth/get-session')
53-
}
63+
})
5464

55-
export async function getOrgs(): Promise<ServerOrg[]> {
65+
export const getOrgs = cache(async (): Promise<ServerOrg[]> => {
66+
if (!(await hasSessionCookie())) return []
5667
const data = await authFetch<ServerOrg[]>('/api/auth/organization/list')
5768
return data ?? []
58-
}
69+
})
5970

6071
async function setActiveOrgServer(organizationId: string): Promise<void> {
6172
await authFetch('/api/auth/organization/set-active', {

apps/web/src/middleware.test.ts

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -67,37 +67,18 @@ describe('auth middleware', () => {
6767
})
6868

6969
describe('auth pages', () => {
70-
test('redirects authenticated users from /login to /dashboard', () => {
71-
const res = middleware(makeRequest('/login', SESSION_COOKIE))
72-
expect(res.status).toBe(307)
73-
expect(new URL(res.headers.get('location')!).pathname).toBe('/dashboard')
74-
})
75-
76-
test('redirects authenticated users from /signup to /dashboard', () => {
77-
const res = middleware(makeRequest('/signup', SESSION_COOKIE))
78-
expect(res.status).toBe(307)
79-
expect(new URL(res.headers.get('location')!).pathname).toBe('/dashboard')
80-
})
81-
82-
test('redirects authenticated users from /verify to /dashboard', () => {
83-
const res = middleware(makeRequest('/verify', SESSION_COOKIE))
84-
expect(res.status).toBe(307)
85-
expect(new URL(res.headers.get('location')!).pathname).toBe('/dashboard')
86-
})
70+
test('does not redirect from auth pages — pages handle their own auth logic', () => {
71+
// Auth pages (/login, /signup, /verify) validate sessions server-side
72+
// via getSession() instead of relying on cookie existence checks.
73+
// This prevents redirect loops when cookies outlive sessions.
74+
const loginRes = middleware(makeRequest('/login', SESSION_COOKIE))
75+
expect(loginRes.status).toBe(200)
8776

88-
test('allows unauthenticated users to access /login', () => {
89-
const res = middleware(makeRequest('/login'))
90-
expect(res.status).toBe(200)
91-
})
77+
const signupRes = middleware(makeRequest('/signup', SESSION_COOKIE))
78+
expect(signupRes.status).toBe(200)
9279

93-
test('allows unauthenticated users to access /signup', () => {
94-
const res = middleware(makeRequest('/signup'))
95-
expect(res.status).toBe(200)
96-
})
97-
98-
test('allows unauthenticated users to access /verify', () => {
99-
const res = middleware(makeRequest('/verify'))
100-
expect(res.status).toBe(200)
80+
const verifyRes = middleware(makeRequest('/verify', SESSION_COOKIE))
81+
expect(verifyRes.status).toBe(200)
10182
})
10283
})
10384

@@ -110,10 +91,10 @@ describe('auth middleware', () => {
11091
expect(config.matcher).toContain('/onboarding')
11192
})
11293

113-
test('includes auth pages', () => {
114-
expect(config.matcher).toContain('/login')
115-
expect(config.matcher).toContain('/signup')
116-
expect(config.matcher).toContain('/verify')
94+
test('does not include auth pages — they handle auth server-side', () => {
95+
expect(config.matcher).not.toContain('/login')
96+
expect(config.matcher).not.toContain('/signup')
97+
expect(config.matcher).not.toContain('/verify')
11798
})
11899
})
119100
})

apps/web/src/middleware.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,29 @@ const AUTH_COOKIE = 'better-auth.session_token'
55
const SECURE_AUTH_COOKIE = '__Secure-better-auth.session_token'
66

77
const PROTECTED_PREFIXES = ['/dashboard', '/onboarding']
8-
const AUTH_PAGES = ['/login', '/signup', '/verify']
98

109
function hasSessionCookie(request: NextRequest): boolean {
1110
return request.cookies.has(AUTH_COOKIE) || request.cookies.has(SECURE_AUTH_COOKIE)
1211
}
1312

1413
export function middleware(request: NextRequest) {
1514
const { pathname } = request.nextUrl
16-
const authenticated = hasSessionCookie(request)
1715

1816
// Redirect unauthenticated users away from protected routes
1917
if (PROTECTED_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
20-
if (!authenticated) {
18+
if (!hasSessionCookie(request)) {
2119
const loginUrl = request.nextUrl.clone()
2220
loginUrl.pathname = '/login'
2321
return NextResponse.redirect(loginUrl)
2422
}
2523
}
2624

27-
// Redirect authenticated users away from auth pages
28-
if (AUTH_PAGES.some((page) => pathname === page)) {
29-
if (authenticated) {
30-
const dashboardUrl = request.nextUrl.clone()
31-
dashboardUrl.pathname = '/dashboard'
32-
return NextResponse.redirect(dashboardUrl)
33-
}
34-
}
25+
// Auth pages (/login, /signup, /verify) handle their own redirect logic
26+
// server-side via getSession() to avoid loops when cookies outlive sessions.
3527

3628
return NextResponse.next()
3729
}
3830

3931
export const config = {
40-
matcher: ['/dashboard/:path*', '/onboarding', '/login', '/signup', '/verify'],
32+
matcher: ['/dashboard/:path*', '/onboarding'],
4133
}

0 commit comments

Comments
 (0)