Spaces:
Running
Running
File size: 7,583 Bytes
92a138e f4623fe 92a138e f4623fe 92a138e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | 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;
|