tududi/backend/app.js
2025-12-07 14:12:38 +02:00

260 lines
7.6 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const path = require('path');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const morgan = require('morgan');
const session = require('express-session');
const SequelizeStore = require('connect-session-sequelize')(session.Store);
const { sequelize } = require('./models');
const { initializeTelegramPolling } = require('./services/telegramInitializer');
const taskScheduler = require('./services/taskScheduler');
const { setConfig, getConfig } = require('./config/config');
const config = getConfig();
const API_VERSION = process.env.API_VERSION || 'v1';
const API_BASE_PATH = `/api/${API_VERSION}`;
const app = express();
// Session store
const sessionStore = new SequelizeStore({
db: sequelize,
});
// Middlewares
app.use(
helmet({
hsts: false,
forceHTTPS: false,
contentSecurityPolicy: false,
})
);
app.use(compression());
app.use(morgan('combined'));
// CORS configuration
app.use(
cors({
origin: config.allowedOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Authorization',
'Content-Type',
'Accept',
'X-Requested-With',
],
exposedHeaders: ['Content-Type'],
maxAge: 1728000,
})
);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Session configuration
app.use(
session({
secret: config.secret,
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false,
maxAge: 2592000000, // 30 days
sameSite: 'lax',
},
})
);
// Static files
if (config.production) {
app.use(express.static(path.join(__dirname, 'dist')));
} else {
app.use(express.static('public'));
}
// Serve locales
if (config.production) {
app.use('/locales', express.static(path.join(__dirname, 'dist/locales')));
} else {
app.use(
'/locales',
express.static(path.join(__dirname, '../public/locales'))
);
}
// Serve uploaded files
const registerUploadsStatic = (basePath) => {
app.use(`${basePath}/uploads`, express.static(config.uploadPath));
};
registerUploadsStatic('/api');
if (API_VERSION && API_BASE_PATH !== '/api') {
registerUploadsStatic(API_BASE_PATH);
}
// Authentication middleware
const { requireAuth } = require('./middleware/auth');
const { logError } = require('./services/logService');
// Rate limiting middleware
const {
apiLimiter,
authenticatedApiLimiter,
} = require('./middleware/rateLimiter');
// Swagger documentation - enabled by default, protected by authentication
// Mounted on /api-docs to avoid conflicts with API routes
if (config.swagger.enabled) {
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./config/swagger');
const swaggerUiOptions = {
customSiteTitle: 'Tududi API Documentation',
customfavIcon: '/favicon.ico',
customCss: '.swagger-ui .topbar { display: none }',
swaggerOptions: {
url: '/api-docs/swagger.json',
},
};
// Expose on /api-docs, protected by authentication
app.use('/api-docs', requireAuth, swaggerUi.serve);
app.get('/api-docs/swagger.json', requireAuth, (req, res) =>
res.json(swaggerSpec)
);
app.get(
'/api-docs',
requireAuth,
swaggerUi.serveFiles(swaggerSpec, swaggerUiOptions),
swaggerUi.setup(swaggerSpec, swaggerUiOptions)
);
}
// Apply rate limiting to API routes
// Use both limiters: apiLimiter for unauthenticated, authenticatedApiLimiter for authenticated
// Each has skip logic to handle their specific use case
const registerRateLimiting = (basePath) => {
app.use(basePath, apiLimiter);
app.use(basePath, authenticatedApiLimiter);
};
const rateLimitPath =
API_VERSION && API_BASE_PATH !== '/api' ? API_BASE_PATH : '/api';
registerRateLimiting(rateLimitPath);
// Health check (before auth middleware) - ensure it's completely bypassed
const registerHealthCheck = (basePath) => {
app.get(`${basePath}/health`, (req, res) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: config.environment,
});
});
};
const healthPaths = new Set(['/api']);
if (API_VERSION && API_BASE_PATH !== '/api') {
healthPaths.add(API_BASE_PATH);
}
healthPaths.forEach(registerHealthCheck);
// Routes
const registerApiRoutes = (basePath) => {
app.use(basePath, require('./routes/auth'));
app.use(basePath, requireAuth);
app.use(basePath, require('./routes/tasks'));
app.use(basePath, require('./routes/projects'));
app.use(basePath, require('./routes/admin'));
app.use(basePath, require('./routes/shares'));
app.use(basePath, require('./routes/areas'));
app.use(basePath, require('./routes/notes'));
app.use(basePath, require('./routes/tags'));
app.use(basePath, require('./routes/users'));
app.use(basePath, require('./routes/inbox'));
app.use(basePath, require('./routes/url'));
app.use(basePath, require('./routes/telegram'));
app.use(basePath, require('./routes/quotes'));
app.use(basePath, require('./routes/task-events'));
app.use(basePath, require('./routes/task-attachments'));
app.use(`${basePath}/search`, require('./routes/search'));
app.use(`${basePath}/views`, require('./routes/views'));
app.use(`${basePath}/notifications`, require('./routes/notifications'));
};
// Register routes at both /api and /api/v1 (if versioned) to maintain backwards compatibility
// The requireAuth middleware is applied once per base path, preventing the auth loop
const routeBases = new Set(['/api']);
if (API_VERSION && API_BASE_PATH !== '/api') {
routeBases.add(API_BASE_PATH);
}
routeBases.forEach(registerApiRoutes);
// SPA fallback
app.get('*', (req, res) => {
if (
!req.path.startsWith('/api/') &&
!req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)
) {
if (config.production) {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
} else {
res.sendFile(path.join(__dirname, '../public', 'index.html'));
}
} else {
res.status(404).json({
error: 'Not Found',
message: 'The requested resource could not be found.',
});
}
});
// Error handling fallback.
// We shouldn't be here normally!
// Each route should properly handle
// and log its own errors.
app.use((err, req, res, next) => {
logError(err);
res.status(500).json({
error: 'Internal Server Error',
// message: err.message,
});
});
// Initialize database and start server
async function startServer() {
try {
// Create session store table
await sessionStore.sync();
// Initialize Telegram polling after database is ready
await initializeTelegramPolling();
// Initialize task scheduler
await taskScheduler.initialize();
const server = app.listen(config.port, config.host, () => {
console.log(`Server running on port ${config.port}`);
console.log(`Server listening on http://localhost:${config.port}`);
});
server.on('error', (err) => {
console.error('Server error:', err);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
if (require.main === module) {
startServer();
}
module.exports = app;