/ src / hooks.server.ts
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  };