Initial commit

This commit is contained in:
2025-10-16 17:04:52 -05:00
commit 039d51c4e5
45 changed files with 6939 additions and 0 deletions

View 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
View 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;

View 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;

View 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
View 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;

View 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
View 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;