mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
fix(nextjs): break server-only import leak in auth-store
auth-store.ts directly imported fetch-session, login, and register
which transitively pulled next/headers and server-only into the client
bundle, causing the Next.js 16 build to fail.
Replaced direct imports with fetch() calls to new API route handlers
at /api/auth/{login,register,session,logout}.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
58
frontends/nextjs/src/app/api/auth/login/route.ts
Normal file
58
frontends/nextjs/src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
*
|
||||
* Authenticates a user and sets a session cookie.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { login } from '@/lib/auth/api/login'
|
||||
import { db } from '@/lib/db-client'
|
||||
import crypto from 'crypto'
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
try {
|
||||
const body = await request.json() as { identifier?: string; password?: string }
|
||||
const { identifier, password } = body
|
||||
|
||||
if (identifier === undefined || password === undefined) {
|
||||
return NextResponse.json(
|
||||
{ success: false, user: null, error: 'Identifier and password are required' },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
const result = await login(identifier, password)
|
||||
|
||||
if (!result.success || result.user === null) {
|
||||
return NextResponse.json(result, { status: 401 })
|
||||
}
|
||||
|
||||
// Create a session token and persist it
|
||||
const sessionToken = crypto.randomUUID()
|
||||
await db.sessions.create({
|
||||
id: `sess_${sessionToken}`,
|
||||
userId: result.user.id ?? '',
|
||||
token: sessionToken,
|
||||
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||
})
|
||||
|
||||
// Set session cookie
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set('session_token', sessionToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 7 * 24 * 60 * 60,
|
||||
})
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error) {
|
||||
console.error('Login route error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, user: null, error: 'Internal server error' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
33
frontends/nextjs/src/app/api/auth/logout/route.ts
Normal file
33
frontends/nextjs/src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
*
|
||||
* Clears the session cookie and invalidates the session.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { db } from '@/lib/db-client'
|
||||
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
const sessionToken = cookieStore.get('session_token')?.value
|
||||
|
||||
// Remove session from DBAL if it exists
|
||||
if (sessionToken !== undefined && sessionToken.length > 0) {
|
||||
const sessions = await db.sessions.list({ filter: { token: sessionToken } })
|
||||
const session = sessions.data[0] as { id: string } | undefined
|
||||
if (session !== undefined) {
|
||||
await db.sessions.remove(session.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear cookie
|
||||
cookieStore.delete('session_token')
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Logout route error:', error)
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
}
|
||||
62
frontends/nextjs/src/app/api/auth/register/route.ts
Normal file
62
frontends/nextjs/src/app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* POST /api/auth/register
|
||||
*
|
||||
* Creates a new user account and sets a session cookie.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { register } from '@/lib/auth/api/register'
|
||||
import { db } from '@/lib/db-client'
|
||||
import crypto from 'crypto'
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
try {
|
||||
const body = await request.json() as {
|
||||
username?: string
|
||||
email?: string
|
||||
password?: string
|
||||
}
|
||||
const { username, email, password } = body
|
||||
|
||||
if (username === undefined || email === undefined || password === undefined) {
|
||||
return NextResponse.json(
|
||||
{ success: false, user: null, error: 'Username, email, and password are required' },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
const result = await register(username, email, password)
|
||||
|
||||
if (!result.success || result.user === null) {
|
||||
return NextResponse.json(result, { status: 400 })
|
||||
}
|
||||
|
||||
// Create a session token and persist it
|
||||
const sessionToken = crypto.randomUUID()
|
||||
await db.sessions.create({
|
||||
id: `sess_${sessionToken}`,
|
||||
userId: result.user.id ?? '',
|
||||
token: sessionToken,
|
||||
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||
})
|
||||
|
||||
// Set session cookie
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set('session_token', sessionToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 7 * 24 * 60 * 60,
|
||||
})
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error) {
|
||||
console.error('Register route error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, user: null, error: 'Internal server error' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
23
frontends/nextjs/src/app/api/auth/session/route.ts
Normal file
23
frontends/nextjs/src/app/api/auth/session/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* GET /api/auth/session
|
||||
*
|
||||
* Returns the current authenticated user from the session cookie.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import { fetchSession } from '@/lib/auth/api/fetch-session'
|
||||
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
try {
|
||||
const user = await fetchSession()
|
||||
|
||||
if (user === null) {
|
||||
return NextResponse.json({ user: null }, { status: 401 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ user })
|
||||
} catch (error) {
|
||||
console.error('Session route error:', error)
|
||||
return NextResponse.json({ user: null }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
/**
|
||||
* @file auth-store.ts
|
||||
* @description Authentication state management store
|
||||
*
|
||||
* All auth operations go through API routes to avoid importing
|
||||
* server-only modules into the client bundle.
|
||||
*/
|
||||
|
||||
import { fetchSession } from '@/lib/auth/api/fetch-session'
|
||||
import { login as loginRequest } from '@/lib/auth/api/login'
|
||||
import { logout as logoutRequest } from '@/lib/auth/api/logout'
|
||||
import { register as registerRequest } from '@/lib/auth/api/register'
|
||||
|
||||
import type { User } from '@/lib/types/level-types'
|
||||
import type { AuthState } from './auth-types'
|
||||
import { mapUserToAuthUser } from './utils/map-user'
|
||||
import { BASE_PATH } from '@/lib/app-config'
|
||||
|
||||
interface AuthApiResponse {
|
||||
success: boolean
|
||||
user: User | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
export class AuthStore {
|
||||
private state: AuthState = {
|
||||
@@ -51,8 +57,15 @@ export class AuthStore {
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await loginRequest(identifier, password)
|
||||
|
||||
const response = await fetch(`${BASE_PATH}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ identifier, password }),
|
||||
})
|
||||
|
||||
const result = await response.json() as AuthApiResponse
|
||||
|
||||
if (!result.success || result.user === null) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
@@ -60,7 +73,7 @@ export class AuthStore {
|
||||
})
|
||||
throw new Error(result.error ?? 'Login failed')
|
||||
}
|
||||
|
||||
|
||||
this.setState({
|
||||
user: mapUserToAuthUser(result.user),
|
||||
isAuthenticated: true,
|
||||
@@ -82,8 +95,15 @@ export class AuthStore {
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await registerRequest(username, email, password)
|
||||
|
||||
const response = await fetch(`${BASE_PATH}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ username, email, password }),
|
||||
})
|
||||
|
||||
const result = await response.json() as AuthApiResponse
|
||||
|
||||
if (!result.success || result.user === null) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
@@ -91,7 +111,7 @@ export class AuthStore {
|
||||
})
|
||||
throw new Error(result.error ?? 'Registration failed')
|
||||
}
|
||||
|
||||
|
||||
this.setState({
|
||||
user: mapUserToAuthUser(result.user),
|
||||
isAuthenticated: true,
|
||||
@@ -108,7 +128,10 @@ export class AuthStore {
|
||||
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
await logoutRequest()
|
||||
await fetch(`${BASE_PATH}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
} finally {
|
||||
this.setState({
|
||||
user: null,
|
||||
@@ -125,10 +148,15 @@ export class AuthStore {
|
||||
})
|
||||
|
||||
try {
|
||||
const user = await fetchSession()
|
||||
if (user !== null && user !== undefined) {
|
||||
const response = await fetch(`${BASE_PATH}/api/auth/session`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
const result = await response.json() as { user: User | null }
|
||||
|
||||
if (result.user !== null && result.user !== undefined) {
|
||||
this.setState({
|
||||
user: mapUserToAuthUser(user),
|
||||
user: mapUserToAuthUser(result.user),
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user