Initial commit
This commit is contained in:
478
backend/src/routes/jobs.js
Normal file
478
backend/src/routes/jobs.js
Normal file
@@ -0,0 +1,478 @@
|
||||
const express = require('express');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const pool = require('../database/connection');
|
||||
const { authenticateToken, requireRole } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get all jobs with filtering and search
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
search,
|
||||
location,
|
||||
employmentType,
|
||||
experienceLevel,
|
||||
skills,
|
||||
salaryMin,
|
||||
salaryMax,
|
||||
remoteAllowed,
|
||||
status = 'active',
|
||||
page = 1,
|
||||
limit = 10
|
||||
} = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT j.*, e.company_name, e.industry, e.company_size
|
||||
FROM jobs j
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
`;
|
||||
const queryParams = [];
|
||||
let paramCount = 0;
|
||||
const conditions = [];
|
||||
|
||||
// Always filter by status
|
||||
paramCount++;
|
||||
conditions.push(`j.status = $${paramCount}`);
|
||||
queryParams.push(status);
|
||||
|
||||
if (search) {
|
||||
paramCount++;
|
||||
conditions.push(`(j.title ILIKE $${paramCount} OR j.description ILIKE $${paramCount})`);
|
||||
queryParams.push(`%${search}%`);
|
||||
}
|
||||
|
||||
if (location) {
|
||||
paramCount++;
|
||||
conditions.push(`j.location ILIKE $${paramCount}`);
|
||||
queryParams.push(`%${location}%`);
|
||||
}
|
||||
|
||||
if (employmentType) {
|
||||
paramCount++;
|
||||
conditions.push(`j.employment_type = $${paramCount}`);
|
||||
queryParams.push(employmentType);
|
||||
}
|
||||
|
||||
if (experienceLevel) {
|
||||
paramCount++;
|
||||
conditions.push(`j.experience_level = $${paramCount}`);
|
||||
queryParams.push(experienceLevel);
|
||||
}
|
||||
|
||||
if (skills) {
|
||||
const skillArray = skills.split(',').map(s => s.trim());
|
||||
paramCount++;
|
||||
conditions.push(`j.skills_required && $${paramCount}`);
|
||||
queryParams.push(skillArray);
|
||||
}
|
||||
|
||||
if (salaryMin) {
|
||||
paramCount++;
|
||||
conditions.push(`j.salary_max >= $${paramCount}`);
|
||||
queryParams.push(parseInt(salaryMin));
|
||||
}
|
||||
|
||||
if (salaryMax) {
|
||||
paramCount++;
|
||||
conditions.push(`j.salary_min <= $${paramCount}`);
|
||||
queryParams.push(parseInt(salaryMax));
|
||||
}
|
||||
|
||||
if (remoteAllowed === 'true') {
|
||||
conditions.push(`j.remote_allowed = true`);
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query += ` WHERE ${conditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
query += ` ORDER BY j.created_at DESC`;
|
||||
|
||||
// Add pagination
|
||||
const offset = (page - 1) * limit;
|
||||
paramCount++;
|
||||
query += ` LIMIT $${paramCount}`;
|
||||
queryParams.push(limit);
|
||||
paramCount++;
|
||||
query += ` OFFSET $${paramCount}`;
|
||||
queryParams.push(offset);
|
||||
|
||||
const result = await pool.query(query, queryParams);
|
||||
|
||||
// Get total count for pagination
|
||||
let countQuery = `
|
||||
SELECT COUNT(*)
|
||||
FROM jobs j
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
`;
|
||||
const countParams = [];
|
||||
let countParamCount = 0;
|
||||
const countConditions = [];
|
||||
|
||||
countParamCount++;
|
||||
countConditions.push(`j.status = $${countParamCount}`);
|
||||
countParams.push(status);
|
||||
|
||||
if (search) {
|
||||
countParamCount++;
|
||||
countConditions.push(`(j.title ILIKE $${countParamCount} OR j.description ILIKE $${countParamCount})`);
|
||||
countParams.push(`%${search}%`);
|
||||
}
|
||||
|
||||
if (location) {
|
||||
countParamCount++;
|
||||
countConditions.push(`j.location ILIKE $${countParamCount}`);
|
||||
countParams.push(`%${location}%`);
|
||||
}
|
||||
|
||||
if (employmentType) {
|
||||
countParamCount++;
|
||||
countConditions.push(`j.employment_type = $${countParamCount}`);
|
||||
countParams.push(employmentType);
|
||||
}
|
||||
|
||||
if (experienceLevel) {
|
||||
countParamCount++;
|
||||
countConditions.push(`j.experience_level = $${countParamCount}`);
|
||||
countParams.push(experienceLevel);
|
||||
}
|
||||
|
||||
if (skills) {
|
||||
const skillArray = skills.split(',').map(s => s.trim());
|
||||
countParamCount++;
|
||||
countConditions.push(`j.skills_required && $${countParamCount}`);
|
||||
countParams.push(skillArray);
|
||||
}
|
||||
|
||||
if (salaryMin) {
|
||||
countParamCount++;
|
||||
countConditions.push(`j.salary_max >= $${countParamCount}`);
|
||||
countParams.push(parseInt(salaryMin));
|
||||
}
|
||||
|
||||
if (salaryMax) {
|
||||
countParamCount++;
|
||||
countConditions.push(`j.salary_min <= $${countParamCount}`);
|
||||
countParams.push(parseInt(salaryMax));
|
||||
}
|
||||
|
||||
if (remoteAllowed === 'true') {
|
||||
countConditions.push(`j.remote_allowed = true`);
|
||||
}
|
||||
|
||||
if (countConditions.length > 0) {
|
||||
countQuery += ` WHERE ${countConditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
const countResult = await pool.query(countQuery, countParams);
|
||||
|
||||
res.json({
|
||||
jobs: result.rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: parseInt(countResult.rows[0].count),
|
||||
pages: Math.ceil(countResult.rows[0].count / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get jobs error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch jobs' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get job by ID
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT j.*, e.company_name, e.industry, e.company_size, e.website, e.description as company_description
|
||||
FROM jobs j
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
WHERE j.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Get job error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch job' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create job posting
|
||||
router.post('/', authenticateToken, requireRole(['employer', 'recruiter']), [
|
||||
body('title').notEmpty().trim(),
|
||||
body('description').notEmpty().trim(),
|
||||
body('requirements').isArray(),
|
||||
body('responsibilities').isArray(),
|
||||
body('location').notEmpty().trim(),
|
||||
body('employmentType').isIn(['full-time', 'part-time', 'contract', 'internship']),
|
||||
body('salaryMin').optional().isInt({ min: 0 }),
|
||||
body('salaryMax').optional().isInt({ min: 0 }),
|
||||
body('currency').optional().isLength({ min: 3, max: 3 }),
|
||||
body('remoteAllowed').optional().isBoolean(),
|
||||
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
|
||||
body('skillsRequired').optional().isArray(),
|
||||
body('benefits').optional().isArray(),
|
||||
body('applicationDeadline').optional().isISO8601()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
requirements,
|
||||
responsibilities,
|
||||
location,
|
||||
employmentType,
|
||||
salaryMin,
|
||||
salaryMax,
|
||||
currency = 'USD',
|
||||
remoteAllowed = false,
|
||||
experienceLevel,
|
||||
skillsRequired,
|
||||
benefits,
|
||||
applicationDeadline
|
||||
} = req.body;
|
||||
|
||||
// Get employer_id for the current user
|
||||
let employerId;
|
||||
if (req.user.role === 'employer') {
|
||||
const employerResult = await pool.query(
|
||||
'SELECT id FROM employers WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (employerResult.rows.length === 0) {
|
||||
return res.status(400).json({ error: 'Employer profile not found' });
|
||||
}
|
||||
employerId = employerResult.rows[0].id;
|
||||
} else {
|
||||
// For recruiters, they need to specify which employer
|
||||
const { employerId: providedEmployerId } = req.body;
|
||||
if (!providedEmployerId) {
|
||||
return res.status(400).json({ error: 'Employer ID required for recruiters' });
|
||||
}
|
||||
employerId = providedEmployerId;
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO jobs (employer_id, title, description, requirements, responsibilities, location, employment_type, salary_min, salary_max, currency, remote_allowed, experience_level, skills_required, benefits, application_deadline)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING *
|
||||
`, [employerId, title, description, requirements, responsibilities, location, employmentType, salaryMin, salaryMax, currency, remoteAllowed, experienceLevel, skillsRequired, benefits, applicationDeadline]);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Job posting created successfully',
|
||||
job: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create job error:', error);
|
||||
res.status(500).json({ error: 'Failed to create job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update job posting
|
||||
router.put('/:id', authenticateToken, [
|
||||
body('title').optional().notEmpty().trim(),
|
||||
body('description').optional().notEmpty().trim(),
|
||||
body('requirements').optional().isArray(),
|
||||
body('responsibilities').optional().isArray(),
|
||||
body('location').optional().notEmpty().trim(),
|
||||
body('employmentType').optional().isIn(['full-time', 'part-time', 'contract', 'internship']),
|
||||
body('salaryMin').optional().isInt({ min: 0 }),
|
||||
body('salaryMax').optional().isInt({ min: 0 }),
|
||||
body('currency').optional().isLength({ min: 3, max: 3 }),
|
||||
body('status').optional().isIn(['active', 'paused', 'closed', 'draft']),
|
||||
body('remoteAllowed').optional().isBoolean(),
|
||||
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
|
||||
body('skillsRequired').optional().isArray(),
|
||||
body('benefits').optional().isArray(),
|
||||
body('applicationDeadline').optional().isISO8601()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
requirements,
|
||||
responsibilities,
|
||||
location,
|
||||
employmentType,
|
||||
salaryMin,
|
||||
salaryMax,
|
||||
currency,
|
||||
status,
|
||||
remoteAllowed,
|
||||
experienceLevel,
|
||||
skillsRequired,
|
||||
benefits,
|
||||
applicationDeadline
|
||||
} = req.body;
|
||||
|
||||
// Check if job exists and user has permission
|
||||
const jobResult = await pool.query(`
|
||||
SELECT j.*, e.user_id as employer_user_id
|
||||
FROM jobs j
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
WHERE j.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (jobResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
const job = jobResult.rows[0];
|
||||
|
||||
// Users can only update jobs from their own employer unless they're admin
|
||||
if (job.employer_user_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const updateFields = [];
|
||||
const updateValues = [];
|
||||
let paramCount = 1;
|
||||
|
||||
if (title) {
|
||||
updateFields.push(`title = $${paramCount}`);
|
||||
updateValues.push(title);
|
||||
paramCount++;
|
||||
}
|
||||
if (description) {
|
||||
updateFields.push(`description = $${paramCount}`);
|
||||
updateValues.push(description);
|
||||
paramCount++;
|
||||
}
|
||||
if (requirements) {
|
||||
updateFields.push(`requirements = $${paramCount}`);
|
||||
updateValues.push(requirements);
|
||||
paramCount++;
|
||||
}
|
||||
if (responsibilities) {
|
||||
updateFields.push(`responsibilities = $${paramCount}`);
|
||||
updateValues.push(responsibilities);
|
||||
paramCount++;
|
||||
}
|
||||
if (location) {
|
||||
updateFields.push(`location = $${paramCount}`);
|
||||
updateValues.push(location);
|
||||
paramCount++;
|
||||
}
|
||||
if (employmentType) {
|
||||
updateFields.push(`employment_type = $${paramCount}`);
|
||||
updateValues.push(employmentType);
|
||||
paramCount++;
|
||||
}
|
||||
if (salaryMin !== undefined) {
|
||||
updateFields.push(`salary_min = $${paramCount}`);
|
||||
updateValues.push(salaryMin);
|
||||
paramCount++;
|
||||
}
|
||||
if (salaryMax !== undefined) {
|
||||
updateFields.push(`salary_max = $${paramCount}`);
|
||||
updateValues.push(salaryMax);
|
||||
paramCount++;
|
||||
}
|
||||
if (currency) {
|
||||
updateFields.push(`currency = $${paramCount}`);
|
||||
updateValues.push(currency);
|
||||
paramCount++;
|
||||
}
|
||||
if (status) {
|
||||
updateFields.push(`status = $${paramCount}`);
|
||||
updateValues.push(status);
|
||||
paramCount++;
|
||||
}
|
||||
if (remoteAllowed !== undefined) {
|
||||
updateFields.push(`remote_allowed = $${paramCount}`);
|
||||
updateValues.push(remoteAllowed);
|
||||
paramCount++;
|
||||
}
|
||||
if (experienceLevel) {
|
||||
updateFields.push(`experience_level = $${paramCount}`);
|
||||
updateValues.push(experienceLevel);
|
||||
paramCount++;
|
||||
}
|
||||
if (skillsRequired) {
|
||||
updateFields.push(`skills_required = $${paramCount}`);
|
||||
updateValues.push(skillsRequired);
|
||||
paramCount++;
|
||||
}
|
||||
if (benefits) {
|
||||
updateFields.push(`benefits = $${paramCount}`);
|
||||
updateValues.push(benefits);
|
||||
paramCount++;
|
||||
}
|
||||
if (applicationDeadline) {
|
||||
updateFields.push(`application_deadline = $${paramCount}`);
|
||||
updateValues.push(applicationDeadline);
|
||||
paramCount++;
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({ error: 'No fields to update' });
|
||||
}
|
||||
|
||||
updateValues.push(id);
|
||||
const query = `UPDATE jobs SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING *`;
|
||||
|
||||
const result = await pool.query(query, updateValues);
|
||||
|
||||
res.json({
|
||||
message: 'Job posting updated successfully',
|
||||
job: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update job error:', error);
|
||||
res.status(500).json({ error: 'Failed to update job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete job posting
|
||||
router.delete('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Check if job exists and user has permission
|
||||
const jobResult = await pool.query(`
|
||||
SELECT j.*, e.user_id as employer_user_id
|
||||
FROM jobs j
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
WHERE j.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (jobResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
const job = jobResult.rows[0];
|
||||
|
||||
// Users can only delete jobs from their own employer unless they're admin
|
||||
if (job.employer_user_id !== req.user.id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM jobs WHERE id = $1', [id]);
|
||||
|
||||
res.json({ message: 'Job posting deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete job error:', error);
|
||||
res.status(500).json({ error: 'Failed to delete job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user