fix:maybe
This commit is contained in:
parent
2fed9b4b8b
commit
96cfc90e17
@ -6,9 +6,8 @@
|
||||
*/
|
||||
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
|
||||
const AUTH_COOKIES = ['refreshToken']
|
||||
const AUTH_COOKIES = ['refreshToken', '__Secure-refreshToken']
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const { pathname } = req.nextUrl
|
||||
|
||||
69
src/app/api/login/route.ts
Normal file
69
src/app/api/login/route.ts
Normal 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
|
||||
}
|
||||
40
src/app/api/logout/route.ts
Normal file
40
src/app/api/logout/route.ts
Normal 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
|
||||
}
|
||||
67
src/app/api/refresh/route.ts
Normal file
67
src/app/api/refresh/route.ts
Normal 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
|
||||
}
|
||||
@ -28,16 +28,16 @@ export function useLogin() {
|
||||
rememberMe: credentials.rememberMe
|
||||
})
|
||||
|
||||
// Make actual API call to backend
|
||||
const loginUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/login`
|
||||
console.log('Calling login API:', loginUrl)
|
||||
|
||||
// Call same-origin BFF route so it can set Domain=.profit-planet.partners cookie
|
||||
const loginUrl = `/api/login`
|
||||
console.log('Calling login API (BFF):', loginUrl)
|
||||
|
||||
const response = await fetch(loginUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include', // Include cookies for refresh token
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
|
||||
@ -139,8 +139,10 @@ const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
logout: async () => {
|
||||
log("🚪 Zustand: Logging out — revoking refresh token on server");
|
||||
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);
|
||||
|
||||
const res = await fetch(logoutUrl, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
@ -148,15 +150,13 @@ const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
log("📡 Logout response status:", res.status);
|
||||
try {
|
||||
const body = await res.json().catch(() => null);
|
||||
log("📦 Logout response body:", body);
|
||||
} catch {}
|
||||
// Attempt to clear refreshToken cookie client-side (will only work if not httpOnly)
|
||||
if (typeof window !== 'undefined') {
|
||||
document.cookie = "refreshToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; SameSite=Strict";
|
||||
}
|
||||
// (No client-side cookie clearing; refreshToken is HttpOnly)
|
||||
} catch (error) {
|
||||
log("❌ Error calling logout endpoint:", error);
|
||||
} finally {
|
||||
@ -187,7 +187,8 @@ const useAuthStore = create<AuthStore>((set, get) => ({
|
||||
const p = (async () => {
|
||||
set({ isRefreshing: true });
|
||||
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);
|
||||
|
||||
const res = await fetch(refreshUrl, {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user