Initial commit: Second Brain Platform
Complete platform with unified design system and real API integration. Apps: Dashboard, Fitness, Budget, Inventory, Trips, Reader, Media, Settings Infrastructure: SvelteKit + Python gateway + Docker Compose
This commit is contained in:
194
frontend-v2/src/hooks.server.ts
Normal file
194
frontend-v2/src/hooks.server.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
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<string, string> = { '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<string, unknown>) => ({
|
||||
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);
|
||||
};
|
||||
Reference in New Issue
Block a user