2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Server – Fastify configuration with CORS, static, and WebSocket
|
|
|
|
|
|
*/
|
|
|
|
|
|
import Fastify from 'fastify';
|
|
|
|
|
|
import cors from '@fastify/cors';
|
|
|
|
|
|
import staticPlugin from '@fastify/static';
|
|
|
|
|
|
import websocket from '@fastify/websocket';
|
|
|
|
|
|
import { join, dirname } from 'path';
|
|
|
|
|
|
import { fileURLToPath } from 'url';
|
|
|
|
|
|
import type { DatabaseInterface } from './database/DatabaseInterface.js';
|
|
|
|
|
|
import { AuthService } from './auth/AuthService.js';
|
|
|
|
|
|
import { registerYjsWebSocket, initYjsPersistence, closeYjsPersistence } from './websocket/yjsServer.js';
|
|
|
|
|
|
|
|
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
|
|
|
|
|
|
|
export interface ServerOptions {
|
|
|
|
|
|
port?: number;
|
|
|
|
|
|
host?: string;
|
|
|
|
|
|
db: DatabaseInterface;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function createServer(opts: ServerOptions) {
|
|
|
|
|
|
const fastify = Fastify({ logger: true });
|
|
|
|
|
|
|
2026-06-29 23:17:19 +02:00
|
|
|
|
// CORS — restrictive origin list
|
2026-06-26 10:50:24 +02:00
|
|
|
|
await fastify.register(cors, {
|
2026-06-29 23:17:19 +02:00
|
|
|
|
origin: (origin: string | undefined, cb: (err: Error | null, allow: boolean) => void) => {
|
|
|
|
|
|
// Allow requests with no origin (same-origin, curl, etc.)
|
|
|
|
|
|
if (!origin) return cb(null, true);
|
|
|
|
|
|
// Allow known origins
|
|
|
|
|
|
const allowed = ['https://web-cad-neu.server.media-on.de', 'http://localhost:5173', 'http://localhost:8082'];
|
|
|
|
|
|
if (allowed.includes(origin)) return cb(null, true);
|
|
|
|
|
|
cb(new Error('Not allowed by CORS'), false);
|
|
|
|
|
|
},
|
2026-06-26 10:50:24 +02:00
|
|
|
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// WebSocket
|
|
|
|
|
|
await fastify.register(websocket, {
|
|
|
|
|
|
options: { maxPayload: 1048576 },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Static files (serve frontend build if exists)
|
|
|
|
|
|
const frontendDist = join(__dirname, '..', '..', 'frontend', 'dist');
|
|
|
|
|
|
try {
|
|
|
|
|
|
await fastify.register(staticPlugin, {
|
|
|
|
|
|
root: frontendDist,
|
|
|
|
|
|
prefix: '/',
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Frontend dist may not exist in dev mode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Health check
|
|
|
|
|
|
fastify.get('/api/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
|
|
|
|
|
|
|
|
|
|
|
|
// Register route modules
|
|
|
|
|
|
const { registerProjectRoutes } = await import('./routes/projects.js');
|
|
|
|
|
|
const { registerDrawingRoutes } = await import('./routes/drawings.js');
|
|
|
|
|
|
const { registerLayerRoutes } = await import('./routes/layers.js');
|
|
|
|
|
|
const { registerElementRoutes } = await import('./routes/elements.js');
|
|
|
|
|
|
const { registerBlockRoutes } = await import('./routes/blocks.js');
|
|
|
|
|
|
const { registerSettingsRoutes } = await import('./routes/settings.js');
|
|
|
|
|
|
const { registerAuthRoutes } = await import('./routes/auth.js');
|
|
|
|
|
|
const { registerUserRoutes } = await import('./routes/users.js');
|
|
|
|
|
|
const { registerAIRoutes } = await import('./routes/ai.js');
|
2026-06-28 13:52:54 +02:00
|
|
|
|
const { registerNotificationRoutes } = await import('./routes/notifications.js');
|
|
|
|
|
|
const { registerShareRoutes } = await import('./routes/shares.js');
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
const authService = new AuthService(opts.db);
|
|
|
|
|
|
|
2026-06-29 23:17:19 +02:00
|
|
|
|
// Auth middleware — protect all /api/ routes except auth login/register
|
|
|
|
|
|
fastify.addHook('onRequest', async (request: any, reply: any) => {
|
|
|
|
|
|
const url = request.url;
|
|
|
|
|
|
// Skip auth endpoints and health check
|
|
|
|
|
|
if (url === '/api/auth/login' || url === '/api/auth/register' || url === '/api/health') return;
|
|
|
|
|
|
// Skip non-API routes (static files, WebSocket)
|
|
|
|
|
|
if (!url.startsWith('/api/')) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auth = request.headers.authorization;
|
|
|
|
|
|
if (!auth || !auth.startsWith('Bearer ')) {
|
|
|
|
|
|
return reply.code(401).send({ error: 'Authentication required' });
|
|
|
|
|
|
}
|
|
|
|
|
|
const token = auth.slice(7);
|
|
|
|
|
|
const user = authService.getUserFromSession(token);
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
return reply.code(401).send({ error: 'Invalid or expired session' });
|
|
|
|
|
|
}
|
|
|
|
|
|
(request as any).user = user;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
registerAuthRoutes(fastify, authService);
|
2026-06-30 11:32:33 +02:00
|
|
|
|
registerProjectRoutes(fastify, opts.db, authService);
|
|
|
|
|
|
registerDrawingRoutes(fastify, opts.db, authService);
|
|
|
|
|
|
registerLayerRoutes(fastify, opts.db, authService);
|
|
|
|
|
|
registerElementRoutes(fastify, opts.db, authService);
|
|
|
|
|
|
registerBlockRoutes(fastify, opts.db, authService);
|
|
|
|
|
|
registerSettingsRoutes(fastify, opts.db, authService);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
registerUserRoutes(fastify, opts.db, authService);
|
|
|
|
|
|
registerAIRoutes(fastify, authService);
|
2026-06-28 13:52:54 +02:00
|
|
|
|
registerNotificationRoutes(fastify, opts.db, authService);
|
|
|
|
|
|
registerShareRoutes(fastify, opts.db, authService);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
// Yjs collaboration WebSocket
|
2026-06-29 23:33:58 +02:00
|
|
|
|
registerYjsWebSocket(fastify, authService);
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
return fastify;
|
|
|
|
|
|
}
|