Initial commit
This commit is contained in:
		
							
								
								
									
										18
									
								
								backend/src/database/connection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend/src/database/connection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| const { Pool } = require('pg'); | ||||
| require('dotenv').config(); | ||||
|  | ||||
| const pool = new Pool({ | ||||
|   connectionString: process.env.DATABASE_URL, | ||||
|   ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false | ||||
| }); | ||||
|  | ||||
| // Test database connection | ||||
| pool.on('connect', () => { | ||||
|   console.log('Connected to MysteryApp-Cursor database'); | ||||
| }); | ||||
|  | ||||
| pool.on('error', (err) => { | ||||
|   console.error('Database connection error:', err); | ||||
| }); | ||||
|  | ||||
| module.exports = pool; | ||||
							
								
								
									
										29
									
								
								backend/src/database/migrate.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								backend/src/database/migrate.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
| const pool = require('./connection'); | ||||
|  | ||||
| async function migrate() { | ||||
|   try { | ||||
|     console.log('Starting database migration...'); | ||||
|      | ||||
|     // Read schema file | ||||
|     const schemaPath = path.join(__dirname, 'schema.sql'); | ||||
|     const schema = fs.readFileSync(schemaPath, 'utf8'); | ||||
|      | ||||
|     // Execute schema | ||||
|     await pool.query(schema); | ||||
|      | ||||
|     console.log('Database migration completed successfully!'); | ||||
|   } catch (error) { | ||||
|     console.error('Migration failed:', error); | ||||
|     process.exit(1); | ||||
|   } finally { | ||||
|     await pool.end(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| if (require.main === module) { | ||||
|   migrate(); | ||||
| } | ||||
|  | ||||
| module.exports = migrate; | ||||
							
								
								
									
										141
									
								
								backend/src/database/schema.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								backend/src/database/schema.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| -- MysteryApp-Cursor Database Schema | ||||
|  | ||||
| -- Users table (for authentication and user management) | ||||
| CREATE TABLE IF NOT EXISTS users ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     email VARCHAR(255) UNIQUE NOT NULL, | ||||
|     password_hash VARCHAR(255) NOT NULL, | ||||
|     first_name VARCHAR(100) NOT NULL, | ||||
|     last_name VARCHAR(100) NOT NULL, | ||||
|     role VARCHAR(50) NOT NULL CHECK (role IN ('admin', 'recruiter', 'employer', 'candidate')), | ||||
|     is_active BOOLEAN DEFAULT true, | ||||
|     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||||
| ); | ||||
|  | ||||
| -- Employers table | ||||
| CREATE TABLE IF NOT EXISTS employers ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     user_id UUID REFERENCES users(id) ON DELETE CASCADE, | ||||
|     company_name VARCHAR(255) NOT NULL, | ||||
|     industry VARCHAR(100), | ||||
|     company_size VARCHAR(50), | ||||
|     website VARCHAR(255), | ||||
|     description TEXT, | ||||
|     address TEXT, | ||||
|     phone VARCHAR(20), | ||||
|     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||||
| ); | ||||
|  | ||||
| -- Candidates table | ||||
| CREATE TABLE IF NOT EXISTS candidates ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     user_id UUID REFERENCES users(id) ON DELETE CASCADE, | ||||
|     phone VARCHAR(20), | ||||
|     location VARCHAR(255), | ||||
|     linkedin_url VARCHAR(255), | ||||
|     github_url VARCHAR(255), | ||||
|     portfolio_url VARCHAR(255), | ||||
|     bio TEXT, | ||||
|     skills TEXT[], | ||||
|     experience_level VARCHAR(50), | ||||
|     availability VARCHAR(50), | ||||
|     salary_expectation INTEGER, | ||||
|     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||||
| ); | ||||
|  | ||||
| -- Job postings table | ||||
| CREATE TABLE IF NOT EXISTS jobs ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     employer_id UUID REFERENCES employers(id) ON DELETE CASCADE, | ||||
|     title VARCHAR(255) NOT NULL, | ||||
|     description TEXT NOT NULL, | ||||
|     requirements TEXT[], | ||||
|     responsibilities TEXT[], | ||||
|     location VARCHAR(255), | ||||
|     employment_type VARCHAR(50) CHECK (employment_type IN ('full-time', 'part-time', 'contract', 'internship')), | ||||
|     salary_min INTEGER, | ||||
|     salary_max INTEGER, | ||||
|     currency VARCHAR(3) DEFAULT 'USD', | ||||
|     status VARCHAR(50) DEFAULT 'active' CHECK (status IN ('active', 'paused', 'closed', 'draft')), | ||||
|     remote_allowed BOOLEAN DEFAULT false, | ||||
|     experience_level VARCHAR(50), | ||||
|     skills_required TEXT[], | ||||
|     benefits TEXT[], | ||||
|     application_deadline DATE, | ||||
|     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||||
| ); | ||||
|  | ||||
| -- Applications table | ||||
| CREATE TABLE IF NOT EXISTS applications ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     job_id UUID REFERENCES jobs(id) ON DELETE CASCADE, | ||||
|     candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE, | ||||
|     status VARCHAR(50) DEFAULT 'applied' CHECK (status IN ('applied', 'reviewed', 'shortlisted', 'interviewed', 'offered', 'rejected', 'withdrawn')), | ||||
|     cover_letter TEXT, | ||||
|     notes TEXT, | ||||
|     applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     UNIQUE(job_id, candidate_id) | ||||
| ); | ||||
|  | ||||
| -- Resumes table | ||||
| CREATE TABLE IF NOT EXISTS resumes ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE, | ||||
|     filename VARCHAR(255) NOT NULL, | ||||
|     original_name VARCHAR(255) NOT NULL, | ||||
|     file_path VARCHAR(500) NOT NULL, | ||||
|     file_size INTEGER NOT NULL, | ||||
|     mime_type VARCHAR(100) NOT NULL, | ||||
|     is_primary BOOLEAN DEFAULT false, | ||||
|     uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||||
| ); | ||||
|  | ||||
| -- Interviews table | ||||
| CREATE TABLE IF NOT EXISTS interviews ( | ||||
|     id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||||
|     application_id UUID REFERENCES applications(id) ON DELETE CASCADE, | ||||
|     scheduled_at TIMESTAMP NOT NULL, | ||||
|     duration_minutes INTEGER DEFAULT 60, | ||||
|     interview_type VARCHAR(50) CHECK (interview_type IN ('phone', 'video', 'in-person', 'technical')), | ||||
|     location VARCHAR(255), | ||||
|     meeting_link VARCHAR(500), | ||||
|     notes TEXT, | ||||
|     status VARCHAR(50) DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'completed', 'cancelled', 'rescheduled')), | ||||
|     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||||
| ); | ||||
|  | ||||
| -- Create indexes for better performance | ||||
| CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); | ||||
| CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); | ||||
| CREATE INDEX IF NOT EXISTS idx_employers_user_id ON employers(user_id); | ||||
| CREATE INDEX IF NOT EXISTS idx_candidates_user_id ON candidates(user_id); | ||||
| CREATE INDEX IF NOT EXISTS idx_jobs_employer_id ON jobs(employer_id); | ||||
| CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status); | ||||
| CREATE INDEX IF NOT EXISTS idx_applications_job_id ON applications(job_id); | ||||
| CREATE INDEX IF NOT EXISTS idx_applications_candidate_id ON applications(candidate_id); | ||||
| CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status); | ||||
| CREATE INDEX IF NOT EXISTS idx_resumes_candidate_id ON resumes(candidate_id); | ||||
| CREATE INDEX IF NOT EXISTS idx_interviews_application_id ON interviews(application_id); | ||||
|  | ||||
| -- Create updated_at trigger function | ||||
| CREATE OR REPLACE FUNCTION update_updated_at_column() | ||||
| RETURNS TRIGGER AS $$ | ||||
| BEGIN | ||||
|     NEW.updated_at = CURRENT_TIMESTAMP; | ||||
|     RETURN NEW; | ||||
| END; | ||||
| $$ language 'plpgsql'; | ||||
|  | ||||
| -- Apply updated_at triggers | ||||
| CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); | ||||
| CREATE TRIGGER update_employers_updated_at BEFORE UPDATE ON employers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); | ||||
| CREATE TRIGGER update_candidates_updated_at BEFORE UPDATE ON candidates FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); | ||||
| CREATE TRIGGER update_jobs_updated_at BEFORE UPDATE ON jobs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); | ||||
| CREATE TRIGGER update_applications_updated_at BEFORE UPDATE ON applications FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); | ||||
| CREATE TRIGGER update_interviews_updated_at BEFORE UPDATE ON interviews FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); | ||||
							
								
								
									
										128
									
								
								backend/src/database/seed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								backend/src/database/seed.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| const bcrypt = require('bcryptjs'); | ||||
