Spaces:
Running
Running
| console.log('>>> [BOOT] Process started. Node version:', process.version); | |
| require('dotenv').config(); | |
| require('express-async-errors'); | |
| const express = require('express'); | |
| const cors = require('cors'); | |
| const helmet = require('helmet'); | |
| const morgan = require('morgan'); | |
| const compression = require('compression'); | |
| const rateLimit = require('express-rate-limit'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| // βββ Ensure directories exist (BEFORE logger) ββββββββββββββββββββββββββββββββββ | |
| try { | |
| ['uploads/resumes','uploads/avatars','exports','logs','data'].forEach((dir) => { | |
| const p = path.join(__dirname, '..', dir); | |
| if (!fs.existsSync(p)) { | |
| fs.mkdirSync(p, { recursive: true }); | |
| } | |
| }); | |
| } catch (err) { | |
| console.error('Failed to create initial directories:', err); | |
| } | |
| const logger = require('./config/logger'); | |
| const { getDb } = require('./config/database'); | |
| const errorHandler = require('./middleware/errorHandler'); | |
| // βββ Route modules ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const authRoutes = require('./routes/auth'); | |
| const resumeRoutes = require('./routes/resumes'); | |
| const templateRoutes = require('./routes/templates'); | |
| const importRoutes = require('./routes/import'); | |
| const exportRoutes = require('./routes/export'); | |
| const tipsRoutes = require('./routes/tips'); | |
| const aiRoutes = require('./routes/ai'); | |
| // βββ App ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const app = express(); | |
| // Security | |
| app.use(helmet({ | |
| contentSecurityPolicy: false, | |
| crossOriginResourcePolicy: false, | |
| frameguard: false, // Allow iframing from the frontend | |
| })); | |
| app.use(cors({ | |
| origin: function(origin, callback) { callback(null, true); }, | |
| methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], | |
| allowedHeaders: ['Content-Type', 'Authorization'], | |
| credentials: true, | |
| })); | |
| // Compression & parsing | |
| app.use(compression()); | |
| app.use(express.json({ limit: '5mb' })); | |
| app.use(express.urlencoded({ extended: true, limit: '5mb' })); | |
| // Logging | |
| if (process.env.NODE_ENV !== 'test') { | |
| app.use(morgan('combined', { | |
| stream: { write: (msg) => logger.info(msg.trim()) }, | |
| })); | |
| } | |
| // Rate limiting | |
| const limiter = rateLimit({ | |
| windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 15 * 60 * 1000, | |
| max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 200, | |
| standardHeaders: true, | |
| legacyHeaders: false, | |
| message: { success: false, message: 'Too many requests, please try again later.' }, | |
| }); | |
| // app.use('/api', limiter); | |
| // Stricter rate limit for auth endpoints | |
| const authLimiter = rateLimit({ | |
| windowMs: 15 * 60 * 1000, | |
| max: 20, | |
| message: { success: false, message: 'Too many auth attempts, please try again in 15 minutes.' }, | |
| }); | |
| // app.use('/api/auth', authLimiter); | |
| // βββ Static files βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use('/uploads', express.static(path.join(__dirname, '../uploads'))); | |
| // Serve Frontend Static Files β disable caching for JS/CSS so edits show immediately | |
| app.use(express.static(path.join(__dirname, '../../frontend'), { | |
| setHeaders(res, filePath) { | |
| if (/\.(js|css|html)$/.test(filePath)) { | |
| res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); | |
| res.setHeader('Pragma', 'no-cache'); | |
| res.setHeader('Expires', '0'); | |
| } | |
| } | |
| })); | |
| // βββ Public Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Welcome route | |
| app.get('/', (req, res) => { | |
| res.json({ | |
| success: true, | |
| message: 'π Resume Builder API is LIVE', | |
| version: '1.0.0', | |
| docs: '/api', | |
| health: '/health' | |
| }); | |
| }); | |
| // Health check | |
| app.get('/health', async (_req, res) => { | |
| try { | |
| const db = await getDb(); | |
| const result = await db.get('SELECT COUNT(*) AS c FROM templates'); | |
| res.json({ | |
| status: 'ok', | |
| version: '1.0.0', | |
| env: process.env.NODE_ENV, | |
| database: 'connected', | |
| templates: result ? result.c : 0, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ status: 'error', database: 'disconnected', message: err.message }); | |
| } | |
| }); | |
| // βββ API routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use('/api/auth', authRoutes); | |
| app.use('/api/resumes', resumeRoutes); | |
| app.use('/api/templates', templateRoutes); | |
| app.use('/api/import', importRoutes); | |
| app.use('/api/export', exportRoutes); | |
| app.use('/api/tips', tipsRoutes); | |
| app.use('/api/ai', aiRoutes); | |
| // βββ API docs (mini) βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get('/api', (_req, res) => { | |
| res.json({ | |
| name: 'Resume Builder API', | |
| version: '1.0.0', | |
| endpoints: { | |
| auth: '/api/auth β register Β· login Β· refresh Β· me', | |
| resumes: '/api/resumes β CRUD Β· ATS Β· design Β· order Β· versions Β· duplicate', | |
| templates: '/api/templates β list Β· categories Β· single', | |
| import: '/api/import β upload DOCX/image Β· jobs', | |
| export: '/api/export β Word download Β· HTML preview Β· history', | |
| tips: '/api/tips β section tips Β· random tip', | |
| }, | |
| }); | |
| }); | |
| // βββ Catch-all for Frontend SPA ββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get('*', (req, res, next) => { | |
| if (req.originalUrl.startsWith('/api')) { | |
| return next(); // Fall through to 404 handler | |
| } | |
| res.sendFile(path.join(__dirname, '../../frontend/index.html')); | |
| }); | |
| // βββ 404 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use((_req, res) => { | |
| res.status(404).json({ success: false, message: 'Route not found' }); | |
| }); | |
| // βββ Error handler ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use(errorHandler); | |
| // βββ Boot ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const PORT = parseInt(process.env.PORT, 10) || 5000; | |
| if (process.env.NODE_ENV !== 'test') { | |
| app.listen(PORT, '0.0.0.0', () => { | |
| logger.info(`π Resume Builder API starting up...`); | |
| logger.info(`π‘ Listening on 0.0.0.0:${PORT}`); | |
| logger.info(`π API docs: http://localhost:${PORT}/api`); | |
| logger.info(`π₯ Health: http://localhost:${PORT}/health`); | |
| }); | |
| } | |
| module.exports = app; | |