Letschat / src /lib /appSession.ts
HonzaH's picture
Upload 192 files
7447362 verified
Raw
History Blame Contribute Delete
3.19 kB
import { cookies, headers } from 'next/headers'
import { NextResponse, NextRequest } from 'next/server'
import { createHmac } from 'crypto'
import { isHuggingFaceSpace } from 'lib/supabase/publicEnv'
const APP_SESSION_COOKIE = 'letschat_session'
const DEFAULT_MAX_AGE_SECONDS = 30 * 24 * 60 * 60 // 30 days
const isProd = process.env.NODE_ENV === 'production'
// Prefer broadest compatibility in production (embeds/iframe) -> None; dev -> Lax.
const sameSite: 'lax' | 'none' =
isProd || process.env.FORCE_SAMESITE_NONE === '1' || isHuggingFaceSpace ? 'none' : 'lax'
type AppSessionPayload = {
userId: string
email: string
exp: number
}
const secret = process.env.APP_SECRET || process.env.SUPABASE_SERVICE_ROLE_KEY || 'dev-secret'
function sign(payload: AppSessionPayload) {
const data = Buffer.from(JSON.stringify(payload)).toString('base64url')
const sig = createHmac('sha256', secret).update(data).digest('base64url')
return `${data}.${sig}`
}
function verify(token: string): AppSessionPayload | null {
const [data, sig] = token.split('.')
if (!data || !sig) return null
const expected = createHmac('sha256', secret).update(data).digest('base64url')
if (expected !== sig) return null
try {
const payload = JSON.parse(Buffer.from(data, 'base64url').toString()) as AppSessionPayload
if (payload.exp && Date.now() > payload.exp) return null
return payload
} catch {
return null
}
}
export async function setAppSession(res: NextResponse, userId: string, email: string) {
const exp = Date.now() + DEFAULT_MAX_AGE_SECONDS * 1000
const token = sign({ userId, email, exp })
res.cookies.set(APP_SESSION_COOKIE, token, {
httpOnly: true,
secure: isProd,
sameSite,
maxAge: DEFAULT_MAX_AGE_SECONDS,
path: '/'
})
// Also set a non-httpOnly cookie so client JS / proxies can read it if necessary
try {
res.cookies.set(`${APP_SESSION_COOKIE}_pub`, token, {
httpOnly: false,
secure: isProd,
sameSite,
maxAge: DEFAULT_MAX_AGE_SECONDS,
path: '/'
})
} catch (e) {
// ignore in environments that disallow multiple Set-Cookie settings
}
return token
}
export async function clearAppSession(res: NextResponse) {
res.cookies.set(APP_SESSION_COOKIE, '', {
httpOnly: true,
secure: isProd,
sameSite,
maxAge: 0,
path: '/'
})
}
export async function getAppSession(req?: NextRequest): Promise<AppSessionPayload | null> {
// Try req cookies first (server handler may pass NextRequest)
let token = req?.cookies?.get(APP_SESSION_COOKIE)?.value
// Fallback to Next.js cookies() helper
if (!token) {
token = (await cookies()).get(APP_SESSION_COOKIE)?.value
}
// As a last resort, try to parse Cookie header string directly (some proxies/platforms)
if (!token && req?.headers?.get) {
const cookieHeader = req.headers.get('cookie') || ''
if (cookieHeader) {
const match = cookieHeader.split(';').map((s) => s.trim()).find((c) => c.startsWith(`${APP_SESSION_COOKIE}=`))
if (match) token = match.split('=')[1]
}
}
console.log('[getAppSession] token present=', !!token)
if (!token) return null
return verify(token)
}