fix:maybe

This commit is contained in:
DeathKaioken 2026-01-18 21:37:18 +01:00
parent 2fed9b4b8b
commit 96cfc90e17
6 changed files with 189 additions and 13 deletions

View File

@ -6,9 +6,8 @@
*/ */
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
// Move accessToken to HttpOnly cookie in future for better security
// Backend sets 'refreshToken' cookie on login; use it as auth presence // Backend sets 'refreshToken' cookie on login; use it as auth presence
const AUTH_COOKIES = ['refreshToken'] const AUTH_COOKIES = ['refreshToken', '__Secure-refreshToken']
export function middleware(req: NextRequest) { export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl const { pathname } = req.nextUrl

View File

@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from 'next/server'
export const runtime = 'nodejs'
function getSharedCookieDomain(req: NextRequest): string | undefined {
const host = req.headers.get('host') ?? ''
return host.endsWith('profit-planet.partners') ? '.profit-planet.partners' : undefined
}
function readSetCookies(res: Response): string[] {
const anyHeaders = res.headers as any
if (typeof anyHeaders.getSetCookie === 'function') return anyHeaders.getSetCookie()
const single = res.headers.get('set-cookie')
return single ? [single] : []
}
function parseRefreshCookie(setCookie: string) {
// refreshToken=VALUE; Max-Age=...; Expires=...; SameSite=...; ...
const m = setCookie.match(/^refreshToken=([^;]*)/i)
if (!m) return null
const value = m[1] ?? ''
const maxAgeMatch = setCookie.match(/;\s*max-age=(\d+)/i)
const expiresMatch = setCookie.match(/;\s*expires=([^;]+)/i)
const sameSiteMatch = setCookie.match(/;\s*samesite=(lax|strict|none)/i)
return {
value,
maxAge: maxAgeMatch ? Number(maxAgeMatch[1]) : undefined,
expires: expiresMatch ? new Date(expiresMatch[1]) : undefined,
sameSite: (sameSiteMatch?.[1]?.toLowerCase() as 'lax' | 'strict' | 'none' | undefined) ?? undefined,
}
}
export async function POST(req: NextRequest) {
const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL
if (!apiBase) return NextResponse.json({ message: 'Missing NEXT_PUBLIC_API_BASE_URL' }, { status: 500 })
const body = await req.json().catch(() => null)
const apiRes = await fetch(`${apiBase}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body ?? {}),
cache: 'no-store',
})
const data = await apiRes.json().catch(() => null)
const out = NextResponse.json(data, { status: apiRes.status })
const setCookies = readSetCookies(apiRes)
const refreshSetCookie = setCookies.find((c) => /^refreshToken=/i.test(c))
if (refreshSetCookie) {
const parsed = parseRefreshCookie(refreshSetCookie)
if (parsed) {
out.cookies.set('refreshToken', parsed.value, {
domain: getSharedCookieDomain(req),
path: '/',
httpOnly: true,
secure: true,
sameSite: parsed.sameSite ?? 'lax',
...(parsed.maxAge !== undefined ? { maxAge: parsed.maxAge } : {}),
...(parsed.maxAge === undefined && parsed.expires ? { expires: parsed.expires } : {}),
})
}
}
return out
}

View File

@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from 'next/server'
export const runtime = 'nodejs'
function getSharedCookieDomain(req: NextRequest): string | undefined {
const host = req.headers.get('host') ?? ''
return host.endsWith('profit-planet.partners') ? '.profit-planet.partners' : undefined
}
export async function POST(req: NextRequest) {
const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL
const cookie = req.headers.get('cookie') ?? ''
// Best-effort: tell backend to revoke refresh token (if endpoint exists)
let data: any = { success: true }
let status = 200
if (apiBase) {
const apiRes = await fetch(`${apiBase}/api/logout`, {
method: 'POST',
headers: { cookie, 'Content-Type': 'application/json' },
cache: 'no-store',
}).catch(() => null)
if (apiRes) {
status = apiRes.status
data = await apiRes.json().catch(() => data)
}
}
const out = NextResponse.json(data, { status })
out.cookies.set('refreshToken', '', {
domain: getSharedCookieDomain(req),
path: '/',
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 0,
})
return out
}

View File

@ -0,0 +1,67 @@
import { NextRequest, NextResponse } from 'next/server'
export const runtime = 'nodejs'
function getSharedCookieDomain(req: NextRequest): string | undefined {
const host = req.headers.get('host') ?? ''
return host.endsWith('profit-planet.partners') ? '.profit-planet.partners' : undefined
}
function readSetCookies(res: Response): string[] {
const anyHeaders = res.headers as any
if (typeof anyHeaders.getSetCookie === 'function') return anyHeaders.getSetCookie()
const single = res.headers.get('set-cookie')
return single ? [single] : []
}
function parseRefreshCookie(setCookie: string) {
const m = setCookie.match(/^refreshToken=([^;]*)/i)
if (!m) return null
const value = m[1] ?? ''
const maxAgeMatch = setCookie.match(/;\s*max-age=(\d+)/i)
const expiresMatch = setCookie.match(/;\s*expires=([^;]+)/i)
const sameSiteMatch = setCookie.match(/;\s*samesite=(lax|strict|none)/i)
return {
value,
maxAge: maxAgeMatch ? Number(maxAgeMatch[1]) : undefined,
expires: expiresMatch ? new Date(expiresMatch[1]) : undefined,
sameSite: (sameSiteMatch?.[1]?.toLowerCase() as 'lax' | 'strict' | 'none' | undefined) ?? undefined,
}
}
export async function POST(req: NextRequest) {
const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL
if (!apiBase) return NextResponse.json({ message: 'Missing NEXT_PUBLIC_API_BASE_URL' }, { status: 500 })
const cookie = req.headers.get('cookie') ?? ''
const apiRes = await fetch(`${apiBase}/api/refresh`, {
method: 'POST',
headers: { cookie },
cache: 'no-store',
})
const data = await apiRes.json().catch(() => null)
const out = NextResponse.json(data, { status: apiRes.status })
const setCookies = readSetCookies(apiRes)
const refreshSetCookie = setCookies.find((c) => /^refreshToken=/i.test(c))
if (refreshSetCookie) {
const parsed = parseRefreshCookie(refreshSetCookie)
if (parsed) {
out.cookies.set('refreshToken', parsed.value, {
domain: getSharedCookieDomain(req),
path: '/',
httpOnly: true,
secure: true,
sameSite: parsed.sameSite ?? 'lax',
...(parsed.maxAge !== undefined ? { maxAge: parsed.maxAge } : {}),
...(parsed.maxAge === undefined && parsed.expires ? { expires: parsed.expires } : {}),
})
}
}
return out
}

View File

@ -28,16 +28,16 @@ export function useLogin() {
rememberMe: credentials.rememberMe rememberMe: credentials.rememberMe
}) })
// Make actual API call to backend // Call same-origin BFF route so it can set Domain=.profit-planet.partners cookie
const loginUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/login` const loginUrl = `/api/login`
console.log('Calling login API:', loginUrl) console.log('Calling login API (BFF):', loginUrl)
const response = await fetch(loginUrl, { const response = await fetch(loginUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
credentials: 'include', // Include cookies for refresh token credentials: 'include',
body: JSON.stringify({ body: JSON.stringify({
email: credentials.email, email: credentials.email,
password: credentials.password, password: credentials.password,

View File

@ -139,8 +139,10 @@ const useAuthStore = create<AuthStore>((set, get) => ({
logout: async () => { logout: async () => {
log("🚪 Zustand: Logging out — revoking refresh token on server"); log("🚪 Zustand: Logging out — revoking refresh token on server");
try { try {
const logoutUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/logout`; // Use same-origin BFF route so it can clear Domain=.profit-planet.partners cookie
const logoutUrl = `/api/logout`;
log("🌐 Zustand: Calling logout endpoint:", logoutUrl); log("🌐 Zustand: Calling logout endpoint:", logoutUrl);
const res = await fetch(logoutUrl, { const res = await fetch(logoutUrl, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
@ -148,15 +150,13 @@ const useAuthStore = create<AuthStore>((set, get) => ({
"Content-Type": "application/json" "Content-Type": "application/json"
} }
}); });
log("📡 Logout response status:", res.status); log("📡 Logout response status:", res.status);
try { try {
const body = await res.json().catch(() => null); const body = await res.json().catch(() => null);
log("📦 Logout response body:", body); log("📦 Logout response body:", body);
} catch {} } catch {}
// Attempt to clear refreshToken cookie client-side (will only work if not httpOnly) // (No client-side cookie clearing; refreshToken is HttpOnly)
if (typeof window !== 'undefined') {
document.cookie = "refreshToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; SameSite=Strict";
}
} catch (error) { } catch (error) {
log("❌ Error calling logout endpoint:", error); log("❌ Error calling logout endpoint:", error);
} finally { } finally {
@ -187,7 +187,8 @@ const useAuthStore = create<AuthStore>((set, get) => ({
const p = (async () => { const p = (async () => {
set({ isRefreshing: true }); set({ isRefreshing: true });
try { try {
const refreshUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/refresh`; // Use same-origin BFF route so it can (re)issue shared refreshToken cookie
const refreshUrl = `/api/refresh`;
log("🌐 Zustand: Calling refresh endpoint:", refreshUrl); log("🌐 Zustand: Calling refresh endpoint:", refreshUrl);
const res = await fetch(refreshUrl, { const res = await fetch(refreshUrl, {