hooks.server.ts
1 /* eslint-disable no-console */ 2 import getOptionalEnvVar from '$lib/utils/get-optional-env-var/public'; 3 import { PuppeteerManager } from '$lib/utils/puppeteer'; 4 import z from 'zod'; 5 import setCookieParser from 'set-cookie-parser'; 6 import { error, isRedirect, redirect } from '@sveltejs/kit'; 7 import { getUserData } from '$lib/utils/wave/auth'; 8 9 PuppeteerManager.launch({ 10 args: ['--no-sandbox', '--disable-setuid-sandbox'], 11 }); 12 13 const WAVE_API_URL = getOptionalEnvVar( 14 'PUBLIC_INTERNAL_WAVE_API_URL', 15 true, 16 'Wave functionality will not work.', 17 ); 18 19 export const handle = async ({ event, resolve }) => { 20 // If we're under /wave path, handle Wave authentication. 21 // This allows the initial page render to be SSR even for logged-in-only views. 22 23 if (event.url.pathname.startsWith('/wave') && WAVE_API_URL) { 24 const refreshToken = event.cookies.get('wave_refresh_token', {}); 25 const accessToken = event.cookies.get('wave_access_token', {}); 26 27 if (refreshToken) { 28 // Check if access token is valid and not expired 29 const userData = accessToken ? getUserData(accessToken) : null; 30 31 if (userData) { 32 // Access token still valid, use it directly (no refresh needed) 33 event.locals.waveRefreshToken = refreshToken; 34 event.locals.waveAccessToken = accessToken; 35 } else { 36 // Access token missing or invalid/expired, attempt refresh 37 try { 38 const res = await fetch(`${WAVE_API_URL}/api/auth/token/refresh`, { 39 method: 'POST', 40 credentials: 'include', 41 headers: { Cookie: `wave_refresh_token=${refreshToken}` }, 42 }); 43 44 if (!res.ok) { 45 if (res.status === 403) { 46 const body = await res.text(); 47 if (body.includes('suspended')) { 48 event.cookies.delete('wave_refresh_token', { path: '/' }); 49 event.cookies.delete('wave_access_token', { path: '/' }); 50 throw redirect(302, '/wave/suspended'); 51 } 52 } 53 throw new Error('Failed to refresh token'); 54 } 55 56 const data = z 57 .object({ 58 accessToken: z.string(), 59 }) 60 .parse(await res.json()); 61 62 event.locals.waveRefreshToken = refreshToken; 63 event.locals.waveAccessToken = data.accessToken; 64 65 // Forward new cookies to browser 66 for (const str of setCookieParser.splitCookiesString( 67 res.headers.get('set-cookie') ?? '', 68 )) { 69 const { name, value, ...options } = setCookieParser.parseString(str); 70 71 if (name === 'wave_refresh_token' || name === 'wave_access_token') { 72 event.cookies.set(name, value, { 73 ...options, 74 sameSite: options.sameSite as 'lax' | 'strict' | 'none', 75 path: options.path || '/', 76 httpOnly: name === 'wave_refresh_token', // Only refresh token is httpOnly 77 }); 78 } 79 } 80 } catch (e) { 81 if (isRedirect(e)) throw e; 82 83 // Refresh failed, clear auth state 84 event.cookies.delete('wave_refresh_token', { path: '/' }); 85 event.cookies.delete('wave_access_token', { path: '/' }); 86 delete event.locals.waveRefreshToken; 87 delete event.locals.waveAccessToken; 88 } 89 } 90 } 91 } 92 93 try { 94 return resolve(event, { 95 filterSerializedResponseHeaders(name) { 96 if (name === 'content-type') return true; 97 98 return false; 99 }, 100 }); 101 } catch (e) { 102 console.log('Error during request handling:', e); 103 104 throw error(500, 'Internal Server Error'); 105 } 106 }; 107 108 export const handleFetch = async ({ event, request, fetch }) => { 109 // If the request is going to Wave API, attach auth credentials 110 if (WAVE_API_URL && request.url.startsWith(WAVE_API_URL)) { 111 const accessToken = event.locals.waveAccessToken; 112 const refreshToken = event.locals.waveRefreshToken; 113 114 // Set Authorization header as primary auth method 115 if (accessToken) { 116 request.headers.set('Authorization', `Bearer ${accessToken}`); 117 } 118 119 // Also set cookies (needed for refresh endpoint and as backup) 120 const cookies = [ 121 refreshToken && `wave_refresh_token=${refreshToken}`, 122 accessToken && `wave_access_token=${accessToken}`, 123 ] 124 .filter(Boolean) 125 .join('; '); 126 127 if (cookies) { 128 request.headers.set('Cookie', cookies); 129 } 130 } 131 132 return fetch(request); 133 };