117 lines
3.3 KiB
TypeScript
117 lines
3.3 KiB
TypeScript
import type { JWT } from 'next-auth/jwt'
|
||
import { encode } from 'next-auth/jwt'
|
||
|
||
const API_BASE = process.env.API_BASE_URL ?? 'http://localhost:8080'
|
||
|
||
/** Access token bitiminden önce yenile (ms) — backend 15 dk ise güvenli tampon */
|
||
const REFRESH_BUFFER_MS = 120_000
|
||
|
||
/** NextAuth JWT şifreli cookie ömrü (saniye) — varsayılan NextAuth ile uyumlu */
|
||
const JWT_COOKIE_MAX_AGE_SEC = 30 * 24 * 60 * 60
|
||
|
||
export function sessionCookieName(): string {
|
||
return isSecureSessionCookieEnabled()
|
||
? '__Secure-next-auth.session-token'
|
||
: 'next-auth.session-token'
|
||
}
|
||
|
||
export function getJwtExpMs(accessToken: string): number {
|
||
try {
|
||
const payloadPart = accessToken.split('.')[1]
|
||
if (!payloadPart) return Date.now()
|
||
const payload = JSON.parse(Buffer.from(payloadPart, 'base64url').toString('utf8')) as {
|
||
exp?: number
|
||
}
|
||
if (!payload.exp) return Date.now()
|
||
return payload.exp * 1000
|
||
} catch {
|
||
return Date.now()
|
||
}
|
||
}
|
||
|
||
/** Access süresi dolmak üzereyse veya dolmuşsa true */
|
||
export function shouldRefreshBackendToken(token: JWT | null): boolean {
|
||
if (!token?.refreshToken) return false
|
||
const exp = token.accessTokenExpires as number | undefined
|
||
if (!exp) return true
|
||
return Date.now() >= exp - REFRESH_BUFFER_MS
|
||
}
|
||
|
||
export async function fetchRefreshedBackendJwt(token: JWT): Promise<JWT | null> {
|
||
const refreshToken = token.refreshToken
|
||
if (!refreshToken) return null
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/v1/auth/refresh`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||
})
|
||
|
||
if (!res.ok) return null
|
||
|
||
const data = (await res.json()) as { access?: unknown; refresh?: unknown }
|
||
// Backend: { "access": "...", "refresh": "..." } — Login_Register.md ile aynı
|
||
if (typeof data.access !== 'string' || !data.access) return null
|
||
|
||
const accessToken = data.access
|
||
const nextRefresh =
|
||
typeof data.refresh === 'string' && data.refresh.length > 0
|
||
? data.refresh
|
||
: refreshToken
|
||
|
||
return {
|
||
...token,
|
||
accessToken,
|
||
refreshToken: nextRefresh,
|
||
accessTokenExpires: getJwtExpMs(accessToken),
|
||
error: undefined,
|
||
}
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
export async function encodeSessionJwt(token: JWT): Promise<string> {
|
||
const secret = process.env.NEXTAUTH_SECRET ?? process.env.AUTH_SECRET
|
||
if (!secret) throw new Error('NEXTAUTH_SECRET eksik')
|
||
|
||
return encode({
|
||
token,
|
||
secret,
|
||
maxAge: JWT_COOKIE_MAX_AGE_SEC,
|
||
})
|
||
}
|
||
|
||
export function sessionCookieOptions(): {
|
||
httpOnly: boolean
|
||
secure: boolean
|
||
sameSite: 'lax'
|
||
path: string
|
||
maxAge: number
|
||
} {
|
||
const secure = isSecureSessionCookieEnabled()
|
||
return {
|
||
httpOnly: true,
|
||
secure,
|
||
sameSite: 'lax',
|
||
path: '/',
|
||
maxAge: JWT_COOKIE_MAX_AGE_SEC,
|
||
}
|
||
}
|
||
|
||
export function isSecureSessionCookieEnabled(): boolean {
|
||
return (
|
||
!!process.env.NEXTAUTH_URL?.startsWith('https://') ||
|
||
!!process.env.VERCEL
|
||
)
|
||
}
|
||
|
||
/** Server action / Route Handler: güncellenmiş JWT’yi NextAuth session çerezine yazar */
|
||
export function applySessionCookie(
|
||
cookieStore: { set: (name: string, value: string, options: Record<string, unknown>) => void },
|
||
jwt: string
|
||
): void {
|
||
cookieStore.set(sessionCookieName(), jwt, sessionCookieOptions())
|
||
}
|