Initial commit: AI Recruitment Site for Ryans Recruit Firm
- Complete PostgreSQL schema with migrations - Node.js/Express backend with authentication - Public website (home, about, services, jobs, apply, contact) - Admin dashboard with applicant and job management - CV upload and storage in PostgreSQL BYTEA - Docker Compose setup for deployment - Session-based authentication - Responsive design with Ryan brand colors
This commit is contained in:
669
server.js
Normal file
669
server.js
Normal file
@@ -0,0 +1,669 @@
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { Pool } = require('pg');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// PostgreSQL connection
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'postgres',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'recruitment',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
// Test database connection
|
||||
pool.query('SELECT NOW()', (err, res) => {
|
||||
if (err) {
|
||||
console.error('Database connection error:', err);
|
||||
} else {
|
||||
console.log('Database connected successfully at:', res.rows[0].now);
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'recruitment-site-secret-key-change-in-production',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true if using HTTPS
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Middleware to handle routes without .html extension
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.indexOf('.') === -1 && !req.path.startsWith('/api/') && !req.path.startsWith('/admin/')) {
|
||||
const file = `${req.path}.html`;
|
||||
res.sendFile(path.join(__dirname, 'public', file), (err) => {
|
||||
if (err) next();
|
||||
});
|
||||
} else if (req.path.indexOf('.') === -1 && req.path.startsWith('/admin/') && req.path !== '/admin/') {
|
||||
const file = `${req.path}.html`;
|
||||
res.sendFile(path.join(__dirname, 'public', file), (err) => {
|
||||
if (err) next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Configure multer for file uploads (memory storage for CV uploads)
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024, // 5MB limit
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = /pdf|doc|docx/;
|
||||
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
|
||||
const mimetype = allowedTypes.test(file.mimetype) ||
|
||||
file.mimetype === 'application/msword' ||
|
||||
file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
|
||||
if (mimetype && extname) {
|
||||
return cb(null, true);
|
||||
}
|
||||
cb(new Error('Only PDF, DOC, and DOCX files are allowed'));
|
||||
}
|
||||
});
|
||||
|
||||
// Auth middleware
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (req.session.adminId) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PUBLIC API ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
// Get all active job postings
|
||||
app.get('/api/jobs', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT id, title, department, location, employment_type, salary_range, description, requirements, benefits, created_at FROM job_postings WHERE is_active = true ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error fetching jobs:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch jobs' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single job posting
|
||||
app.get('/api/jobs/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT id, title, department, location, employment_type, salary_range, description, requirements, benefits, created_at FROM job_postings WHERE id = $1 AND is_active = true',
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error fetching job:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch job' });
|
||||
}
|
||||
});
|
||||
|
||||
// Submit job application with CV
|
||||
app.post('/api/apply', upload.single('cv'), async (req, res) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const {
|
||||
fullName,
|
||||
email,
|
||||
phone,
|
||||
linkedinUrl,
|
||||
portfolioUrl,
|
||||
yearsOfExperience,
|
||||
currentPosition,
|
||||
currentCompany,
|
||||
preferredLocation,
|
||||
jobId,
|
||||
coverLetter
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!fullName || !email || !req.file) {
|
||||
return res.status(400).json({ error: 'Name, email, and CV are required' });
|
||||
}
|
||||
|
||||
// Insert applicant
|
||||
const applicantResult = await client.query(
|
||||
`INSERT INTO applicants (full_name, email, phone, linkedin_url, portfolio_url, years_of_experience, current_position, current_company, preferred_location)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id`,
|
||||
[fullName, email, phone, linkedinUrl, portfolioUrl, yearsOfExperience, currentPosition, currentCompany, preferredLocation]
|
||||
);
|
||||
|
||||
const applicantId = applicantResult.rows[0].id;
|
||||
|
||||
// Insert application with CV
|
||||
await client.query(
|
||||
`INSERT INTO applications (applicant_id, job_id, cover_letter, cv_filename, cv_content_type, cv_file, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'new')`,
|
||||
[
|
||||
applicantId,
|
||||
jobId || null,
|
||||
coverLetter,
|
||||
req.file.originalname,
|
||||
req.file.mimetype,
|
||||
req.file.buffer
|
||||
]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Application submitted successfully',
|
||||
applicantId
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('Error submitting application:', err);
|
||||
res.status(500).json({ error: 'Failed to submit application' });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
// Submit contact form
|
||||
app.post('/api/contact', async (req, res) => {
|
||||
try {
|
||||
const { name, email, subject, message } = req.body;
|
||||
|
||||
if (!name || !email || !message) {
|
||||
return res.status(400).json({ error: 'Name, email, and message are required' });
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO contact_submissions (name, email, subject, message) VALUES ($1, $2, $3, $4)',
|
||||
[name, email, subject, message]
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Message sent successfully' });
|
||||
} catch (err) {
|
||||
console.error('Error submitting contact form:', err);
|
||||
res.status(500).json({ error: 'Failed to send message' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ADMIN AUTHENTICATION ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
// Admin login (creates admin on first login if none exist)
|
||||
app.post('/api/admin/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password, fullName } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email and password are required' });
|
||||
}
|
||||
|
||||
// Check if any admins exist
|
||||
const adminCount = await pool.query('SELECT COUNT(*) FROM admins');
|
||||
const isFirstAdmin = adminCount.rows[0].count === '0';
|
||||
|
||||
if (isFirstAdmin) {
|
||||
// Create first admin
|
||||
if (!fullName) {
|
||||
return res.status(400).json({ error: 'Full name is required for first admin creation' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const result = await pool.query(
|
||||
'INSERT INTO admins (email, password_hash, full_name) VALUES ($1, $2, $3) RETURNING id, email, full_name',
|
||||
[email, hashedPassword, fullName]
|
||||
);
|
||||
|
||||
const admin = result.rows[0];
|
||||
req.session.adminId = admin.id;
|
||||
req.session.adminEmail = admin.email;
|
||||
req.session.adminName = admin.full_name;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Admin account created successfully',
|
||||
admin: { id: admin.id, email: admin.email, fullName: admin.full_name },
|
||||
isFirstLogin: true
|
||||
});
|
||||
}
|
||||
|
||||
// Regular login
|
||||
const result = await pool.query(
|
||||
'SELECT id, email, password_hash, full_name FROM admins WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const admin = result.rows[0];
|
||||
const passwordMatch = await bcrypt.compare(password, admin.password_hash);
|
||||
|
||||
if (!passwordMatch) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await pool.query('UPDATE admins SET last_login = CURRENT_TIMESTAMP WHERE id = $1', [admin.id]);
|
||||
|
||||
req.session.adminId = admin.id;
|
||||
req.session.adminEmail = admin.email;
|
||||
req.session.adminName = admin.full_name;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
admin: { id: admin.id, email: admin.email, fullName: admin.full_name }
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error during login:', err);
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Check if admin is logged in
|
||||
app.get('/api/admin/check', (req, res) => {
|
||||
if (req.session.adminId) {
|
||||
res.json({
|
||||
loggedIn: true,
|
||||
admin: {
|
||||
id: req.session.adminId,
|
||||
email: req.session.adminEmail,
|
||||
fullName: req.session.adminName
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.json({ loggedIn: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Check if first admin needs to be created
|
||||
app.get('/api/admin/check-first', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT COUNT(*) FROM admins');
|
||||
const isFirstAdmin = result.rows[0].count === '0';
|
||||
res.json({ isFirstAdmin });
|
||||
} catch (err) {
|
||||
console.error('Error checking first admin:', err);
|
||||
res.status(500).json({ error: 'Failed to check admin status' });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin logout
|
||||
app.post('/api/admin/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Logout failed' });
|
||||
}
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ADMIN API ENDPOINTS (Protected)
|
||||
// ============================================
|
||||
|
||||
// Get dashboard statistics
|
||||
app.get('/api/admin/stats', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const stats = await pool.query(`
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM applications WHERE status = 'new') as new_applications,
|
||||
(SELECT COUNT(*) FROM applications) as total_applications,
|
||||
(SELECT COUNT(*) FROM applicants) as total_applicants,
|
||||
(SELECT COUNT(*) FROM job_postings WHERE is_active = true) as active_jobs,
|
||||
(SELECT COUNT(*) FROM contact_submissions WHERE is_read = false) as unread_messages
|
||||
`);
|
||||
|
||||
res.json(stats.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error fetching stats:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch statistics' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all applications with applicant details
|
||||
app.get('/api/admin/applications', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { status, jobId, search } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
app.id,
|
||||
app.status,
|
||||
app.applied_at,
|
||||
app.cv_filename,
|
||||
app.cover_letter,
|
||||
app.notes,
|
||||
applicant.id as applicant_id,
|
||||
applicant.full_name,
|
||||
applicant.email,
|
||||
applicant.phone,
|
||||
applicant.years_of_experience,
|
||||
applicant.current_position,
|
||||
applicant.current_company,
|
||||
job.id as job_id,
|
||||
job.title as job_title
|
||||
FROM applications app
|
||||
INNER JOIN applicants applicant ON app.applicant_id = applicant.id
|
||||
LEFT JOIN job_postings job ON app.job_id = job.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
let paramCount = 0;
|
||||
|
||||
if (status) {
|
||||
paramCount++;
|
||||
query += ` AND app.status = $${paramCount}`;
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (jobId) {
|
||||
paramCount++;
|
||||
query += ` AND app.job_id = $${paramCount}`;
|
||||
params.push(jobId);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
paramCount++;
|
||||
query += ` AND (applicant.full_name ILIKE $${paramCount} OR applicant.email ILIKE $${paramCount})`;
|
||||
params.push(`%${search}%`);
|
||||
}
|
||||
|
||||
query += ' ORDER BY app.applied_at DESC';
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error fetching applications:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch applications' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single application details
|
||||
app.get('/api/admin/applications/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
app.*,
|
||||
applicant.full_name,
|
||||
applicant.email,
|
||||
applicant.phone,
|
||||
applicant.linkedin_url,
|
||||
applicant.portfolio_url,
|
||||
applicant.years_of_experience,
|
||||
applicant.current_position,
|
||||
applicant.current_company,
|
||||
applicant.preferred_location,
|
||||
job.title as job_title
|
||||
FROM applications app
|
||||
INNER JOIN applicants applicant ON app.applicant_id = applicant.id
|
||||
LEFT JOIN job_postings job ON app.job_id = job.id
|
||||
WHERE app.id = $1
|
||||
`, [req.params.id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Application not found' });
|
||||
}
|
||||
|
||||
// Don't send the CV file in this response
|
||||
const application = result.rows[0];
|
||||
delete application.cv_file;
|
||||
|
||||
res.json(application);
|
||||
} catch (err) {
|
||||
console.error('Error fetching application:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch application' });
|
||||
}
|
||||
});
|
||||
|
||||
// Download CV
|
||||
app.get('/api/admin/applications/:id/cv', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT cv_file, cv_filename, cv_content_type FROM applications WHERE id = $1',
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0 || !result.rows[0].cv_file) {
|
||||
return res.status(404).json({ error: 'CV not found' });
|
||||
}
|
||||
|
||||
const { cv_file, cv_filename, cv_content_type } = result.rows[0];
|
||||
|
||||
res.setHeader('Content-Type', cv_content_type);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${cv_filename}"`);
|
||||
res.send(cv_file);
|
||||
} catch (err) {
|
||||
console.error('Error downloading CV:', err);
|
||||
res.status(500).json({ error: 'Failed to download CV' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update application status
|
||||
app.patch('/api/admin/applications/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { status, notes } = req.body;
|
||||
|
||||
const result = await pool.query(
|
||||
'UPDATE applications SET status = COALESCE($1, status), notes = COALESCE($2, notes) WHERE id = $3 RETURNING *',
|
||||
[status, notes, req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Application not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error updating application:', err);
|
||||
res.status(500).json({ error: 'Failed to update application' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all job postings (admin view - includes inactive)
|
||||
app.get('/api/admin/jobs', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM job_postings ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error fetching jobs:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch jobs' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create job posting
|
||||
app.post('/api/admin/jobs', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
department,
|
||||
location,
|
||||
employmentType,
|
||||
salaryRange,
|
||||
description,
|
||||
requirements,
|
||||
benefits
|
||||
} = req.body;
|
||||
|
||||
if (!title || !description) {
|
||||
return res.status(400).json({ error: 'Title and description are required' });
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO job_postings
|
||||
(title, department, location, employment_type, salary_range, description, requirements, benefits, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING *`,
|
||||
[title, department, location, employmentType, salaryRange, description, requirements, benefits, req.session.adminId]
|
||||
);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error creating job:', err);
|
||||
res.status(500).json({ error: 'Failed to create job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update job posting
|
||||
app.patch('/api/admin/jobs/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
department,
|
||||
location,
|
||||
employmentType,
|
||||
salaryRange,
|
||||
description,
|
||||
requirements,
|
||||
benefits,
|
||||
isActive
|
||||
} = req.body;
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE job_postings
|
||||
SET title = COALESCE($1, title),
|
||||
department = COALESCE($2, department),
|
||||
location = COALESCE($3, location),
|
||||
employment_type = COALESCE($4, employment_type),
|
||||
salary_range = COALESCE($5, salary_range),
|
||||
description = COALESCE($6, description),
|
||||
requirements = COALESCE($7, requirements),
|
||||
benefits = COALESCE($8, benefits),
|
||||
is_active = COALESCE($9, is_active)
|
||||
WHERE id = $10
|
||||
RETURNING *`,
|
||||
[title, department, location, employmentType, salaryRange, description, requirements, benefits, isActive, req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error updating job:', err);
|
||||
res.status(500).json({ error: 'Failed to update job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete job posting
|
||||
app.delete('/api/admin/jobs/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'DELETE FROM job_postings WHERE id = $1 RETURNING id',
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Job deleted successfully' });
|
||||
} catch (err) {
|
||||
console.error('Error deleting job:', err);
|
||||
res.status(500).json({ error: 'Failed to delete job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get contact submissions
|
||||
app.get('/api/admin/contacts', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM contact_submissions ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error fetching contacts:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch contact submissions' });
|
||||
}
|
||||
});
|
||||
|
||||
// Mark contact as read
|
||||
app.patch('/api/admin/contacts/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'UPDATE contact_submissions SET is_read = true WHERE id = $1 RETURNING *',
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Contact submission not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error updating contact:', err);
|
||||
res.status(500).json({ error: 'Failed to update contact submission' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ERROR HANDLING
|
||||
// ============================================
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({ error: 'File size too large. Maximum size is 5MB.' });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
|
||||
console.error('Unhandled error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// START SERVER
|
||||
// ============================================
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`AI Recruitment Site running on port ${PORT}`);
|
||||
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`Database: ${process.env.DB_HOST || 'postgres'}:${process.env.DB_PORT || 5432}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM signal received: closing HTTP server');
|
||||
pool.end(() => {
|
||||
console.log('Database pool closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user