import type { Handle } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; const gatewayUrl = env.GATEWAY_URL || 'http://localhost:8100'; const immichUrl = env.IMMICH_URL || ''; const immichApiKey = env.IMMICH_API_KEY || ''; const karakeepUrl = env.KARAKEEP_URL || ''; const karakeepApiKey = env.KARAKEEP_API_KEY || ''; export const handle: Handle = async ({ event, resolve }) => { // ── Immich API proxy (shared across all pages) ── if (event.url.pathname.startsWith('/api/immich/') && immichUrl && immichApiKey) { const immichPath = event.url.pathname.replace('/api/immich', '/api'); // Thumbnail/original image proxy — cache-friendly binary response if (immichPath.includes('/thumbnail') || immichPath.includes('/original')) { const assetId = event.url.pathname.split('/')[4]; // /api/immich/assets/{id}/thumbnail const type = immichPath.includes('/original') ? 'original' : 'thumbnail'; try { const response = await fetch(`${immichUrl}/api/assets/${assetId}/${type}`, { headers: { 'x-api-key': immichApiKey } }); return new Response(response.body, { status: response.status, headers: { 'Content-Type': response.headers.get('Content-Type') || 'image/webp', 'Cache-Control': 'public, max-age=86400' } }); } catch { return new Response('', { status: 502 }); } } // JSON API proxy (search, timeline, etc.) try { const targetUrl = `${immichUrl}${immichPath}${event.url.search}`; const reqHeaders: Record = { 'x-api-key': immichApiKey }; const ct = event.request.headers.get('content-type'); if (ct) reqHeaders['content-type'] = ct; const response = await fetch(targetUrl, { method: event.request.method, headers: reqHeaders, body: event.request.method !== 'GET' && event.request.method !== 'HEAD' ? await event.request.arrayBuffer() : undefined }); // For search results, strip down to minimal fields to avoid huge payloads if (immichPath.includes('/search/') && response.ok) { const data = await response.json(); if (data.assets?.items) { data.assets.items = data.assets.items.map((a: Record) => ({ id: a.id, type: a.type, fileCreatedAt: a.fileCreatedAt, originalFileName: a.originalFileName })); } return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } }); } const responseHeaders = new Headers(); for (const [key, value] of response.headers.entries()) { responseHeaders.append(key, value); } return new Response(response.body, { status: response.status, headers: responseHeaders }); } catch { return new Response(JSON.stringify({ error: 'Immich unavailable' }), { status: 502, headers: { 'Content-Type': 'application/json' } }); } } // ── Karakeep API proxy (server-to-server) ── if (event.url.pathname.startsWith('/api/karakeep/') && karakeepUrl && karakeepApiKey) { const action = event.url.pathname.split('/').pop(); // 'save' or 'delete' try { const body = await event.request.json(); if (action === 'save' && body.url) { const response = await fetch(`${karakeepUrl}/api/v1/bookmarks`, { method: 'POST', headers: { 'Authorization': `Bearer ${karakeepApiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'link', url: body.url }) }); const data = await response.json(); if (response.ok) { return new Response(JSON.stringify({ ok: true, id: data.id || '' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ error: 'Save failed', status: response.status }), { status: response.status, headers: { 'Content-Type': 'application/json' } }); } if (action === 'delete' && body.id) { const response = await fetch(`${karakeepUrl}/api/v1/bookmarks/${body.id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${karakeepApiKey}` } }); return new Response(JSON.stringify({ ok: response.ok }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ error: 'Invalid request' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } catch (err) { console.error('Karakeep proxy error:', err); return new Response(JSON.stringify({ error: 'Karakeep unavailable', detail: String(err) }), { status: 502, headers: { 'Content-Type': 'application/json' } }); } } // Legacy trips-specific Immich thumbnail (keep for backward compat) if (event.url.pathname.startsWith('/api/trips/immich/thumb/') && immichUrl && immichApiKey) { const assetId = event.url.pathname.split('/').pop(); try { const response = await fetch(`${immichUrl}/api/assets/${assetId}/thumbnail`, { headers: { 'x-api-key': immichApiKey } }); return new Response(response.body, { status: response.status, headers: { 'Content-Type': response.headers.get('Content-Type') || 'image/webp', 'Cache-Control': 'public, max-age=86400' } }); } catch { return new Response('', { status: 502 }); } } // Proxy all /api/* requests to the gateway if (event.url.pathname.startsWith('/api/') || event.url.pathname.startsWith('/images/')) { // Rewrite /images/trips/* to /api/trips/images/* for gateway routing let targetPath = event.url.pathname; if (targetPath.startsWith('/images/trips/')) { targetPath = '/api/trips' + targetPath.replace('/images/trips', '/images'); } else if (targetPath.startsWith('/images/fitness/')) { targetPath = '/api/fitness' + targetPath.replace('/images/fitness', '/images'); } const targetUrl = `${gatewayUrl}${targetPath}${event.url.search}`; const headers = new Headers(); // Forward all relevant headers for (const [key, value] of event.request.headers.entries()) { if (['authorization', 'content-type', 'cookie', 'x-api-key', 'x-telegram-user-id'].includes(key.toLowerCase())) { headers.set(key, value); } } try { const response = await fetch(targetUrl, { method: event.request.method, headers, body: event.request.method !== 'GET' && event.request.method !== 'HEAD' ? await event.request.arrayBuffer() : undefined }); // Forward set-cookie headers from gateway const responseHeaders = new Headers(); for (const [key, value] of response.headers.entries()) { responseHeaders.append(key, value); } return new Response(response.body, { status: response.status, headers: responseHeaders }); } catch (error) { console.error('Gateway proxy error:', error); return new Response(JSON.stringify({ error: 'Gateway unavailable' }), { status: 502, headers: { 'Content-Type': 'application/json' } }); } } return resolve(event); };