Initial commit
This commit is contained in:
424
backend/src/routes/applications.js
Normal file
424
backend/src/routes/applications.js
Normal file
@@ -0,0 +1,424 @@
|
||||
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 applications (with filtering)
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
jobId,
|
||||
candidateId,
|
||||
status,
|
||||
employerId,
|
||||
page = 1,
|
||||
limit = 10
|
||||
} = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT a.*,
|
||||
j.title as job_title,
|
||||
j.employer_id,
|
||||
e.company_name,
|
||||
c.user_id as candidate_user_id,
|
||||
u.first_name,
|
||||
u.last_name,
|
||||
u.email as candidate_email
|
||||
FROM applications a
|
||||
JOIN jobs j ON a.job_id = j.id
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
JOIN candidates c ON a.candidate_id = c.id
|
||||
JOIN users u ON c.user_id = u.id
|
||||
`;
|
||||
const queryParams = [];
|
||||
let paramCount = 0;
|
||||
const conditions = [];
|
||||
|
||||
if (jobId) {
|
||||
paramCount++;
|
||||
conditions.push(`a.job_id = $${paramCount}`);
|
||||
queryParams.push(jobId);
|
||||
}
|
||||
|
||||
if (candidateId) {
|
||||
paramCount++;
|
||||
conditions.push(`a.candidate_id = $${paramCount}`);
|
||||
queryParams.push(candidateId);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
paramCount++;
|
||||
conditions.push(`a.status = $${paramCount}`);
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
if (employerId) {
|
||||
paramCount++;
|
||||
conditions.push(`j.employer_id = $${paramCount}`);
|
||||
queryParams.push(employerId);
|
||||
}
|
||||
|
||||
// Role-based filtering
|
||||
if (req.user.role === 'candidate') {
|
||||
// Candidates can only see their own applications
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (candidateResult.rows.length > 0) {
|
||||
paramCount++;
|
||||
conditions.push(`a.candidate_id = $${paramCount}`);
|
||||
queryParams.push(candidateResult.rows[0].id);
|
||||
}
|
||||
} else if (req.user.role === 'employer') {
|
||||
// Employers can only see applications for their jobs
|
||||
const employerResult = await pool.query(
|
||||
'SELECT id FROM employers WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (employerResult.rows.length > 0) {
|
||||
paramCount++;
|
||||
conditions.push(`j.employer_id = $${paramCount}`);
|
||||
queryParams.push(employerResult.rows[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query += ` WHERE ${conditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
query += ` ORDER BY a.applied_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 applications a
|
||||
JOIN jobs j ON a.job_id = j.id
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
JOIN candidates c ON a.candidate_id = c.id
|
||||
`;
|
||||
const countParams = [];
|
||||
let countParamCount = 0;
|
||||
const countConditions = [];
|
||||
|
||||
if (jobId) {
|
||||
countParamCount++;
|
||||
countConditions.push(`a.job_id = $${countParamCount}`);
|
||||
countParams.push(jobId);
|
||||
}
|
||||
|
||||
if (candidateId) {
|
||||
countParamCount++;
|
||||
countConditions.push(`a.candidate_id = $${countParamCount}`);
|
||||
countParams.push(candidateId);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
countParamCount++;
|
||||
countConditions.push(`a.status = $${countParamCount}`);
|
||||
countParams.push(status);
|
||||
}
|
||||
|
||||
if (employerId) {
|
||||
countParamCount++;
|
||||
countConditions.push(`j.employer_id = $${countParamCount}`);
|
||||
countParams.push(employerId);
|
||||
}
|
||||
|
||||
// Role-based filtering for count
|
||||
if (req.user.role === 'candidate') {
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (candidateResult.rows.length > 0) {
|
||||
countParamCount++;
|
||||
countConditions.push(`a.candidate_id = $${countParamCount}`);
|
||||
countParams.push(candidateResult.rows[0].id);
|
||||
}
|
||||
} else 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) {
|
||||
countParamCount++;
|
||||
countConditions.push(`j.employer_id = $${countParamCount}`);
|
||||
countParams.push(employerResult.rows[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
if (countConditions.length > 0) {
|
||||
countQuery += ` WHERE ${countConditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
const countResult = await pool.query(countQuery, countParams);
|
||||
|
||||
res.json({
|
||||
applications: 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 applications error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch applications' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get application by ID
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT a.*,
|
||||
j.title as job_title,
|
||||
j.description as job_description,
|
||||
j.employer_id,
|
||||
e.company_name,
|
||||
c.user_id as candidate_user_id,
|
||||
u.first_name,
|
||||
u.last_name,
|
||||
u.email as candidate_email
|
||||
FROM applications a
|
||||
JOIN jobs j ON a.job_id = j.id
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
JOIN candidates c ON a.candidate_id = c.id
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE a.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Application not found' });
|
||||
}
|
||||
|
||||
const application = result.rows[0];
|
||||
|
||||
// Check permissions
|
||||
if (req.user.role === 'candidate') {
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (candidateResult.rows.length === 0 || application.candidate_id !== candidateResult.rows[0].id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
} else 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 || application.employer_id !== employerResult.rows[0].id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
}
|
||||
|
||||
res.json(application);
|
||||
} catch (error) {
|
||||
console.error('Get application error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch application' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create application
|
||||
router.post('/', authenticateToken, requireRole(['candidate']), [
|
||||
body('jobId').isUUID(),
|
||||
body('coverLetter').optional().trim(),
|
||||
body('notes').optional().trim()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { jobId, coverLetter, notes } = req.body;
|
||||
|
||||
// Get candidate ID for the current user
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
|
||||
if (candidateResult.rows.length === 0) {
|
||||
return res.status(400).json({ error: 'Candidate profile not found' });
|
||||
}
|
||||
|
||||
const candidateId = candidateResult.rows[0].id;
|
||||
|
||||
// Check if job exists and is active
|
||||
const jobResult = await pool.query(
|
||||
'SELECT id, status FROM jobs WHERE id = $1',
|
||||
[jobId]
|
||||
);
|
||||
|
||||
if (jobResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
if (jobResult.rows[0].status !== 'active') {
|
||||
return res.status(400).json({ error: 'Job is not accepting applications' });
|
||||
}
|
||||
|
||||
// Check if application already exists
|
||||
const existingApplication = await pool.query(
|
||||
'SELECT id FROM applications WHERE job_id = $1 AND candidate_id = $2',
|
||||
[jobId, candidateId]
|
||||
);
|
||||
|
||||
if (existingApplication.rows.length > 0) {
|
||||
return res.status(400).json({ error: 'Application already exists' });
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO applications (job_id, candidate_id, cover_letter, notes)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *
|
||||
`, [jobId, candidateId, coverLetter, notes]);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Application submitted successfully',
|
||||
application: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create application error:', error);
|
||||
res.status(500).json({ error: 'Failed to submit application' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update application status
|
||||
router.put('/:id/status', authenticateToken, [
|
||||
body('status').isIn(['applied', 'reviewed', 'shortlisted', 'interviewed', 'offered', 'rejected', 'withdrawn'])
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
// Check if application exists and user has permission
|
||||
const applicationResult = await pool.query(`
|
||||
SELECT a.*, j.employer_id, e.user_id as employer_user_id
|
||||
FROM applications a
|
||||
JOIN jobs j ON a.job_id = j.id
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
WHERE a.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (applicationResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Application not found' });
|
||||
}
|
||||
|
||||
const application = applicationResult.rows[0];
|
||||
|
||||
// Check permissions
|
||||
if (req.user.role === 'candidate') {
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (candidateResult.rows.length === 0 || application.candidate_id !== candidateResult.rows[0].id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
// Candidates can only withdraw their applications
|
||||
if (status !== 'withdrawn') {
|
||||
return res.status(403).json({ error: 'Candidates can only withdraw applications' });
|
||||
}
|
||||
} else if (req.user.role === 'employer') {
|
||||
if (application.employer_user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
'UPDATE applications SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
|
||||
[status, id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Application status updated successfully',
|
||||
application: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update application status error:', error);
|
||||
res.status(500).json({ error: 'Failed to update application status' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update application notes
|
||||
router.put('/:id/notes', authenticateToken, [
|
||||
body('notes').notEmpty().trim()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { notes } = req.body;
|
||||
|
||||
// Check if application exists and user has permission
|
||||
const applicationResult = await pool.query(`
|
||||
SELECT a.*, j.employer_id, e.user_id as employer_user_id
|
||||
FROM applications a
|
||||
JOIN jobs j ON a.job_id = j.id
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
WHERE a.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (applicationResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Application not found' });
|
||||
}
|
||||
|
||||
const application = applicationResult.rows[0];
|
||||
|
||||
// Check permissions (employers, recruiters, and admins can add notes)
|
||||
if (req.user.role === 'candidate') {
|
||||
return res.status(403).json({ error: 'Candidates cannot add notes to applications' });
|
||||
} else if (req.user.role === 'employer') {
|
||||
if (application.employer_user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
'UPDATE applications SET notes = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
|
||||
[notes, id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Application notes updated successfully',
|
||||
application: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update application notes error:', error);
|
||||
res.status(500).json({ error: 'Failed to update application notes' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
153
backend/src/routes/auth.js
Normal file
153
backend/src/routes/auth.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const pool = require('../database/connection');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Register
|
||||
router.post('/register', [
|
||||
body('email').isEmail().normalizeEmail(),
|
||||
body('password').isLength({ min: 6 }),
|
||||
body('firstName').notEmpty().trim(),
|
||||
body('lastName').notEmpty().trim(),
|
||||
body('role').isIn(['recruiter', 'employer', 'candidate'])
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { email, password, firstName, lastName, role } = req.body;
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await pool.query(
|
||||
'SELECT id FROM users WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (existingUser.rows.length > 0) {
|
||||
return res.status(400).json({ error: 'User already exists' });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create user
|
||||
const userResult = await pool.query(
|
||||
'INSERT INTO users (email, password_hash, first_name, last_name, role) VALUES ($1, $2, $3, $4, $5) RETURNING id, email, first_name, last_name, role',
|
||||
[email, passwordHash, firstName, lastName, role]
|
||||
);
|
||||
|
||||
const user = userResult.rows[0];
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'User created successfully',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
role: user.role
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
res.status(500).json({ error: 'Registration failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Login
|
||||
router.post('/login', [
|
||||
body('email').isEmail().normalizeEmail(),
|
||||
body('password').notEmpty()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Get user
|
||||
const userResult = await pool.query(
|
||||
'SELECT id, email, password_hash, first_name, last_name, role, is_active FROM users WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const user = userResult.rows[0];
|
||||
|
||||
if (!user.is_active) {
|
||||
return res.status(401).json({ error: 'Account deactivated' });
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Login successful',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
role: user.role
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get current user
|
||||
router.get('/me', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
user: {
|
||||
id: req.user.id,
|
||||
email: req.user.email,
|
||||
firstName: req.user.first_name,
|
||||
lastName: req.user.last_name,
|
||||
role: req.user.role
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get user error:', error);
|
||||
res.status(500).json({ error: 'Failed to get user information' });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout (client-side token removal)
|
||||
router.post('/logout', authenticateToken, (req, res) => {
|
||||
res.json({ message: 'Logout successful' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
374
backend/src/routes/candidates.js
Normal file
374
backend/src/routes/candidates.js
Normal file
@@ -0,0 +1,374 @@
|
||||
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 candidates
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { skills, experienceLevel, location, page = 1, limit = 10 } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT c.*, u.email, u.first_name, u.last_name
|
||||
FROM candidates c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
`;
|
||||
const queryParams = [];
|
||||
let paramCount = 0;
|
||||
const conditions = [];
|
||||
|
||||
if (skills) {
|
||||
const skillArray = skills.split(',').map(s => s.trim());
|
||||
paramCount++;
|
||||
conditions.push(`c.skills && $${paramCount}`);
|
||||
queryParams.push(skillArray);
|
||||
}
|
||||
|
||||
if (experienceLevel) {
|
||||
paramCount++;
|
||||
conditions.push(`c.experience_level = $${paramCount}`);
|
||||
queryParams.push(experienceLevel);
|
||||
}
|
||||
|
||||
if (location) {
|
||||
paramCount++;
|
||||
conditions.push(`c.location ILIKE $${paramCount}`);
|
||||
queryParams.push(`%${location}%`);
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query += ` WHERE ${conditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
query += ` ORDER BY c.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 candidates c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
`;
|
||||
const countParams = [];
|
||||
let countParamCount = 0;
|
||||
const countConditions = [];
|
||||
|
||||
if (skills) {
|
||||
const skillArray = skills.split(',').map(s => s.trim());
|
||||
countParamCount++;
|
||||
countConditions.push(`c.skills && $${countParamCount}`);
|
||||
countParams.push(skillArray);
|
||||
}
|
||||
|
||||
if (experienceLevel) {
|
||||
countParamCount++;
|
||||
countConditions.push(`c.experience_level = $${countParamCount}`);
|
||||
countParams.push(experienceLevel);
|
||||
}
|
||||
|
||||
if (location) {
|
||||
countParamCount++;
|
||||
countConditions.push(`c.location ILIKE $${countParamCount}`);
|
||||
countParams.push(`%${location}%`);
|
||||
}
|
||||
|
||||
if (countConditions.length > 0) {
|
||||
countQuery += ` WHERE ${countConditions.join(' AND ')}`;
|
||||
}
|
||||
|
||||
const countResult = await pool.query(countQuery, countParams);
|
||||
|
||||
res.json({
|
||||
candidates: 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 candidates error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch candidates' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get candidate by ID
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT c.*, u.email, u.first_name, u.last_name
|
||||
FROM candidates c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE c.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Candidate not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Get candidate error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch candidate' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create candidate profile
|
||||
router.post('/', authenticateToken, requireRole(['candidate']), [
|
||||
body('phone').optional().trim(),
|
||||
body('location').optional().trim(),
|
||||
body('linkedinUrl').optional().isURL(),
|
||||
body('githubUrl').optional().isURL(),
|
||||
body('portfolioUrl').optional().isURL(),
|
||||
body('bio').optional().trim(),
|
||||
body('skills').optional().isArray(),
|
||||
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
|
||||
body('availability').optional().trim(),
|
||||
body('salaryExpectation').optional().isInt({ min: 0 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const {
|
||||
phone,
|
||||
location,
|
||||
linkedinUrl,
|
||||
githubUrl,
|
||||
portfolioUrl,
|
||||
bio,
|
||||
skills,
|
||||
experienceLevel,
|
||||
availability,
|
||||
salaryExpectation
|
||||
} = req.body;
|
||||
|
||||
// Check if candidate profile already exists for this user
|
||||
const existingCandidate = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
|
||||
if (existingCandidate.rows.length > 0) {
|
||||
return res.status(400).json({ error: 'Candidate profile already exists' });
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO candidates (user_id, phone, location, linkedin_url, github_url, portfolio_url, bio, skills, experience_level, availability, salary_expectation)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING *
|
||||
`, [req.user.id, phone, location, linkedinUrl, githubUrl, portfolioUrl, bio, skills, experienceLevel, availability, salaryExpectation]);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Candidate profile created successfully',
|
||||
candidate: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create candidate error:', error);
|
||||
res.status(500).json({ error: 'Failed to create candidate profile' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update candidate profile
|
||||
router.put('/:id', authenticateToken, [
|
||||
body('phone').optional().trim(),
|
||||
body('location').optional().trim(),
|
||||
body('linkedinUrl').optional().isURL(),
|
||||
body('githubUrl').optional().isURL(),
|
||||
body('portfolioUrl').optional().isURL(),
|
||||
body('bio').optional().trim(),
|
||||
body('skills').optional().isArray(),
|
||||
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
|
||||
body('availability').optional().trim(),
|
||||
body('salaryExpectation').optional().isInt({ min: 0 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const {
|
||||
phone,
|
||||
location,
|
||||
linkedinUrl,
|
||||
githubUrl,
|
||||
portfolioUrl,
|
||||
bio,
|
||||
skills,
|
||||
experienceLevel,
|
||||
availability,
|
||||
salaryExpectation
|
||||
} = req.body;
|
||||
|
||||
// Check if candidate exists and user has permission
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT user_id FROM candidates WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (candidateResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Candidate not found' });
|
||||
}
|
||||
|
||||
// Users can only update their own candidate profile unless they're admin
|
||||
if (candidateResult.rows[0].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 (phone !== undefined) {
|
||||
updateFields.push(`phone = $${paramCount}`);
|
||||
updateValues.push(phone);
|
||||
paramCount++;
|
||||
}
|
||||
if (location !== undefined) {
|
||||
updateFields.push(`location = $${paramCount}`);
|
||||
updateValues.push(location);
|
||||
paramCount++;
|
||||
}
|
||||
if (linkedinUrl !== undefined) {
|
||||
updateFields.push(`linkedin_url = $${paramCount}`);
|
||||
updateValues.push(linkedinUrl);
|
||||
paramCount++;
|
||||
}
|
||||
if (githubUrl !== undefined) {
|
||||
updateFields.push(`github_url = $${paramCount}`);
|
||||
updateValues.push(githubUrl);
|
||||
paramCount++;
|
||||
}
|
||||
if (portfolioUrl !== undefined) {
|
||||
updateFields.push(`portfolio_url = $${paramCount}`);
|
||||
updateValues.push(portfolioUrl);
|
||||
paramCount++;
|
||||
}
|
||||
if (bio !== undefined) {
|
||||
updateFields.push(`bio = $${paramCount}`);
|
||||
updateValues.push(bio);
|
||||
paramCount++;
|
||||
}
|
||||
if (skills !== undefined) {
|
||||
updateFields.push(`skills = $${paramCount}`);
|
||||
updateValues.push(skills);
|
||||
paramCount++;
|
||||
}
|
||||
if (experienceLevel !== undefined) {
|
||||
updateFields.push(`experience_level = $${paramCount}`);
|
||||
updateValues.push(experienceLevel);
|
||||
paramCount++;
|
||||
}
|
||||
if (availability !== undefined) {
|
||||
updateFields.push(`availability = $${paramCount}`);
|
||||
updateValues.push(availability);
|
||||
paramCount++;
|
||||
}
|
||||
if (salaryExpectation !== undefined) {
|
||||
updateFields.push(`salary_expectation = $${paramCount}`);
|
||||
updateValues.push(salaryExpectation);
|
||||
paramCount++;
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({ error: 'No fields to update' });
|
||||
}
|
||||
|
||||
updateValues.push(id);
|
||||
const query = `UPDATE candidates SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING *`;
|
||||
|
||||
const result = await pool.query(query, updateValues);
|
||||
|
||||
res.json({
|
||||
message: 'Candidate profile updated successfully',
|
||||
candidate: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update candidate error:', error);
|
||||
res.status(500).json({ error: 'Failed to update candidate profile' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get candidate's applications
|
||||
router.get('/:id/applications', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, page = 1, limit = 10 } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT a.*, j.title as job_title, j.employer_id, e.company_name
|
||||
FROM applications a
|
||||
JOIN jobs j ON a.job_id = j.id
|
||||
JOIN employers e ON j.employer_id = e.id
|
||||
WHERE a.candidate_id = $1
|
||||
`;
|
||||
const queryParams = [id];
|
||||
let paramCount = 1;
|
||||
|
||||
if (status) {
|
||||
paramCount++;
|
||||
query += ` AND a.status = $${paramCount}`;
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
query += ` ORDER BY a.applied_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 applications a
|
||||
WHERE a.candidate_id = $1
|
||||
`;
|
||||
const countParams = [id];
|
||||
if (status) {
|
||||
countQuery += ' AND a.status = $2';
|
||||
countParams.push(status);
|
||||
}
|
||||
const countResult = await pool.query(countQuery, countParams);
|
||||
|
||||
res.json({
|
||||
applications: 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 candidate applications error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch candidate applications' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
256
backend/src/routes/employers.js
Normal file
256
backend/src/routes/employers.js
Normal file
@@ -0,0 +1,256 @@
|
||||
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 employers
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT e.*, u.email, u.first_name, u.last_name
|
||||
FROM employers e
|
||||
JOIN users u ON e.user_id = u.id
|
||||
ORDER BY e.created_at DESC
|
||||
`);
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Get employers error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch employers' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get employer by ID
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT e.*, u.email, u.first_name, u.last_name
|
||||
FROM employers e
|
||||
JOIN users u ON e.user_id = u.id
|
||||
WHERE e.id = $1
|
||||
`, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Employer not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Get employer error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch employer' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create employer profile
|
||||
router.post('/', authenticateToken, requireRole(['employer']), [
|
||||
body('companyName').notEmpty().trim(),
|
||||
body('industry').optional().trim(),
|
||||
body('companySize').optional().trim(),
|
||||
body('website').optional().isURL(),
|
||||
body('description').optional().trim(),
|
||||
body('address').optional().trim(),
|
||||
body('phone').optional().trim()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const {
|
||||
companyName,
|
||||
industry,
|
||||
companySize,
|
||||
website,
|
||||
description,
|
||||
address,
|
||||
phone
|
||||
} = req.body;
|
||||
|
||||
// Check if employer profile already exists for this user
|
||||
const existingEmployer = await pool.query(
|
||||
'SELECT id FROM employers WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
|
||||
if (existingEmployer.rows.length > 0) {
|
||||
return res.status(400).json({ error: 'Employer profile already exists' });
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO employers (user_id, company_name, industry, company_size, website, description, address, phone)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *
|
||||
`, [req.user.id, companyName, industry, companySize, website, description, address, phone]);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Employer profile created successfully',
|
||||
employer: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create employer error:', error);
|
||||
res.status(500).json({ error: 'Failed to create employer profile' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update employer profile
|
||||
router.put('/:id', authenticateToken, [
|
||||
body('companyName').optional().notEmpty().trim(),
|
||||
body('industry').optional().trim(),
|
||||
body('companySize').optional().trim(),
|
||||
body('website').optional().isURL(),
|
||||
body('description').optional().trim(),
|
||||
body('address').optional().trim(),
|
||||
body('phone').optional().trim()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const {
|
||||
companyName,
|
||||
industry,
|
||||
companySize,
|
||||
website,
|
||||
description,
|
||||
address,
|
||||
phone
|
||||
} = req.body;
|
||||
|
||||
// Check if employer exists and user has permission
|
||||
const employerResult = await pool.query(
|
||||
'SELECT user_id FROM employers WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (employerResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Employer not found' });
|
||||
}
|
||||
|
||||
// Users can only update their own employer profile unless they're admin
|
||||
if (employerResult.rows[0].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 (companyName) {
|
||||
updateFields.push(`company_name = $${paramCount}`);
|
||||
updateValues.push(companyName);
|
||||
paramCount++;
|
||||
}
|
||||
if (industry !== undefined) {
|
||||
updateFields.push(`industry = $${paramCount}`);
|
||||
updateValues.push(industry);
|
||||
paramCount++;
|
||||
}
|
||||
if (companySize !== undefined) {
|
||||
updateFields.push(`company_size = $${paramCount}`);
|
||||
updateValues.push(companySize);
|
||||
paramCount++;
|
||||
}
|
||||
if (website !== undefined) {
|
||||
updateFields.push(`website = $${paramCount}`);
|
||||
updateValues.push(website);
|
||||
paramCount++;
|
||||
}
|
||||
if (description !== undefined) {
|
||||
updateFields.push(`description = $${paramCount}`);
|
||||
updateValues.push(description);
|
||||
paramCount++;
|
||||
}
|
||||
if (address !== undefined) {
|
||||
updateFields.push(`address = $${paramCount}`);
|
||||
updateValues.push(address);
|
||||
paramCount++;
|
||||
}
|
||||
if (phone !== undefined) {
|
||||
updateFields.push(`phone = $${paramCount}`);
|
||||
updateValues.push(phone);
|
||||
paramCount++;
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({ error: 'No fields to update' });
|
||||
}
|
||||
|
||||
updateValues.push(id);
|
||||
const query = `UPDATE employers SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING *`;
|
||||
|
||||
const result = await pool.query(query, updateValues);
|
||||
|
||||
res.json({
|
||||
message: 'Employer profile updated successfully',
|
||||
employer: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update employer error:', error);
|
||||
res.status(500).json({ error: 'Failed to update employer profile' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get employer's jobs
|
||||
router.get('/:id/jobs', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, page = 1, limit = 10 } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT * FROM jobs
|
||||
WHERE employer_id = $1
|
||||
`;
|
||||
const queryParams = [id];
|
||||
let paramCount = 1;
|
||||
|
||||
if (status) {
|
||||
paramCount++;
|
||||
query += ` AND status = $${paramCount}`;
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
query += ` ORDER BY 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 WHERE employer_id = $1';
|
||||
const countParams = [id];
|
||||
if (status) {
|
||||
countQuery += ' AND status = $2';
|
||||
countParams.push(status);
|
||||
}
|
||||
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 employer jobs error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch employer jobs' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
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;
|
||||
298
backend/src/routes/resumes.js
Normal file
298
backend/src/routes/resumes.js
Normal file
@@ -0,0 +1,298 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const pool = require('../database/connection');
|
||||
const { authenticateToken, requireRole } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Configure multer for file uploads
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const uploadDir = path.join(__dirname, '../../uploads/resumes');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueName = `${uuidv4()}-${file.originalname}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain'
|
||||
];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only PDF, DOC, DOCX, and TXT files are allowed.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get all resumes for a candidate
|
||||
router.get('/candidate/:candidateId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { candidateId } = req.params;
|
||||
|
||||
// Check permissions
|
||||
if (req.user.role === 'candidate') {
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (candidateResult.rows.length === 0 || candidateResult.rows[0].id !== candidateId) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter' && req.user.role !== 'employer') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM resumes WHERE candidate_id = $1 ORDER BY uploaded_at DESC',
|
||||
[candidateId]
|
||||
);
|
||||
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Get resumes error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch resumes' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get resume by ID
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM resumes WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Resume not found' });
|
||||
}
|
||||
|
||||
const resume = result.rows[0];
|
||||
|
||||
// Check permissions
|
||||
if (req.user.role === 'candidate') {
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (candidateResult.rows.length === 0 || candidateResult.rows[0].id !== resume.candidate_id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter' && req.user.role !== 'employer') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
res.json(resume);
|
||||
} catch (error) {
|
||||
console.error('Get resume error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch resume' });
|
||||
}
|
||||
});
|
||||
|
||||
// Upload resume
|
||||
router.post('/upload', authenticateToken, requireRole(['candidate']), upload.single('resume'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
// Get candidate ID for the current user
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
|
||||
if (candidateResult.rows.length === 0) {
|
||||
return res.status(400).json({ error: 'Candidate profile not found' });
|
||||
}
|
||||
|
||||
const candidateId = candidateResult.rows[0].id;
|
||||
|
||||
// If this is set as primary, unset other primary resumes
|
||||
if (req.body.isPrimary === 'true') {
|
||||
await pool.query(
|
||||
'UPDATE resumes SET is_primary = false WHERE candidate_id = $1',
|
||||
[candidateId]
|
||||
);
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO resumes (candidate_id, filename, original_name, file_path, file_size, mime_type, is_primary)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *
|
||||
`, [
|
||||
candidateId,
|
||||
req.file.filename,
|
||||
req.file.originalname,
|
||||
req.file.path,
|
||||
req.file.size,
|
||||
req.file.mimetype,
|
||||
req.body.isPrimary === 'true'
|
||||
]);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Resume uploaded successfully',
|
||||
resume: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload resume error:', error);
|
||||
res.status(500).json({ error: 'Failed to upload resume' });
|
||||
}
|
||||
});
|
||||
|
||||
// Download resume
|
||||
router.get('/:id/download', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM resumes WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Resume not found' });
|
||||
}
|
||||
|
||||
const resume = result.rows[0];
|
||||
|
||||
// Check permissions
|
||||
if (req.user.role === 'candidate') {
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
if (candidateResult.rows.length === 0 || candidateResult.rows[0].id !== resume.candidate_id) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter' && req.user.role !== 'employer') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(resume.file_path)) {
|
||||
return res.status(404).json({ error: 'Resume file not found' });
|
||||
}
|
||||
|
||||
res.download(resume.file_path, resume.original_name);
|
||||
} catch (error) {
|
||||
console.error('Download resume error:', error);
|
||||
res.status(500).json({ error: 'Failed to download resume' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set primary resume
|
||||
router.put('/:id/primary', authenticateToken, requireRole(['candidate']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get candidate ID for the current user
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
|
||||
if (candidateResult.rows.length === 0) {
|
||||
return res.status(400).json({ error: 'Candidate profile not found' });
|
||||
}
|
||||
|
||||
const candidateId = candidateResult.rows[0].id;
|
||||
|
||||
// Check if resume exists and belongs to the candidate
|
||||
const resumeResult = await pool.query(
|
||||
'SELECT id FROM resumes WHERE id = $1 AND candidate_id = $2',
|
||||
[id, candidateId]
|
||||
);
|
||||
|
||||
if (resumeResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Resume not found' });
|
||||
}
|
||||
|
||||
// Unset other primary resumes
|
||||
await pool.query(
|
||||
'UPDATE resumes SET is_primary = false WHERE candidate_id = $1',
|
||||
[candidateId]
|
||||
);
|
||||
|
||||
// Set this resume as primary
|
||||
const result = await pool.query(
|
||||
'UPDATE resumes SET is_primary = true WHERE id = $1 RETURNING *',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Primary resume updated successfully',
|
||||
resume: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Set primary resume error:', error);
|
||||
res.status(500).json({ error: 'Failed to set primary resume' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete resume
|
||||
router.delete('/:id', authenticateToken, requireRole(['candidate']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get candidate ID for the current user
|
||||
const candidateResult = await pool.query(
|
||||
'SELECT id FROM candidates WHERE user_id = $1',
|
||||
[req.user.id]
|
||||
);
|
||||
|
||||
if (candidateResult.rows.length === 0) {
|
||||
return res.status(400).json({ error: 'Candidate profile not found' });
|
||||
}
|
||||
|
||||
const candidateId = candidateResult.rows[0].id;
|
||||
|
||||
// Check if resume exists and belongs to the candidate
|
||||
const resumeResult = await pool.query(
|
||||
'SELECT * FROM resumes WHERE id = $1 AND candidate_id = $2',
|
||||
[id, candidateId]
|
||||
);
|
||||
|
||||
if (resumeResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Resume not found' });
|
||||
}
|
||||
|
||||
const resume = resumeResult.rows[0];
|
||||
|
||||
// Delete file from filesystem
|
||||
if (fs.existsSync(resume.file_path)) {
|
||||
fs.unlinkSync(resume.file_path);
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await pool.query('DELETE FROM resumes WHERE id = $1', [id]);
|
||||
|
||||
res.json({ message: 'Resume deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete resume error:', error);
|
||||
res.status(500).json({ error: 'Failed to delete resume' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
165
backend/src/routes/users.js
Normal file
165
backend/src/routes/users.js
Normal file
@@ -0,0 +1,165 @@
|
||||
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 users (admin only)
|
||||
router.get('/', authenticateToken, requireRole(['admin']), async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT id, email, first_name, last_name, role, is_active, created_at FROM users ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch users' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get user by ID
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Users can only view their own profile unless they're admin
|
||||
if (req.user.id !== id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT id, email, first_name, last_name, role, is_active, created_at FROM users WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Get user error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch user' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update user profile
|
||||
router.put('/:id', authenticateToken, [
|
||||
body('firstName').optional().notEmpty().trim(),
|
||||
body('lastName').optional().notEmpty().trim(),
|
||||
body('email').optional().isEmail().normalizeEmail()
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { firstName, lastName, email } = req.body;
|
||||
|
||||
// Users can only update their own profile unless they're admin
|
||||
if (req.user.id !== id && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
// Check if email is already taken by another user
|
||||
if (email) {
|
||||
const existingUser = await pool.query(
|
||||
'SELECT id FROM users WHERE email = $1 AND id != $2',
|
||||
[email, id]
|
||||
);
|
||||
if (existingUser.rows.length > 0) {
|
||||
return res.status(400).json({ error: 'Email already in use' });
|
||||
}
|
||||
}
|
||||
|
||||
const updateFields = [];
|
||||
const updateValues = [];
|
||||
let paramCount = 1;
|
||||
|
||||
if (firstName) {
|
||||
updateFields.push(`first_name = $${paramCount}`);
|
||||
updateValues.push(firstName);
|
||||
paramCount++;
|
||||
}
|
||||
if (lastName) {
|
||||
updateFields.push(`last_name = $${paramCount}`);
|
||||
updateValues.push(lastName);
|
||||
paramCount++;
|
||||
}
|
||||
if (email) {
|
||||
updateFields.push(`email = $${paramCount}`);
|
||||
updateValues.push(email);
|
||||
paramCount++;
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({ error: 'No fields to update' });
|
||||
}
|
||||
|
||||
updateValues.push(id);
|
||||
const query = `UPDATE users SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING id, email, first_name, last_name, role, is_active, updated_at`;
|
||||
|
||||
const result = await pool.query(query, updateValues);
|
||||
|
||||
res.json({
|
||||
message: 'User updated successfully',
|
||||
user: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update user error:', error);
|
||||
res.status(500).json({ error: 'Failed to update user' });
|
||||
}
|
||||
});
|
||||
|
||||
// Deactivate user (admin only)
|
||||
router.put('/:id/deactivate', authenticateToken, requireRole(['admin']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(
|
||||
'UPDATE users SET is_active = false, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING id, email, first_name, last_name, role, is_active',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: 'User deactivated successfully',
|
||||
user: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Deactivate user error:', error);
|
||||
res.status(500).json({ error: 'Failed to deactivate user' });
|
||||
}
|
||||
});
|
||||
|
||||
// Activate user (admin only)
|
||||
router.put('/:id/activate', authenticateToken, requireRole(['admin']), async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(
|
||||
'UPDATE users SET is_active = true, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING id, email, first_name, last_name, role, is_active',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: 'User activated successfully',
|
||||
user: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Activate user error:', error);
|
||||
res.status(500).json({ error: 'Failed to activate user' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user