- Removed migrations volume mount from docker-compose.yml - Added automatic migration runner in server.js on startup - Migrations now run from files built into Docker image - Fixes 'relation does not exist' errors
707 lines
21 KiB
JavaScript
707 lines
21 KiB
JavaScript
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 fs = require('fs');
|
|
|
|
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,
|
|
});
|
|
|
|
// Run database migrations
|
|
async function runMigrations() {
|
|
try {
|
|
console.log('Running database migrations...');
|
|
const migrationsDir = path.join(__dirname, 'migrations');
|
|
const migrationFiles = fs.readdirSync(migrationsDir).sort();
|
|
|
|
for (const file of migrationFiles) {
|
|
if (file.endsWith('.sql')) {
|
|
console.log(`Running migration: ${file}`);
|
|
const migrationSQL = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
|
|
await pool.query(migrationSQL);
|
|
console.log(`✓ Migration ${file} completed`);
|
|
}
|
|
}
|
|
|
|
console.log('All migrations completed successfully');
|
|
} catch (error) {
|
|
console.error('Migration error:', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Test database connection and run migrations
|
|
pool.query('SELECT NOW()')
|
|
.then((res) => {
|
|
console.log('Database connected successfully at:', res.rows[0].now);
|
|
return runMigrations();
|
|
})
|
|
.then(() => {
|
|
console.log('Database initialization complete');
|
|
})
|
|
.catch((err) => {
|
|
console.error('Database initialization error:', err.message);
|
|
console.error('Application will continue but database operations will fail');
|
|
});
|
|
|
|
// 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
|
|
}
|
|
}));
|
|
|
|
// Serve static files first
|
|
app.use(express.static('public'));
|
|
|
|
// Middleware to handle routes without .html extension
|
|
app.use((req, res, next) => {
|
|
// Skip API routes
|
|
if (req.path.startsWith('/api/')) {
|
|
return next();
|
|
}
|
|
|
|
// Handle root path
|
|
if (req.path === '/') {
|
|
return res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
}
|
|
|
|
// Handle paths without extensions (not already handled by static middleware)
|
|
if (req.path.indexOf('.') === -1) {
|
|
const file = `${req.path}.html`;
|
|
return res.sendFile(path.join(__dirname, 'public', file), (err) => {
|
|
if (err) {
|
|
next();
|
|
}
|
|
});
|
|
}
|
|
|
|
next();
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
});
|