ci/woodpecker/push/woodpecker Pipeline was successful
Die Bild-URLs in der DB sind /images/events/... bzw. /images/gallery/... - das ist der Pfad auf der publizierten Astro-Seite. Das Backend lieferte die Dateien aber nur unter /static/ mit Root auf dem Workspace aus, die Datei lag also unter /static/public/images/... Ein Aufruf von /images/... traf auf gar keine Route, kam als JSON-404 zurueck und wurde vom Browser als OpaqueResponseBlocking verworfen. Dadurch waren im Adminbereich saemtliche Event- und Gallery-Bilder kaputt. Zusaetzlicher statischer Mount /images/ -> <workspace>/public/images. Die DB bleibt unveraendert und die URLs stimmen weiterhin fuer die publizierte Seite. /static/ bleibt zusaetzlich bestehen. Beide Verzeichnisse werden vorab angelegt - @fastify/static verweigert sonst die Registrierung, wenn der Workspace noch nicht geklont ist. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
158 lines
4.2 KiB
TypeScript
158 lines
4.2 KiB
TypeScript
import Fastify from 'fastify';
|
|
import cors from '@fastify/cors';
|
|
import jwt from '@fastify/jwt';
|
|
import multipart from '@fastify/multipart';
|
|
import cookie from '@fastify/cookie';
|
|
import { authenticate } from './middleware/auth.middleware.js';
|
|
import { env, validateEnv } from './config/env.js';
|
|
import { db, initDatabase } from './config/database.js';
|
|
import fastifyStatic from '@fastify/static';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
|
|
// Import routes
|
|
import authRoute from './routes/auth.js';
|
|
import eventsRoute from './routes/events.js';
|
|
import galleryRoute from './routes/gallery.js';
|
|
import contentRoute from './routes/content.js';
|
|
import settingsRoute from './routes/settings.js';
|
|
import publishRoute from './routes/publish.js';
|
|
import bannersRoute from './routes/banners.js';
|
|
|
|
// Validate environment variables
|
|
try {
|
|
validateEnv();
|
|
} catch (error) {
|
|
console.error('Environment validation failed:', error);
|
|
process.exit(1);
|
|
}
|
|
|
|
const fastify = Fastify({
|
|
logger: {
|
|
level: env.NODE_ENV === 'production' ? 'info' : 'debug',
|
|
transport: env.NODE_ENV === 'development' ? {
|
|
target: 'pino-pretty',
|
|
options: {
|
|
translateTime: 'HH:MM:ss Z',
|
|
ignore: 'pid,hostname',
|
|
},
|
|
} : undefined,
|
|
},
|
|
});
|
|
|
|
// Register plugins
|
|
// Support multiple origins for CORS
|
|
const allowedOrigins = env.CORS_ORIGIN.split(',').map(o => o.trim());
|
|
fastify.register(cors, {
|
|
origin: (origin, cb) => {
|
|
// Allow requests with no origin (like mobile apps or curl)
|
|
if (!origin) {
|
|
return cb(null, true);
|
|
}
|
|
|
|
// Check if origin is in allowed list
|
|
const isAllowed = allowedOrigins.includes(origin);
|
|
|
|
if (isAllowed) {
|
|
return cb(null, true);
|
|
} else {
|
|
return cb(null, false);
|
|
}
|
|
},
|
|
credentials: true,
|
|
});
|
|
|
|
fastify.register(cookie);
|
|
|
|
fastify.register(jwt, {
|
|
secret: env.JWT_SECRET,
|
|
cookie: {
|
|
cookieName: 'token',
|
|
signed: false,
|
|
},
|
|
});
|
|
|
|
fastify.register(multipart, {
|
|
limits: {
|
|
fileSize: env.MAX_FILE_SIZE,
|
|
},
|
|
});
|
|
|
|
// Serve static files (uploaded images, etc.) from persistent volume
|
|
const dataDir = env.GIT_WORKSPACE_DIR || path.join(process.cwd(), 'data');
|
|
// Muss existieren, sonst verweigert @fastify/static die Registrierung
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
fastify.register(fastifyStatic, {
|
|
root: dataDir,
|
|
prefix: '/static/',
|
|
decorateReply: false
|
|
});
|
|
|
|
// Uploads liegen unter <workspace>/public/images. In der DB stehen sie als
|
|
// /images/... - das ist die URL auf der publizierten Astro-Seite. Damit die
|
|
// Admin-Oberflaeche dieselben Pfade verwenden kann, hier ebenso ausliefern.
|
|
const imagesDir = path.join(dataDir, 'public', 'images');
|
|
fs.mkdirSync(imagesDir, { recursive: true });
|
|
fastify.register(fastifyStatic, {
|
|
root: imagesDir,
|
|
prefix: '/images/',
|
|
decorateReply: false
|
|
});
|
|
|
|
// Decorate fastify with authenticate method
|
|
fastify.decorate('authenticate', authenticate);
|
|
|
|
// Register routes
|
|
fastify.register(authRoute, { prefix: '/api' });
|
|
fastify.register(eventsRoute, { prefix: '/api' });
|
|
fastify.register(galleryRoute, { prefix: '/api' });
|
|
fastify.register(contentRoute, { prefix: '/api' });
|
|
fastify.register(settingsRoute, { prefix: '/api' });
|
|
fastify.register(publishRoute, { prefix: '/api' });
|
|
fastify.register(bannersRoute, { prefix: '/api' });
|
|
|
|
// Health check
|
|
fastify.get('/health', async () => {
|
|
return {
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
environment: env.NODE_ENV,
|
|
};
|
|
});
|
|
|
|
// Root endpoint
|
|
fastify.get('/', async () => {
|
|
return {
|
|
name: 'Gallus Pub CMS Backend',
|
|
version: '1.0.0',
|
|
status: 'running',
|
|
};
|
|
});
|
|
|
|
// Error handler
|
|
fastify.setErrorHandler((error, request, reply) => {
|
|
fastify.log.error(error);
|
|
|
|
reply.status(error.statusCode || 500).send({
|
|
error: error.message || 'Internal Server Error',
|
|
statusCode: error.statusCode || 500,
|
|
});
|
|
});
|
|
|
|
// Start server
|
|
const start = async () => {
|
|
try {
|
|
// Initialize database before starting server
|
|
initDatabase();
|
|
await fastify.listen({ port: env.PORT, host: '0.0.0.0' });
|
|
console.log(`🚀 Server listening on port ${env.PORT}`);
|
|
console.log(`📝 Environment: ${env.NODE_ENV}`);
|
|
console.log(`🔐 CORS Origin: ${env.CORS_ORIGIN}`);
|
|
} catch (err) {
|
|
fastify.log.error(err);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
start();
|