// server.ts import express, { Request, Response, NextFunction } from 'express'; import timeout from 'connect-timeout'; import cookieParser from 'cookie-parser'; import listEndpoints from 'express-list-endpoints'; import { getPool } from './src/services/db/connection'; import passport from './src/routes/passport'; import { logger } from './src/services/logger.server'; // Import routers import authRouter from './src/routes/auth'; import publicRouter from './src/routes/public'; // This seems to be missing from the original file list, but is required. import userRouter from './src/routes/user'; import adminRouter from './src/routes/admin'; import aiRouter from './src/routes/ai'; import budgetRouter from './src/routes/budget'; import gamificationRouter from './src/routes/gamification'; import systemRouter from './src/routes/system'; import { startBackgroundJobs } from './src/services/backgroundJobService'; // Environment variables are now loaded by the `tsx` command in package.json scripts. // This ensures the correct .env file is used for development vs. testing. // --- START DEBUG LOGGING --- // Log the database connection details as seen by the SERVER PROCESS. // This will confirm if the `--env-file` flag is working as expected. logger.info('--- [SERVER PROCESS LOG] DATABASE CONNECTION ---'); logger.info(` NODE_ENV: ${process.env.NODE_ENV}`); logger.info(` Host: ${process.env.DB_HOST}`); logger.info(` Port: ${process.env.DB_PORT}`); logger.info(` User: ${process.env.DB_USER}`); logger.info(` Database: ${process.env.DB_DATABASE}`); // Query the users table to see what the server process sees on startup. // Corrected the query to be unambiguous by specifying the table alias for each column. // `id` and `email` come from the `users` table (u), and `role` comes from the `profiles` table (p). getPool().query('SELECT u.user_id, u.email, p.role FROM public.users u JOIN public.profiles p ON u.user_id = p.user_id') .then(res => { logger.debug('[SERVER PROCESS] Users found in DB on startup:'); console.table(res.rows); }) .catch(err => { logger.error('[SERVER PROCESS] Could not query users table on startup.', { err }); }); logger.info('-----------------------------------------------\n'); const app = express(); // --- Core Middleware --- // Increase the limit for JSON and URL-encoded bodies. This is crucial for handling large file uploads // that are part of multipart/form-data requests, as the overall request size is checked. // Setting a 50MB limit to accommodate large flyer images. app.use(express.json({ limit: '100mb' })); app.use(express.urlencoded({ limit: '100mb', extended: true })); app.use(cookieParser()); // Middleware to parse cookies app.use(passport.initialize()); // Initialize Passport // Add a request timeout middleware. This will help prevent requests from hanging indefinitely. // We set a generous 5-minute timeout to accommodate slow AI processing for large flyers. app.use(timeout('5m')); // --- Logging Middleware --- const getDurationInMilliseconds = (start: [number, number]): number => { const NS_PER_SEC = 1e9; const NS_TO_MS = 1e6; const diff = process.hrtime(start); return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS; }; const requestLogger = (req: Request, res: Response, next: NextFunction) => { const start = process.hrtime(); const { method, originalUrl } = req; // If the request times out, log it. if (req.timedout) { logger.error(`REQUEST TIMEOUT: ${method} ${originalUrl} exceeded the 5m limit.`); } logger.debug(`[Request Logger] INCOMING: ${method} ${originalUrl}`); res.on('finish', () => { const user = req.user as { user_id?: string } | undefined; const durationInMilliseconds = getDurationInMilliseconds(start); const { statusCode } = res; const userIdentifier = user?.user_id ? ` (User: ${user.user_id})` : ''; const logMessage = `${method} ${originalUrl} ${statusCode} ${durationInMilliseconds.toFixed(2)}ms${userIdentifier}`; if (statusCode >= 500) logger.error(logMessage); else if (statusCode >= 400) logger.warn(logMessage); else logger.info(logMessage); }); next(); }; app.use(requestLogger); // Use the logging middleware for all requests // --- Security Warning --- if ((process.env.JWT_SECRET || 'your_super_secret_jwt_key_change_this') === 'your_super_secret_jwt_key_change_this') { logger.warn('Security Warning: JWT_SECRET is using a default, insecure value. Please set a strong secret in your .env file.'); } // --- API Routes --- // The order of route registration is critical. // More specific routes should be registered before more general ones. // 1. Authentication routes for login, registration, etc. app.use('/api/auth', authRouter); // 2. System routes for health checks, etc. app.use('/api/system', systemRouter); // 3. General authenticated user routes. app.use('/api/users', userRouter); // 4. AI routes, some of which use optional authentication. app.use('/api/ai', aiRouter); // 5. Admin routes, which are all protected by admin-level checks. app.use('/api/admin', adminRouter); // This seems to be missing from the original file list, but is required. // 6. Budgeting and spending analysis routes. app.use('/api/budgets', budgetRouter); // 7. Gamification routes for achievements. app.use('/api/achievements', gamificationRouter); // 8. Public routes that require no authentication. This should be last among the API routes. app.use('/api', publicRouter); // --- Error Handling and Server Startup --- // Basic error handling middleware app.use((err: Error, req: Request, res: Response, next: NextFunction) => { // Check if the error is from the timeout middleware if (req.timedout) { // The timeout event is already logged by the requestLogger, but we can add more detail here if needed. // The response is handled by the timeout middleware itself, so we don't send another one. return; } // The 4-argument signature is required for Express to identify this as an error-handling middleware. // We log the type of `next` to satisfy the linter rule against unused variables. logger.debug(`[Error Middleware] Encountered an error. The 'next' function is of type: ${typeof next}`); logger.error('Unhandled application error:', { error: err.stack, path: req.originalUrl }); if (!res.headersSent) { res.status(500).json({ message: 'Something broke!' }); } }); if (process.env.NODE_ENV !== 'test') { const PORT = process.env.PORT || 3001; app.listen(PORT, () => { logger.info(`Authentication server started on port ${PORT}`); console.log('--- REGISTERED API ROUTES ---'); console.table(listEndpoints(app)); console.log('-----------------------------'); }); // Start the scheduled background jobs startBackgroundJobs(); } // Export the app for integration testing export default app;