275 lines
7.9 KiB
JavaScript
275 lines
7.9 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
const helmet = require('helmet');
|
|
const app = express();
|
|
const passport = require('passport');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const bodyParser = require('body-parser');
|
|
const config = require('./config');
|
|
const swaggerUI = require('swagger-ui-express');
|
|
const swaggerJsDoc = require('swagger-jsdoc');
|
|
const { logger, requestLogger } = require('./utils/logger');
|
|
const {
|
|
uploadLimiter,
|
|
downloadLimiter,
|
|
searchLimiter,
|
|
aiLimiter,
|
|
} = require('./middlewares/rateLimiter');
|
|
|
|
const authRoutes = require('./routes/auth');
|
|
const fileRoutes = require('./routes/file');
|
|
const searchRoutes = require('./routes/search');
|
|
const sqlRoutes = require('./routes/sql');
|
|
|
|
const openaiRoutes = require('./routes/openai');
|
|
|
|
const usersRoutes = require('./routes/users');
|
|
|
|
const rolesRoutes = require('./routes/roles');
|
|
|
|
const permissionsRoutes = require('./routes/permissions');
|
|
|
|
const projectsRoutes = require('./routes/projects');
|
|
|
|
const project_membershipsRoutes = require('./routes/project_memberships');
|
|
|
|
const assetsRoutes = require('./routes/assets');
|
|
|
|
const asset_variantsRoutes = require('./routes/asset_variants');
|
|
|
|
const presigned_url_requestsRoutes = require('./routes/presigned_url_requests');
|
|
|
|
const tour_pagesRoutes = require('./routes/tour_pages');
|
|
|
|
const project_audio_tracksRoutes = require('./routes/project_audio_tracks');
|
|
|
|
const publish_eventsRoutes = require('./routes/publish_events');
|
|
|
|
const pwa_cachesRoutes = require('./routes/pwa_caches');
|
|
|
|
const access_logsRoutes = require('./routes/access_logs');
|
|
const element_type_defaultsRoutes = require('./routes/element_type_defaults');
|
|
const project_element_defaultsRoutes = require('./routes/project_element_defaults');
|
|
|
|
const publishRoutes = require('./routes/publish');
|
|
const runtimeContextRoutes = require('./routes/runtime-context');
|
|
const { runtimeContextMiddleware } = require('./middlewares/runtime-context');
|
|
const {
|
|
blockNonPublicRuntimeListEndpoints,
|
|
sanitizePublicRuntimeListResponse,
|
|
} = require('./middlewares/runtime-public');
|
|
|
|
const getBaseUrl = (url) => {
|
|
if (!url) return '';
|
|
return url.endsWith('/api') ? url.slice(0, -4) : url;
|
|
};
|
|
|
|
const options = {
|
|
definition: {
|
|
openapi: '3.0.0',
|
|
info: {
|
|
version: '1.0.0',
|
|
title: 'Tour Builder Platform',
|
|
description:
|
|
'Tour Builder Platform Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.',
|
|
},
|
|
servers: [
|
|
{
|
|
url: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl,
|
|
description: 'Development server',
|
|
},
|
|
],
|
|
components: {
|
|
securitySchemes: {
|
|
bearerAuth: {
|
|
type: 'http',
|
|
scheme: 'bearer',
|
|
bearerFormat: 'JWT',
|
|
},
|
|
},
|
|
responses: {
|
|
UnauthorizedError: {
|
|
description: 'Access token is missing or invalid',
|
|
},
|
|
},
|
|
},
|
|
security: [
|
|
{
|
|
bearerAuth: [],
|
|
},
|
|
],
|
|
},
|
|
apis: ['./src/routes/*.js'],
|
|
};
|
|
|
|
const specs = swaggerJsDoc(options);
|
|
app.use(
|
|
'/api-docs',
|
|
function (req, res, next) {
|
|
swaggerUI.host =
|
|
getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host');
|
|
next();
|
|
},
|
|
swaggerUI.serve,
|
|
swaggerUI.setup(specs),
|
|
);
|
|
|
|
app.enable('trust proxy');
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: false,
|
|
crossOriginEmbedderPolicy: false,
|
|
}),
|
|
);
|
|
app.use(cors({ origin: true }));
|
|
require('./auth/auth');
|
|
|
|
// Request logger applied early so all routes are logged
|
|
app.use(requestLogger);
|
|
|
|
// Initialize passport JWT auth early (before file routes)
|
|
const jwtAuth = passport.authenticate('jwt', { session: false });
|
|
|
|
// Mount file routes BEFORE body-parser to avoid JSON parsing on binary uploads
|
|
// These routes handle their own body parsing (JSON for init/finalize, raw streams for chunks)
|
|
// Use downloadLimiter for download/presign (high traffic), uploadLimiter for uploads (strict)
|
|
app.use('/api/file/download', downloadLimiter);
|
|
app.use('/api/file/presign', downloadLimiter);
|
|
app.use('/api/file/upload', uploadLimiter);
|
|
app.use('/api/file/upload-sessions', uploadLimiter);
|
|
app.use('/api/file', fileRoutes);
|
|
|
|
// Body parser for all other routes
|
|
app.use(bodyParser.json({ limit: '1mb' }));
|
|
app.use(bodyParser.urlencoded({ extended: true, limit: '1mb' }));
|
|
app.use(runtimeContextMiddleware);
|
|
|
|
const requireRuntimeReadOrAuth = (req, res, next) => {
|
|
const headerEnvironment = req.runtimeContext?.headerEnvironment;
|
|
const isReadOnlyRequest = ['GET', 'OPTIONS'].includes(req.method);
|
|
const hasAuthHeader = Boolean(req.headers.authorization);
|
|
|
|
// Only production is public. Stage requires authentication (workspace for review).
|
|
const isPublicEnvironment = headerEnvironment === 'production';
|
|
|
|
if (isPublicEnvironment && isReadOnlyRequest && !hasAuthHeader) {
|
|
req.isRuntimePublicRequest = true;
|
|
return next();
|
|
}
|
|
|
|
req.isRuntimePublicRequest = false;
|
|
return jwtAuth(req, res, next);
|
|
};
|
|
|
|
// Health check endpoint (no auth required)
|
|
app.get('/api/health', async (req, res) => {
|
|
const db = require('./db/models');
|
|
const health = {
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
environment: process.env.NODE_ENV || 'development',
|
|
};
|
|
|
|
try {
|
|
await db.sequelize.authenticate();
|
|
health.database = 'connected';
|
|
} catch (error) {
|
|
health.status = 'degraded';
|
|
health.database = 'disconnected';
|
|
health.databaseError = error.message;
|
|
}
|
|
|
|
const statusCode = health.status === 'ok' ? 200 : 503;
|
|
res.status(statusCode).json(health);
|
|
});
|
|
|
|
app.use('/api/auth', authRoutes);
|
|
app.use('/api/runtime-context', runtimeContextRoutes);
|
|
|
|
app.use('/api/users', jwtAuth, usersRoutes);
|
|
|
|
app.use('/api/roles', jwtAuth, rolesRoutes);
|
|
|
|
app.use('/api/permissions', jwtAuth, permissionsRoutes);
|
|
|
|
const mountRuntimeEntityRoute = (path, entityName, router) => {
|
|
app.use(
|
|
path,
|
|
requireRuntimeReadOrAuth,
|
|
blockNonPublicRuntimeListEndpoints,
|
|
sanitizePublicRuntimeListResponse(entityName),
|
|
router,
|
|
);
|
|
};
|
|
|
|
mountRuntimeEntityRoute('/api/projects', 'projects', projectsRoutes);
|
|
|
|
app.use('/api/project_memberships', jwtAuth, project_membershipsRoutes);
|
|
|
|
app.use('/api/assets', jwtAuth, assetsRoutes);
|
|
|
|
app.use('/api/asset_variants', jwtAuth, asset_variantsRoutes);
|
|
|
|
app.use('/api/presigned_url_requests', jwtAuth, presigned_url_requestsRoutes);
|
|
|
|
mountRuntimeEntityRoute('/api/tour_pages', 'tour_pages', tour_pagesRoutes);
|
|
|
|
mountRuntimeEntityRoute(
|
|
'/api/project_audio_tracks',
|
|
'project_audio_tracks',
|
|
project_audio_tracksRoutes,
|
|
);
|
|
|
|
app.use('/api/publish_events', jwtAuth, publish_eventsRoutes);
|
|
|
|
app.use('/api/pwa_caches', jwtAuth, pwa_cachesRoutes);
|
|
|
|
app.use('/api/access_logs', jwtAuth, access_logsRoutes);
|
|
app.use('/api/element-type-defaults', jwtAuth, element_type_defaultsRoutes);
|
|
// Backwards compatibility alias for old API endpoint
|
|
app.use('/api/ui-elements', jwtAuth, element_type_defaultsRoutes);
|
|
app.use(
|
|
'/api/project-element-defaults',
|
|
jwtAuth,
|
|
project_element_defaultsRoutes,
|
|
);
|
|
|
|
app.use('/api/publish', jwtAuth, publishRoutes);
|
|
|
|
app.use('/api/openai', jwtAuth, aiLimiter, openaiRoutes);
|
|
app.use('/api/ai', jwtAuth, aiLimiter, openaiRoutes);
|
|
|
|
app.use('/api/search', jwtAuth, searchLimiter, searchRoutes);
|
|
app.use('/api/sql', jwtAuth, sqlRoutes);
|
|
|
|
const publicDir = path.join(__dirname, '../public');
|
|
|
|
if (fs.existsSync(publicDir)) {
|
|
app.use('/', express.static(publicDir));
|
|
|
|
app.get('*', function (request, response) {
|
|
response.sendFile(path.resolve(publicDir, 'index.html'));
|
|
});
|
|
}
|
|
|
|
// Generic error handler
|
|
app.use((err, req, res, _next) => {
|
|
if (!res.headersSent) {
|
|
logger.error({ err, url: req.url, method: req.method }, 'Unhandled error');
|
|
res.status(500).json({ message: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080;
|
|
|
|
app.listen(PORT, () => {
|
|
logger.info(
|
|
{ port: PORT, env: process.env.NODE_ENV || 'development' },
|
|
'Server started',
|
|
);
|
|
});
|
|
|
|
module.exports = app;
|