fix:maybe
This commit is contained in:
parent
2fed9b4b8b
commit
96cfc90e17
@ -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
|
||||||
|
|||||||
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
|
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,
|
||||||
|
|||||||
@ -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, {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user