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