Initial commit
This commit is contained in:
12
backend/Dockerfile
Normal file
12
backend/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
36
backend/package.json
Normal file
36
backend/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "mysteryapp-cursor-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend for MysteryApp-Cursor recruiter workflow SAAS",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"migrate": "node src/database/migrate.js",
|
||||
"seed": "node src/database/seed.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.11.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"express-validator": "^7.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"uuid": "^9.0.1",
|
||||
"morgan": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"@types/jest": "^29.5.8"
|
||||
},
|
||||
"keywords": ["recruiter", "saas", "workflow", "hiring"],
|
||||
"author": "MysteryApp-Cursor",
|
||||
"license": "MIT"
|
||||
}
|
||||
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