Resume / backend /src /server.js
Resume Builder Dev
feat(export): switch resume export/import from PDF to DOCX
f4623fe
Raw
History Blame Contribute Delete
7.58 kB
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;