| const pool = require('./connection'); | ||||
|  | ||||
| async function seed() { | ||||
|   try { | ||||
|     console.log('Starting database seeding...'); | ||||
|      | ||||
|     // Hash passwords | ||||
|     const adminPassword = await bcrypt.hash('admin123', 10); | ||||
|     const recruiterPassword = await bcrypt.hash('recruiter123', 10); | ||||
|     const employerPassword = await bcrypt.hash('employer123', 10); | ||||
|     const candidatePassword = await bcrypt.hash('candidate123', 10); | ||||
|      | ||||
|     // Insert users | ||||
|     const users = [ | ||||
|       { | ||||
|         email: 'admin@mysteryapp.com', | ||||
|         password_hash: adminPassword, | ||||
|         first_name: 'Admin', | ||||
|         last_name: 'User', | ||||
|         role: 'admin' | ||||
|       }, | ||||
|       { | ||||
|         email: 'recruiter@mysteryapp.com', | ||||
|         password_hash: recruiterPassword, | ||||
|         first_name: 'John', | ||||
|         last_name: 'Recruiter', | ||||
|         role: 'recruiter' | ||||
|       }, | ||||
|       { | ||||
|         email: 'employer@techcorp.com', | ||||
|         password_hash: employerPassword, | ||||
|         first_name: 'Jane', | ||||
|         last_name: 'Smith', | ||||
|         role: 'employer' | ||||
|       }, | ||||
|       { | ||||
|         email: 'candidate@example.com', | ||||
|         password_hash: candidatePassword, | ||||
|         first_name: 'Mike', | ||||
|         last_name: 'Johnson', | ||||
|         role: 'candidate' | ||||
|       } | ||||
|     ]; | ||||
|      | ||||
|     const userResults = []; | ||||
|     for (const user of users) { | ||||
|       const result = await pool.query( | ||||
|         'INSERT INTO users (email, password_hash, first_name, last_name, role) VALUES ($1, $2, $3, $4, $5) RETURNING id', | ||||
|         [user.email, user.password_hash, user.first_name, user.last_name, user.role] | ||||
|       ); | ||||
|       userResults.push({ ...user, id: result.rows[0].id }); | ||||
|     } | ||||
|      | ||||
|     // Insert employer | ||||
|     const employerResult = 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 id`, | ||||
|       [ | ||||
|         userResults[2].id, // employer user | ||||
|         'TechCorp Solutions', | ||||
|         'Technology', | ||||
|         '50-200', | ||||
|         'https://techcorp.com', | ||||
|         'Leading technology company specializing in innovative software solutions.', | ||||
|         '123 Tech Street, San Francisco, CA 94105', | ||||
|         '+1-555-0123' | ||||
|       ] | ||||
|     ); | ||||
|      | ||||
|     // Insert candidate | ||||
|     await pool.query( | ||||
|       `INSERT INTO candidates (user_id, phone, location, linkedin_url, bio, skills, experience_level, availability, salary_expectation) | ||||
|        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, | ||||
|       [ | ||||
|         userResults[3].id, // candidate user | ||||
|         '+1-555-0456', | ||||
|         'San Francisco, CA', | ||||
|         'https://linkedin.com/in/mikejohnson', | ||||
|         'Experienced software developer with 5+ years in full-stack development.', | ||||
|         ['JavaScript', 'React', 'Node.js', 'Python', 'PostgreSQL'], | ||||
|         'senior', | ||||
|         'immediately', | ||||
|         120000 | ||||
|       ] | ||||
|     ); | ||||
|      | ||||
|     // Insert job posting | ||||
|     const jobResult = await pool.query( | ||||
|       `INSERT INTO jobs (employer_id, title, description, requirements, responsibilities, location, employment_type, salary_min, salary_max, remote_allowed, experience_level, skills_required, benefits) | ||||
|        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id`, | ||||
|       [ | ||||
|         employerResult.rows[0].id, | ||||
|         'Senior Full Stack Developer', | ||||
|         'We are looking for a talented Senior Full Stack Developer to join our growing team.', | ||||
|         ['5+ years of experience', 'Bachelor degree in Computer Science', 'Strong problem-solving skills'], | ||||
|         ['Develop web applications', 'Collaborate with team members', 'Code reviews', 'Mentor junior developers'], | ||||
|         'San Francisco, CA', | ||||
|         'full-time', | ||||
|         100000, | ||||
|         150000, | ||||
|         true, | ||||
|         'senior', | ||||
|         ['JavaScript', 'React', 'Node.js', 'PostgreSQL', 'AWS'], | ||||
|         ['Health insurance', '401k', 'Flexible work hours', 'Remote work'] | ||||
|       ] | ||||
|     ); | ||||
|      | ||||
|     console.log('Database seeding completed successfully!'); | ||||
|     console.log('Sample users created:'); | ||||
|     console.log('- Admin: admin@mysteryapp.com / admin123'); | ||||
|     console.log('- Recruiter: recruiter@mysteryapp.com / recruiter123'); | ||||
|     console.log('- Employer: employer@techcorp.com / employer123'); | ||||
|     console.log('- Candidate: candidate@example.com / candidate123'); | ||||
|      | ||||
|   } catch (error) { | ||||
|     console.error('Seeding failed:', error); | ||||
|     process.exit(1); | ||||
|   } finally { | ||||
|     await pool.end(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| if (require.main === module) { | ||||
|   seed(); | ||||
| } | ||||
|  | ||||
| module.exports = seed; | ||||
							
								
								
									
										54
									
								
								backend/src/middleware/auth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								backend/src/middleware/auth.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| const jwt = require('jsonwebtoken'); | ||||
| const pool = require('../database/connection'); | ||||
|  | ||||
| const authenticateToken = async (req, res, next) => { | ||||
|   const authHeader = req.headers['authorization']; | ||||
|   const token = authHeader && authHeader.split(' ')[1]; | ||||
|  | ||||
|   if (!token) { | ||||
|     return res.status(401).json({ error: 'Access token required' }); | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     const decoded = jwt.verify(token, process.env.JWT_SECRET); | ||||
|      | ||||
|     // Get user details from database | ||||
|     const userResult = await pool.query( | ||||
|       'SELECT id, email, first_name, last_name, role, is_active FROM users WHERE id = $1', | ||||
|       [decoded.userId] | ||||
|     ); | ||||
|  | ||||
|     if (userResult.rows.length === 0) { | ||||
|       return res.status(401).json({ error: 'Invalid token' }); | ||||
|     } | ||||
|  | ||||
|     const user = userResult.rows[0]; | ||||
|     if (!user.is_active) { | ||||
|       return res.status(401).json({ error: 'Account deactivated' }); | ||||
|     } | ||||
|  | ||||
|     req.user = user; | ||||
|     next(); | ||||
|   } catch (error) { | ||||
|     return res.status(403).json({ error: 'Invalid or expired token' }); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const requireRole = (roles) => { | ||||
|   return (req, res, next) => { | ||||
|     if (!req.user) { | ||||
|       return res.status(401).json({ error: 'Authentication required' }); | ||||
|     } | ||||
|  | ||||
|     if (!roles.includes(req.user.role)) { | ||||
|       return res.status(403).json({ error: 'Insufficient permissions' }); | ||||
|     } | ||||
|  | ||||
|     next(); | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| module.exports = { | ||||
|   authenticateToken, | ||||
|   requireRole | ||||
| }; | ||||
							
								
								
									
										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; | ||||
							
								
								
									
										57
									
								
								backend/src/server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								backend/src/server.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| const express = require('express'); | ||||
| const cors = require('cors'); | ||||
| const helmet = require('helmet'); | ||||
| const morgan = require('morgan'); | ||||
| require('dotenv').config(); | ||||
|  | ||||
| const authRoutes = require('./routes/auth'); | ||||
| const userRoutes = require('./routes/users'); | ||||
| const employerRoutes = require('./routes/employers'); | ||||
| const candidateRoutes = require('./routes/candidates'); | ||||
| const jobRoutes = require('./routes/jobs'); | ||||
| const applicationRoutes = require('./routes/applications'); | ||||
| const resumeRoutes = require('./routes/resumes'); | ||||
|  | ||||
| const app = express(); | ||||
| const PORT = process.env.PORT || 3001; | ||||
|  | ||||
| // Middleware | ||||
| app.use(helmet()); | ||||
| app.use(cors()); | ||||
| app.use(morgan('combined')); | ||||
| app.use(express.json({ limit: '10mb' })); | ||||
| app.use(express.urlencoded({ extended: true })); | ||||
|  | ||||
| // Routes | ||||
| app.use('/api/auth', authRoutes); | ||||
| app.use('/api/users', userRoutes); | ||||
| app.use('/api/employers', employerRoutes); | ||||
| app.use('/api/candidates', candidateRoutes); | ||||
| app.use('/api/jobs', jobRoutes); | ||||
| app.use('/api/applications', applicationRoutes); | ||||
| app.use('/api/resumes', resumeRoutes); | ||||
|  | ||||
| // Health check | ||||
| app.get('/api/health', (req, res) => { | ||||
|   res.json({ status: 'OK', timestamp: new Date().toISOString() }); | ||||
| }); | ||||
|  | ||||
| // Error handling middleware | ||||
| app.use((err, req, res, next) => { | ||||
|   console.error(err.stack); | ||||
|   res.status(500).json({  | ||||
|     error: 'Something went wrong!', | ||||
|     message: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error' | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| // 404 handler | ||||
| app.use('*', (req, res) => { | ||||
|   res.status(404).json({ error: 'Route not found' }); | ||||
| }); | ||||
|  | ||||
| app.listen(PORT, () => { | ||||
|   console.log(`MysteryApp-Cursor backend server running on port ${PORT}`); | ||||
| }); | ||||
|  | ||||
| module.exports = app; | ||||
							
								
								
									
										185
									
								
								backend/src/tests/auth.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								backend/src/tests/auth.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| const request = require('supertest'); | ||||
| const app = require('../server'); | ||||
| const pool = require('../database/connection'); | ||||
|  | ||||
| describe('Authentication', () => { | ||||
|   beforeEach(async () => { | ||||
|     // Clean up database before each test | ||||
|     await pool.query('DELETE FROM users WHERE email LIKE $1', ['test%']); | ||||
|   }); | ||||
|  | ||||
|   afterAll(async () => { | ||||
|     await pool.end(); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /api/auth/register', () => { | ||||
|     it('should register a new user successfully', async () => { | ||||
|       const userData = { | ||||
|         firstName: 'Test', | ||||
|         lastName: 'User', | ||||
|         email: 'test@example.com', | ||||
|         password: 'password123', | ||||
|         role: 'candidate' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/auth/register') | ||||
|         .send(userData) | ||||
|         .expect(201); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('message', 'User created successfully'); | ||||
|       expect(response.body).toHaveProperty('token'); | ||||
|       expect(response.body).toHaveProperty('user'); | ||||
|       expect(response.body.user.email).toBe(userData.email); | ||||
|     }); | ||||
|  | ||||
|     it('should return error for duplicate email', async () => { | ||||
|       const userData = { | ||||
|         firstName: 'Test', | ||||
|         lastName: 'User', | ||||
|         email: 'test@example.com', | ||||
|         password: 'password123', | ||||
|         role: 'candidate' | ||||
|       }; | ||||
|  | ||||
|       // Register first user | ||||
|       await request(app) | ||||
|         .post('/api/auth/register') | ||||
|         .send(userData); | ||||
|  | ||||
|       // Try to register with same email | ||||
|       const response = await request(app) | ||||
|         .post('/api/auth/register') | ||||
|         .send(userData) | ||||
|         .expect(400); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('error', 'User already exists'); | ||||
|     }); | ||||
|  | ||||
|     it('should return error for invalid role', async () => { | ||||
|       const userData = { | ||||
|         firstName: 'Test', | ||||
|         lastName: 'User', | ||||
|         email: 'test@example.com', | ||||
|         password: 'password123', | ||||
|         role: 'invalid_role' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/auth/register') | ||||
|         .send(userData) | ||||
|         .expect(400); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('errors'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /api/auth/login', () => { | ||||
|     beforeEach(async () => { | ||||
|       // Create a test user | ||||
|       const userData = { | ||||
|         firstName: 'Test', | ||||
|         lastName: 'User', | ||||
|         email: 'test@example.com', | ||||
|         password: 'password123', | ||||
|         role: 'candidate' | ||||
|       }; | ||||
|  | ||||
|       await request(app) | ||||
|         .post('/api/auth/register') | ||||
|         .send(userData); | ||||
|     }); | ||||
|  | ||||
|     it('should login with valid credentials', async () => { | ||||
|       const loginData = { | ||||
|         email: 'test@example.com', | ||||
|         password: 'password123' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/auth/login') | ||||
|         .send(loginData) | ||||
|         .expect(200); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('message', 'Login successful'); | ||||
|       expect(response.body).toHaveProperty('token'); | ||||
|       expect(response.body).toHaveProperty('user'); | ||||
|     }); | ||||
|  | ||||
|     it('should return error for invalid credentials', async () => { | ||||
|       const loginData = { | ||||
|         email: 'test@example.com', | ||||
|         password: 'wrongpassword' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/auth/login') | ||||
|         .send(loginData) | ||||
|         .expect(401); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('error', 'Invalid credentials'); | ||||
|     }); | ||||
|  | ||||
|     it('should return error for non-existent user', async () => { | ||||
|       const loginData = { | ||||
|         email: 'nonexistent@example.com', | ||||
|         password: 'password123' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/auth/login') | ||||
|         .send(loginData) | ||||
|         .expect(401); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('error', 'Invalid credentials'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /api/auth/me', () => { | ||||
|     let token; | ||||
|  | ||||
|     beforeEach(async () => { | ||||
|       // Create a test user and get token | ||||
|       const userData = { | ||||
|         firstName: 'Test', | ||||
|         lastName: 'User', | ||||
|         email: 'test@example.com', | ||||
|         password: 'password123', | ||||
|         role: 'candidate' | ||||
|       }; | ||||
|  | ||||
|       const registerResponse = await request(app) | ||||
|         .post('/api/auth/register') | ||||
|         .send(userData); | ||||
|  | ||||
|       token = registerResponse.body.token; | ||||
|     }); | ||||
|  | ||||
|     it('should return user data with valid token', async () => { | ||||
|       const response = await request(app) | ||||
|         .get('/api/auth/me') | ||||
|         .set('Authorization', `Bearer ${token}`) | ||||
|         .expect(200); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('user'); | ||||
|       expect(response.body.user.email).toBe('test@example.com'); | ||||
|     }); | ||||
|  | ||||
|     it('should return error without token', async () => { | ||||
|       const response = await request(app) | ||||
|         .get('/api/auth/me') | ||||
|         .expect(401); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('error', 'Access token required'); | ||||
|     }); | ||||
|  | ||||
|     it('should return error with invalid token', async () => { | ||||
|       const response = await request(app) | ||||
|         .get('/api/auth/me') | ||||
|         .set('Authorization', 'Bearer invalid_token') | ||||
|         .expect(403); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('error', 'Invalid or expired token'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										252
									
								
								backend/src/tests/jobs.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								backend/src/tests/jobs.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,252 @@ | ||||
| const request = require('supertest'); | ||||
| const app = require('../server'); | ||||
| const pool = require('../database/connection'); | ||||
|  | ||||
| describe('Jobs API', () => { | ||||
|   let authToken; | ||||
|   let employerId; | ||||
|  | ||||
|   beforeAll(async () => { | ||||
|     // Create test user and get auth token | ||||
|     const userData = { | ||||
|       firstName: 'Test', | ||||
|       lastName: 'Employer', | ||||
|       email: 'employer@test.com', | ||||
|       password: 'password123', | ||||
|       role: 'employer' | ||||
|     }; | ||||
|  | ||||
|     const registerResponse = await request(app) | ||||
|       .post('/api/auth/register') | ||||
|       .send(userData); | ||||
|  | ||||
|     authToken = registerResponse.body.token; | ||||
|  | ||||
|     // Create employer profile | ||||
|     const employerData = { | ||||
|       companyName: 'Test Company', | ||||
|       industry: 'Technology', | ||||
|       companySize: '50-200', | ||||
|       website: 'https://testcompany.com', | ||||
|       description: 'A test company', | ||||
|       address: '123 Test St', | ||||
|       phone: '+1-555-0123' | ||||
|     }; | ||||
|  | ||||
|     const employerResponse = await request(app) | ||||
|       .post('/api/employers') | ||||
|       .set('Authorization', `Bearer ${authToken}`) | ||||
|       .send(employerData); | ||||
|  | ||||
|     employerId = employerResponse.body.employer.id; | ||||
|   }); | ||||
|  | ||||
|   afterAll(async () => { | ||||
|     await pool.end(); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /api/jobs', () => { | ||||
|     it('should create a new job posting', async () => { | ||||
|       const jobData = { | ||||
|         title: 'Senior Developer', | ||||
|         description: 'We are looking for a senior developer', | ||||
|         requirements: ['5+ years experience', 'JavaScript knowledge'], | ||||
|         responsibilities: ['Develop applications', 'Code reviews'], | ||||
|         location: 'San Francisco, CA', | ||||
|         employmentType: 'full-time', | ||||
|         salaryMin: 100000, | ||||
|         salaryMax: 150000, | ||||
|         remoteAllowed: true, | ||||
|         experienceLevel: 'senior', | ||||
|         skillsRequired: ['JavaScript', 'React', 'Node.js'], | ||||
|         benefits: ['Health insurance', '401k'] | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/jobs') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .send(jobData) | ||||
|         .expect(201); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('message', 'Job posting created successfully'); | ||||
|       expect(response.body).toHaveProperty('job'); | ||||
|       expect(response.body.job.title).toBe(jobData.title); | ||||
|     }); | ||||
|  | ||||
|     it('should return error for missing required fields', async () => { | ||||
|       const jobData = { | ||||
|         title: 'Senior Developer' | ||||
|         // Missing required fields | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/jobs') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .send(jobData) | ||||
|         .expect(400); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('errors'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /api/jobs', () => { | ||||
|     beforeEach(async () => { | ||||
|       // Create a test job | ||||
|       const jobData = { | ||||
|         title: 'Test Job', | ||||
|         description: 'A test job description', | ||||
|         requirements: ['Test requirement'], | ||||
|         responsibilities: ['Test responsibility'], | ||||
|         location: 'Test Location', | ||||
|         employmentType: 'full-time' | ||||
|       }; | ||||
|  | ||||
|       await request(app) | ||||
|         .post('/api/jobs') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .send(jobData); | ||||
|     }); | ||||
|  | ||||
|     it('should return list of jobs', async () => { | ||||
|       const response = await request(app) | ||||
|         .get('/api/jobs') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .expect(200); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('jobs'); | ||||
|       expect(response.body).toHaveProperty('pagination'); | ||||
|       expect(Array.isArray(response.body.jobs)).toBe(true); | ||||
|     }); | ||||
|  | ||||
|     it('should filter jobs by search term', async () => { | ||||
|       const response = await request(app) | ||||
|         .get('/api/jobs?search=Test') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .expect(200); | ||||
|  | ||||
|       expect(response.body.jobs.length).toBeGreaterThan(0); | ||||
|     }); | ||||
|  | ||||
|     it('should filter jobs by location', async () => { | ||||
|       const response = await request(app) | ||||
|         .get('/api/jobs?location=Test') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .expect(200); | ||||
|  | ||||
|       expect(response.body.jobs.length).toBeGreaterThan(0); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /api/jobs/:id', () => { | ||||
|     let jobId; | ||||
|  | ||||
|     beforeEach(async () => { | ||||
|       // Create a test job | ||||
|       const jobData = { | ||||
|         title: 'Test Job for Details', | ||||
|         description: 'A test job description', | ||||
|         requirements: ['Test requirement'], | ||||
|         responsibilities: ['Test responsibility'], | ||||
|         location: 'Test Location', | ||||
|         employmentType: 'full-time' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/jobs') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .send(jobData); | ||||
|  | ||||
|       jobId = response.body.job.id; | ||||
|     }); | ||||
|  | ||||
|     it('should return job details', async () => { | ||||
|       const response = await request(app) | ||||
|         .get(`/api/jobs/${jobId}`) | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .expect(200); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('id', jobId); | ||||
|       expect(response.body).toHaveProperty('title', 'Test Job for Details'); | ||||
|     }); | ||||
|  | ||||
|     it('should return 404 for non-existent job', async () => { | ||||
|       const response = await request(app) | ||||
|         .get('/api/jobs/00000000-0000-0000-0000-000000000000') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .expect(404); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('error', 'Job not found'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('PUT /api/jobs/:id', () => { | ||||
|     let jobId; | ||||
|  | ||||
|     beforeEach(async () => { | ||||
|       // Create a test job | ||||
|       const jobData = { | ||||
|         title: 'Test Job for Update', | ||||
|         description: 'A test job description', | ||||
|         requirements: ['Test requirement'], | ||||
|         responsibilities: ['Test responsibility'], | ||||
|         location: 'Test Location', | ||||
|         employmentType: 'full-time' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/jobs') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .send(jobData); | ||||
|  | ||||
|       jobId = response.body.job.id; | ||||
|     }); | ||||
|  | ||||
|     it('should update job successfully', async () => { | ||||
|       const updateData = { | ||||
|         title: 'Updated Test Job', | ||||
|         description: 'Updated description' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .put(`/api/jobs/${jobId}`) | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .send(updateData) | ||||
|         .expect(200); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('message', 'Job posting updated successfully'); | ||||
|       expect(response.body.job.title).toBe('Updated Test Job'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('DELETE /api/jobs/:id', () => { | ||||
|     let jobId; | ||||
|  | ||||
|     beforeEach(async () => { | ||||
|       // Create a test job | ||||
|       const jobData = { | ||||
|         title: 'Test Job for Delete', | ||||
|         description: 'A test job description', | ||||
|         requirements: ['Test requirement'], | ||||
|         responsibilities: ['Test responsibility'], | ||||
|         location: 'Test Location', | ||||
|         employmentType: 'full-time' | ||||
|       }; | ||||
|  | ||||
|       const response = await request(app) | ||||
|         .post('/api/jobs') | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .send(jobData); | ||||
|  | ||||
|       jobId = response.body.job.id; | ||||
|     }); | ||||
|  | ||||
|     it('should delete job successfully', async () => { | ||||
|       const response = await request(app) | ||||
|         .delete(`/api/jobs/${jobId}`) | ||||
|         .set('Authorization', `Bearer ${authToken}`) | ||||
|         .expect(200); | ||||
|  | ||||
|       expect(response.body).toHaveProperty('message', 'Job posting deleted successfully'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user