1- // src/server.js - Updated with all new route mounts
1+ // src/server.js - Updated with all security improvements
22/**
33 * Application entry point.
44 * Sets up Express, global middleware, mounts all routers, and handles startup/shutdown.
55 */
66
77import express from 'express' ;
88import session from 'express-session' ;
9+ import cors from 'cors' ;
10+ import helmet from 'helmet' ;
11+ import crypto from 'crypto' ;
912import authRoutes from './routes/auth.js' ;
1013import oauthRoutes from './routes/oauth2.js' ;
1114import accountsRoutes from './routes/accounts.js' ;
@@ -28,53 +31,114 @@ import { PORT } from './config/index.js';
2831import { swaggerUi , specs } from './config/swagger.js' ;
2932import { authenticateForDocs } from './auth/docsAuth.js' ;
3033import { errorHandler } from './middleware/errorHandler.js' ;
34+ import { requestIdMiddleware } from './middleware/requestId.js' ;
35+ import logger from './logging/logger.js' ;
3136
3237const app = express ( ) ;
3338const isProd = process . env . NODE_ENV === 'production' ;
3439
35- if ( isProd ) {
40+ // Trust proxy if behind reverse proxy
41+ if ( isProd || process . env . TRUST_PROXY === 'true' ) {
3642 app . set ( 'trust proxy' , 1 ) ;
3743}
3844
39- // Basic request logging to trace API calls and durations
45+ // Request ID tracking (first middleware)
46+ app . use ( requestIdMiddleware ) ;
47+
48+ // Security headers
49+ app . use ( helmet ( {
50+ contentSecurityPolicy : false , // API doesn't serve HTML
51+ hsts : {
52+ maxAge : 31536000 , // 1 year
53+ includeSubDomains : true ,
54+ preload : true ,
55+ } ,
56+ frameguard : { action : 'deny' } ,
57+ noSniff : true ,
58+ referrerPolicy : { policy : 'strict-origin-when-cross-origin' } ,
59+ } ) ) ;
60+
61+ // CORS configuration
62+ const allowedOrigins = process . env . ALLOWED_ORIGINS
63+ ? process . env . ALLOWED_ORIGINS . split ( ',' ) . map ( o => o . trim ( ) )
64+ : [ 'http://localhost:3000' , 'http://localhost:5678' ] ;
65+
66+ app . use ( cors ( {
67+ origin : ( origin , callback ) => {
68+ // Allow requests with no origin (mobile apps, Postman, etc.)
69+ if ( ! origin || allowedOrigins . includes ( origin ) ) {
70+ callback ( null , true ) ;
71+ } else {
72+ logger . warn ( 'CORS blocked request from origin' , { origin } ) ;
73+ callback ( new Error ( 'Not allowed by CORS' ) ) ;
74+ }
75+ } ,
76+ credentials : true ,
77+ methods : [ 'GET' , 'POST' , 'PUT' , 'DELETE' , 'OPTIONS' ] ,
78+ allowedHeaders : [ 'Content-Type' , 'Authorization' , 'X-Request-ID' ] ,
79+ maxAge : 86400 , // 24 hours
80+ } ) ) ;
81+
82+ // Request logging with structured logging
4083app . use ( ( req , res , next ) => {
4184 const started = Date . now ( ) ;
4285 res . on ( 'finish' , ( ) => {
4386 const duration = Date . now ( ) - started ;
44- console . log ( `[${ req . method } ] ${ req . originalUrl } -> ${ res . statusCode } (${ duration } ms)` ) ;
87+ logger . info ( 'Request completed' , {
88+ requestId : req . id ,
89+ method : req . method ,
90+ url : req . originalUrl ,
91+ status : res . statusCode ,
92+ duration : `${ duration } ms` ,
93+ ip : req . ip ,
94+ userAgent : req . get ( 'user-agent' ) ,
95+ } ) ;
4596 } ) ;
4697 next ( ) ;
4798} ) ;
4899
49- // Global middleware
100+ // Session configuration with security improvements
101+ if ( isProd && ! process . env . SESSION_SECRET ) {
102+ logger . error ( 'FATAL: SESSION_SECRET is required in production' ) ;
103+ process . exit ( 1 ) ;
104+ }
105+
106+ const sessionSecret = process . env . SESSION_SECRET || ( ( ) => {
107+ if ( ! isProd ) {
108+ const secret = crypto . randomBytes ( 32 ) . toString ( 'hex' ) ;
109+ logger . warn ( 'SESSION_SECRET not set; generated random secret for this session (will be different on restart)' ) ;
110+ return secret ;
111+ }
112+ } ) ( ) ;
113+
50114app . use ( session ( {
51- secret : process . env . SESSION_SECRET || 'dev-secret' ,
115+ secret : sessionSecret ,
52116 resave : false ,
53117 saveUninitialized : false ,
54118 cookie : {
55119 secure : isProd ,
56120 httpOnly : true ,
57- sameSite : isProd ? 'lax' : 'lax' ,
58- maxAge : 60 * 60 * 1000 ,
121+ sameSite : 'lax' ,
122+ maxAge : 60 * 60 * 1000 , // 1 hour
59123 } ,
124+ name : 'sessionId' , // Don't use default 'connect.sid'
60125} ) ) ;
61126
62- if ( ! process . env . SESSION_SECRET ) {
63- console . warn ( 'SESSION_SECRET not set; using development fallback.' ) ;
64- }
65-
66- app . use ( express . json ( ) ) ;
67- app . use ( express . urlencoded ( { extended : true } ) ) ;
127+ // Body parsing with size limits
128+ app . use ( express . json ( { limit : '10kb' } ) ) ;
129+ app . use ( express . urlencoded ( { limit : '10kb' , extended : true } ) ) ;
68130
69131// Serve static files (CSS, JS, HTML)
70- app . use ( '/static' , express . static ( './public/static' ) ) ;
132+ app . use ( '/static' , express . static ( './src/ public/static' ) ) ;
71133
72134// Mount routers
73135app . use ( '/auth' , authRoutes ) ;
74- app . use ( '/oauth' , oauthRoutes ) ;
75136app . use ( '/health' , healthRoutes ) ;
76137app . use ( loginRoutes ) ; // Root /login GET/POST
77138
139+ // Only mount OAuth routes if n8n is configured
140+ let n8nEnabled = false ;
141+
78142app . use ( '/accounts' , accountsRoutes ) ;
79143app . use ( '/transactions' , transactionsGlobalRoutes ) ; // Global update/delete by ID
80144
@@ -98,39 +162,76 @@ app.use(errorHandler);
98162// Startup sequence
99163( async ( ) => {
100164 try {
165+ logger . info ( 'Starting budget-api server...' ) ;
166+
101167 await ensureAdminUserHash ( ) ;
102- await ensureN8NClient ( ) ;
168+
169+ // Try to set up n8n OAuth2 if configured
170+ n8nEnabled = await ensureN8NClient ( ) ;
171+ if ( n8nEnabled ) {
172+ app . use ( '/oauth' , oauthRoutes ) ;
173+ logger . info ( 'n8n OAuth2 integration enabled' ) ;
174+ } else {
175+ logger . info ( 'n8n OAuth2 integration disabled (not configured)' ) ;
176+ }
177+
103178 await initActualApi ( ) ;
104- console . log ( '=== Startup complete ===' ) ;
179+
180+ logger . info ( 'Startup complete' , {
181+ port : PORT ,
182+ env : process . env . NODE_ENV || 'development' ,
183+ n8nOAuth : n8nEnabled ,
184+ } ) ;
105185 } catch ( err ) {
106- console . error ( 'Critical startup failure: ' , err ) ;
186+ logger . error ( 'Critical startup failure' , { error : err . message , stack : err . stack } ) ;
107187 process . exit ( 1 ) ;
108188 }
109189} ) ( ) ;
110190
111191// Graceful shutdown
112- process . on ( 'SIGTERM' , async ( ) => {
113- console . log ( 'SIGTERM received – shutting down...' ) ;
114- await shutdownActualApi ( ) ;
115- closeDb ( ) ;
116- process . exit ( 0 ) ;
192+ const shutdown = async ( signal ) => {
193+ logger . info ( `${ signal } received – shutting down gracefully...` ) ;
194+
195+ try {
196+ await shutdownActualApi ( ) ;
197+ closeDb ( ) ;
198+ logger . info ( 'Shutdown complete' ) ;
199+ process . exit ( 0 ) ;
200+ } catch ( err ) {
201+ logger . error ( 'Error during shutdown' , { error : err . message } ) ;
202+ process . exit ( 1 ) ;
203+ }
204+ } ;
205+
206+ process . on ( 'SIGTERM' , ( ) => shutdown ( 'SIGTERM' ) ) ;
207+ process . on ( 'SIGINT' , ( ) => shutdown ( 'SIGINT' ) ) ;
208+
209+ // Handle uncaught errors
210+ process . on ( 'uncaughtException' , ( err ) => {
211+ logger . error ( 'Uncaught exception' , { error : err . message , stack : err . stack } ) ;
212+ process . exit ( 1 ) ;
213+ } ) ;
214+
215+ process . on ( 'unhandledRejection' , ( reason , promise ) => {
216+ logger . error ( 'Unhandled rejection' , { reason, promise } ) ;
117217} ) ;
118218
119219app . listen ( PORT , ( ) => {
120- console . log ( `\n=== Server running on http://localhost:${ PORT } ===` ) ;
121- console . log ( 'Endpoints:' ) ;
122- console . log ( ' Health: GET /health' ) ;
123- console . log ( ' Login form: GET/POST /login (unified endpoint)' ) ;
124- console . log ( ' Auth: POST /auth/login' ) ;
125- console . log ( ' OAuth2: GET /oauth/authorize, POST /oauth/token' ) ;
126- console . log ( ' API Docs: GET /docs (login at /login?return_to=/docs)' ) ;
127- console . log ( ' Accounts: /accounts/*' ) ;
128- console . log ( ' Transactions:/transactions/* and /accounts/:accountId/transactions/*' ) ;
129- console . log ( ' Categories: /categories/*' ) ;
130- console . log ( ' Category Groups: /category-groups/*' ) ;
131- console . log ( ' Payees: /payees/*' ) ;
132- console . log ( ' Budgets: /budgets/*' ) ;
133- console . log ( ' Rules: /rules/*' ) ;
134- console . log ( ' Schedules: /schedules/*' ) ;
135- console . log ( ' Query: POST /query\n' ) ;
220+ logger . info ( `Server running on http://localhost:${ PORT } ` ) ;
221+ logger . info ( 'Available endpoints:' , {
222+ health : 'GET /health' ,
223+ login : 'GET/POST /login' ,
224+ auth : 'POST /auth/login, POST /auth/logout' ,
225+ oauth2 : n8nEnabled ? 'GET /oauth/authorize, POST /oauth/token' : 'disabled' ,
226+ docs : 'GET /docs' ,
227+ accounts : '/accounts/*' ,
228+ transactions : '/transactions/* and /accounts/:accountId/transactions/*' ,
229+ categories : '/categories/*' ,
230+ categoryGroups : '/category-groups/*' ,
231+ payees : '/payees/*' ,
232+ budgets : '/budgets/*' ,
233+ rules : '/rules/*' ,
234+ schedules : '/schedules/*' ,
235+ query : 'POST /query' ,
236+ } ) ;
136237} ) ;
0 commit comments