Initial commit

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

12
backend/Dockerfile Normal file
View 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
View 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"
}

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

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

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

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

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

View File

@@ -0,0 +1,424 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const pool = require('../database/connection');
const { authenticateToken, requireRole } = require('../middleware/auth');
const router = express.Router();
// Get all applications (with filtering)
router.get('/', authenticateToken, async (req, res) => {
try {
const {
jobId,
candidateId,
status,
employerId,
page = 1,
limit = 10
} = req.query;
let query = `
SELECT a.*,
j.title as job_title,
j.employer_id,
e.company_name,
c.user_id as candidate_user_id,
u.first_name,
u.last_name,
u.email as candidate_email
FROM applications a
JOIN jobs j ON a.job_id = j.id
JOIN employers e ON j.employer_id = e.id
JOIN candidates c ON a.candidate_id = c.id
JOIN users u ON c.user_id = u.id
`;
const queryParams = [];
let paramCount = 0;
const conditions = [];
if (jobId) {
paramCount++;
conditions.push(`a.job_id = $${paramCount}`);
queryParams.push(jobId);
}
if (candidateId) {
paramCount++;
conditions.push(`a.candidate_id = $${paramCount}`);
queryParams.push(candidateId);
}
if (status) {
paramCount++;
conditions.push(`a.status = $${paramCount}`);
queryParams.push(status);
}
if (employerId) {
paramCount++;
conditions.push(`j.employer_id = $${paramCount}`);
queryParams.push(employerId);
}
// Role-based filtering
if (req.user.role === 'candidate') {
// Candidates can only see their own applications
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length > 0) {
paramCount++;
conditions.push(`a.candidate_id = $${paramCount}`);
queryParams.push(candidateResult.rows[0].id);
}
} else if (req.user.role === 'employer') {
// Employers can only see applications for their jobs
const employerResult = await pool.query(
'SELECT id FROM employers WHERE user_id = $1',
[req.user.id]
);
if (employerResult.rows.length > 0) {
paramCount++;
conditions.push(`j.employer_id = $${paramCount}`);
queryParams.push(employerResult.rows[0].id);
}
}
if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`;
}
query += ` ORDER BY a.applied_at DESC`;
// Add pagination
const offset = (page - 1) * limit;
paramCount++;
query += ` LIMIT $${paramCount}`;
queryParams.push(limit);
paramCount++;
query += ` OFFSET $${paramCount}`;
queryParams.push(offset);
const result = await pool.query(query, queryParams);
// Get total count for pagination
let countQuery = `
SELECT COUNT(*)
FROM applications a
JOIN jobs j ON a.job_id = j.id
JOIN employers e ON j.employer_id = e.id
JOIN candidates c ON a.candidate_id = c.id
`;
const countParams = [];
let countParamCount = 0;
const countConditions = [];
if (jobId) {
countParamCount++;
countConditions.push(`a.job_id = $${countParamCount}`);
countParams.push(jobId);
}
if (candidateId) {
countParamCount++;
countConditions.push(`a.candidate_id = $${countParamCount}`);
countParams.push(candidateId);
}
if (status) {
countParamCount++;
countConditions.push(`a.status = $${countParamCount}`);
countParams.push(status);
}
if (employerId) {
countParamCount++;
countConditions.push(`j.employer_id = $${countParamCount}`);
countParams.push(employerId);
}
// Role-based filtering for count
if (req.user.role === 'candidate') {
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length > 0) {
countParamCount++;
countConditions.push(`a.candidate_id = $${countParamCount}`);
countParams.push(candidateResult.rows[0].id);
}
} else if (req.user.role === 'employer') {
const employerResult = await pool.query(
'SELECT id FROM employers WHERE user_id = $1',
[req.user.id]
);
if (employerResult.rows.length > 0) {
countParamCount++;
countConditions.push(`j.employer_id = $${countParamCount}`);
countParams.push(employerResult.rows[0].id);
}
}
if (countConditions.length > 0) {
countQuery += ` WHERE ${countConditions.join(' AND ')}`;
}
const countResult = await pool.query(countQuery, countParams);
res.json({
applications: result.rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: parseInt(countResult.rows[0].count),
pages: Math.ceil(countResult.rows[0].count / limit)
}
});
} catch (error) {
console.error('Get applications error:', error);
res.status(500).json({ error: 'Failed to fetch applications' });
}
});
// Get application by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
SELECT a.*,
j.title as job_title,
j.description as job_description,
j.employer_id,
e.company_name,
c.user_id as candidate_user_id,
u.first_name,
u.last_name,
u.email as candidate_email
FROM applications a
JOIN jobs j ON a.job_id = j.id
JOIN employers e ON j.employer_id = e.id
JOIN candidates c ON a.candidate_id = c.id
JOIN users u ON c.user_id = u.id
WHERE a.id = $1
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Application not found' });
}
const application = result.rows[0];
// Check permissions
if (req.user.role === 'candidate') {
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0 || application.candidate_id !== candidateResult.rows[0].id) {
return res.status(403).json({ error: 'Access denied' });
}
} else if (req.user.role === 'employer') {
const employerResult = await pool.query(
'SELECT id FROM employers WHERE user_id = $1',
[req.user.id]
);
if (employerResult.rows.length === 0 || application.employer_id !== employerResult.rows[0].id) {
return res.status(403).json({ error: 'Access denied' });
}
}
res.json(application);
} catch (error) {
console.error('Get application error:', error);
res.status(500).json({ error: 'Failed to fetch application' });
}
});
// Create application
router.post('/', authenticateToken, requireRole(['candidate']), [
body('jobId').isUUID(),
body('coverLetter').optional().trim(),
body('notes').optional().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { jobId, coverLetter, notes } = req.body;
// Get candidate ID for the current user
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0) {
return res.status(400).json({ error: 'Candidate profile not found' });
}
const candidateId = candidateResult.rows[0].id;
// Check if job exists and is active
const jobResult = await pool.query(
'SELECT id, status FROM jobs WHERE id = $1',
[jobId]
);
if (jobResult.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
if (jobResult.rows[0].status !== 'active') {
return res.status(400).json({ error: 'Job is not accepting applications' });
}
// Check if application already exists
const existingApplication = await pool.query(
'SELECT id FROM applications WHERE job_id = $1 AND candidate_id = $2',
[jobId, candidateId]
);
if (existingApplication.rows.length > 0) {
return res.status(400).json({ error: 'Application already exists' });
}
const result = await pool.query(`
INSERT INTO applications (job_id, candidate_id, cover_letter, notes)
VALUES ($1, $2, $3, $4)
RETURNING *
`, [jobId, candidateId, coverLetter, notes]);
res.status(201).json({
message: 'Application submitted successfully',
application: result.rows[0]
});
} catch (error) {
console.error('Create application error:', error);
res.status(500).json({ error: 'Failed to submit application' });
}
});
// Update application status
router.put('/:id/status', authenticateToken, [
body('status').isIn(['applied', 'reviewed', 'shortlisted', 'interviewed', 'offered', 'rejected', 'withdrawn'])
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const { status } = req.body;
// Check if application exists and user has permission
const applicationResult = await pool.query(`
SELECT a.*, j.employer_id, e.user_id as employer_user_id
FROM applications a
JOIN jobs j ON a.job_id = j.id
JOIN employers e ON j.employer_id = e.id
WHERE a.id = $1
`, [id]);
if (applicationResult.rows.length === 0) {
return res.status(404).json({ error: 'Application not found' });
}
const application = applicationResult.rows[0];
// Check permissions
if (req.user.role === 'candidate') {
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0 || application.candidate_id !== candidateResult.rows[0].id) {
return res.status(403).json({ error: 'Access denied' });
}
// Candidates can only withdraw their applications
if (status !== 'withdrawn') {
return res.status(403).json({ error: 'Candidates can only withdraw applications' });
}
} else if (req.user.role === 'employer') {
if (application.employer_user_id !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter') {
return res.status(403).json({ error: 'Access denied' });
}
const result = await pool.query(
'UPDATE applications SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
[status, id]
);
res.json({
message: 'Application status updated successfully',
application: result.rows[0]
});
} catch (error) {
console.error('Update application status error:', error);
res.status(500).json({ error: 'Failed to update application status' });
}
});
// Update application notes
router.put('/:id/notes', authenticateToken, [
body('notes').notEmpty().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const { notes } = req.body;
// Check if application exists and user has permission
const applicationResult = await pool.query(`
SELECT a.*, j.employer_id, e.user_id as employer_user_id
FROM applications a
JOIN jobs j ON a.job_id = j.id
JOIN employers e ON j.employer_id = e.id
WHERE a.id = $1
`, [id]);
if (applicationResult.rows.length === 0) {
return res.status(404).json({ error: 'Application not found' });
}
const application = applicationResult.rows[0];
// Check permissions (employers, recruiters, and admins can add notes)
if (req.user.role === 'candidate') {
return res.status(403).json({ error: 'Candidates cannot add notes to applications' });
} else if (req.user.role === 'employer') {
if (application.employer_user_id !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter') {
return res.status(403).json({ error: 'Access denied' });
}
const result = await pool.query(
'UPDATE applications SET notes = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
[notes, id]
);
res.json({
message: 'Application notes updated successfully',
application: result.rows[0]
});
} catch (error) {
console.error('Update application notes error:', error);
res.status(500).json({ error: 'Failed to update application notes' });
}
});
module.exports = router;

153
backend/src/routes/auth.js Normal file
View File

@@ -0,0 +1,153 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { body, validationResult } = require('express-validator');
const pool = require('../database/connection');
const { authenticateToken } = require('../middleware/auth');
const router = express.Router();
// Register
router.post('/register', [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
body('firstName').notEmpty().trim(),
body('lastName').notEmpty().trim(),
body('role').isIn(['recruiter', 'employer', 'candidate'])
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password, firstName, lastName, role } = req.body;
// Check if user already exists
const existingUser = await pool.query(
'SELECT id FROM users WHERE email = $1',
[email]
);
if (existingUser.rows.length > 0) {
return res.status(400).json({ error: 'User already exists' });
}
// Hash password
const passwordHash = await bcrypt.hash(password, 10);
// Create user
const userResult = await pool.query(
'INSERT INTO users (email, password_hash, first_name, last_name, role) VALUES ($1, $2, $3, $4, $5) RETURNING id, email, first_name, last_name, role',
[email, passwordHash, firstName, lastName, role]
);
const user = userResult.rows[0];
// Generate JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.status(201).json({
message: 'User created successfully',
token,
user: {
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
role: user.role
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ error: 'Registration failed' });
}
});
// Login
router.post('/login', [
body('email').isEmail().normalizeEmail(),
body('password').notEmpty()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
// Get user
const userResult = await pool.query(
'SELECT id, email, password_hash, first_name, last_name, role, is_active FROM users WHERE email = $1',
[email]
);
if (userResult.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = userResult.rows[0];
if (!user.is_active) {
return res.status(401).json({ error: 'Account deactivated' });
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password_hash);
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
message: 'Login successful',
token,
user: {
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
role: user.role
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Login failed' });
}
});
// Get current user
router.get('/me', authenticateToken, async (req, res) => {
try {
res.json({
user: {
id: req.user.id,
email: req.user.email,
firstName: req.user.first_name,
lastName: req.user.last_name,
role: req.user.role
}
});
} catch (error) {
console.error('Get user error:', error);
res.status(500).json({ error: 'Failed to get user information' });
}
});
// Logout (client-side token removal)
router.post('/logout', authenticateToken, (req, res) => {
res.json({ message: 'Logout successful' });
});
module.exports = router;

View File

@@ -0,0 +1,374 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const pool = require('../database/connection');
const { authenticateToken, requireRole } = require('../middleware/auth');
const router = express.Router();
// Get all candidates
router.get('/', authenticateToken, async (req, res) => {
try {
const { skills, experienceLevel, location, page = 1, limit = 10 } = req.query;
let query = `
SELECT c.*, u.email, u.first_name, u.last_name
FROM candidates c
JOIN users u ON c.user_id = u.id
`;
const queryParams = [];
let paramCount = 0;
const conditions = [];
if (skills) {
const skillArray = skills.split(',').map(s => s.trim());
paramCount++;
conditions.push(`c.skills && $${paramCount}`);
queryParams.push(skillArray);
}
if (experienceLevel) {
paramCount++;
conditions.push(`c.experience_level = $${paramCount}`);
queryParams.push(experienceLevel);
}
if (location) {
paramCount++;
conditions.push(`c.location ILIKE $${paramCount}`);
queryParams.push(`%${location}%`);
}
if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`;
}
query += ` ORDER BY c.created_at DESC`;
// Add pagination
const offset = (page - 1) * limit;
paramCount++;
query += ` LIMIT $${paramCount}`;
queryParams.push(limit);
paramCount++;
query += ` OFFSET $${paramCount}`;
queryParams.push(offset);
const result = await pool.query(query, queryParams);
// Get total count for pagination
let countQuery = `
SELECT COUNT(*)
FROM candidates c
JOIN users u ON c.user_id = u.id
`;
const countParams = [];
let countParamCount = 0;
const countConditions = [];
if (skills) {
const skillArray = skills.split(',').map(s => s.trim());
countParamCount++;
countConditions.push(`c.skills && $${countParamCount}`);
countParams.push(skillArray);
}
if (experienceLevel) {
countParamCount++;
countConditions.push(`c.experience_level = $${countParamCount}`);
countParams.push(experienceLevel);
}
if (location) {
countParamCount++;
countConditions.push(`c.location ILIKE $${countParamCount}`);
countParams.push(`%${location}%`);
}
if (countConditions.length > 0) {
countQuery += ` WHERE ${countConditions.join(' AND ')}`;
}
const countResult = await pool.query(countQuery, countParams);
res.json({
candidates: result.rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: parseInt(countResult.rows[0].count),
pages: Math.ceil(countResult.rows[0].count / limit)
}
});
} catch (error) {
console.error('Get candidates error:', error);
res.status(500).json({ error: 'Failed to fetch candidates' });
}
});
// Get candidate by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
SELECT c.*, u.email, u.first_name, u.last_name
FROM candidates c
JOIN users u ON c.user_id = u.id
WHERE c.id = $1
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Candidate not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Get candidate error:', error);
res.status(500).json({ error: 'Failed to fetch candidate' });
}
});
// Create candidate profile
router.post('/', authenticateToken, requireRole(['candidate']), [
body('phone').optional().trim(),
body('location').optional().trim(),
body('linkedinUrl').optional().isURL(),
body('githubUrl').optional().isURL(),
body('portfolioUrl').optional().isURL(),
body('bio').optional().trim(),
body('skills').optional().isArray(),
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
body('availability').optional().trim(),
body('salaryExpectation').optional().isInt({ min: 0 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
phone,
location,
linkedinUrl,
githubUrl,
portfolioUrl,
bio,
skills,
experienceLevel,
availability,
salaryExpectation
} = req.body;
// Check if candidate profile already exists for this user
const existingCandidate = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (existingCandidate.rows.length > 0) {
return res.status(400).json({ error: 'Candidate profile already exists' });
}
const result = await pool.query(`
INSERT INTO candidates (user_id, phone, location, linkedin_url, github_url, portfolio_url, bio, skills, experience_level, availability, salary_expectation)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`, [req.user.id, phone, location, linkedinUrl, githubUrl, portfolioUrl, bio, skills, experienceLevel, availability, salaryExpectation]);
res.status(201).json({
message: 'Candidate profile created successfully',
candidate: result.rows[0]
});
} catch (error) {
console.error('Create candidate error:', error);
res.status(500).json({ error: 'Failed to create candidate profile' });
}
});
// Update candidate profile
router.put('/:id', authenticateToken, [
body('phone').optional().trim(),
body('location').optional().trim(),
body('linkedinUrl').optional().isURL(),
body('githubUrl').optional().isURL(),
body('portfolioUrl').optional().isURL(),
body('bio').optional().trim(),
body('skills').optional().isArray(),
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
body('availability').optional().trim(),
body('salaryExpectation').optional().isInt({ min: 0 })
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const {
phone,
location,
linkedinUrl,
githubUrl,
portfolioUrl,
bio,
skills,
experienceLevel,
availability,
salaryExpectation
} = req.body;
// Check if candidate exists and user has permission
const candidateResult = await pool.query(
'SELECT user_id FROM candidates WHERE id = $1',
[id]
);
if (candidateResult.rows.length === 0) {
return res.status(404).json({ error: 'Candidate not found' });
}
// Users can only update their own candidate profile unless they're admin
if (candidateResult.rows[0].user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
const updateFields = [];
const updateValues = [];
let paramCount = 1;
if (phone !== undefined) {
updateFields.push(`phone = $${paramCount}`);
updateValues.push(phone);
paramCount++;
}
if (location !== undefined) {
updateFields.push(`location = $${paramCount}`);
updateValues.push(location);
paramCount++;
}
if (linkedinUrl !== undefined) {
updateFields.push(`linkedin_url = $${paramCount}`);
updateValues.push(linkedinUrl);
paramCount++;
}
if (githubUrl !== undefined) {
updateFields.push(`github_url = $${paramCount}`);
updateValues.push(githubUrl);
paramCount++;
}
if (portfolioUrl !== undefined) {
updateFields.push(`portfolio_url = $${paramCount}`);
updateValues.push(portfolioUrl);
paramCount++;
}
if (bio !== undefined) {
updateFields.push(`bio = $${paramCount}`);
updateValues.push(bio);
paramCount++;
}
if (skills !== undefined) {
updateFields.push(`skills = $${paramCount}`);
updateValues.push(skills);
paramCount++;
}
if (experienceLevel !== undefined) {
updateFields.push(`experience_level = $${paramCount}`);
updateValues.push(experienceLevel);
paramCount++;
}
if (availability !== undefined) {
updateFields.push(`availability = $${paramCount}`);
updateValues.push(availability);
paramCount++;
}
if (salaryExpectation !== undefined) {
updateFields.push(`salary_expectation = $${paramCount}`);
updateValues.push(salaryExpectation);
paramCount++;
}
if (updateFields.length === 0) {
return res.status(400).json({ error: 'No fields to update' });
}
updateValues.push(id);
const query = `UPDATE candidates SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING *`;
const result = await pool.query(query, updateValues);
res.json({
message: 'Candidate profile updated successfully',
candidate: result.rows[0]
});
} catch (error) {
console.error('Update candidate error:', error);
res.status(500).json({ error: 'Failed to update candidate profile' });
}
});
// Get candidate's applications
router.get('/:id/applications', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const { status, page = 1, limit = 10 } = req.query;
let query = `
SELECT a.*, j.title as job_title, j.employer_id, e.company_name
FROM applications a
JOIN jobs j ON a.job_id = j.id
JOIN employers e ON j.employer_id = e.id
WHERE a.candidate_id = $1
`;
const queryParams = [id];
let paramCount = 1;
if (status) {
paramCount++;
query += ` AND a.status = $${paramCount}`;
queryParams.push(status);
}
query += ` ORDER BY a.applied_at DESC`;
// Add pagination
const offset = (page - 1) * limit;
paramCount++;
query += ` LIMIT $${paramCount}`;
queryParams.push(limit);
paramCount++;
query += ` OFFSET $${paramCount}`;
queryParams.push(offset);
const result = await pool.query(query, queryParams);
// Get total count for pagination
let countQuery = `
SELECT COUNT(*)
FROM applications a
WHERE a.candidate_id = $1
`;
const countParams = [id];
if (status) {
countQuery += ' AND a.status = $2';
countParams.push(status);
}
const countResult = await pool.query(countQuery, countParams);
res.json({
applications: result.rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: parseInt(countResult.rows[0].count),
pages: Math.ceil(countResult.rows[0].count / limit)
}
});
} catch (error) {
console.error('Get candidate applications error:', error);
res.status(500).json({ error: 'Failed to fetch candidate applications' });
}
});
module.exports = router;

View File

@@ -0,0 +1,256 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const pool = require('../database/connection');
const { authenticateToken, requireRole } = require('../middleware/auth');
const router = express.Router();
// Get all employers
router.get('/', authenticateToken, async (req, res) => {
try {
const result = await pool.query(`
SELECT e.*, u.email, u.first_name, u.last_name
FROM employers e
JOIN users u ON e.user_id = u.id
ORDER BY e.created_at DESC
`);
res.json(result.rows);
} catch (error) {
console.error('Get employers error:', error);
res.status(500).json({ error: 'Failed to fetch employers' });
}
});
// Get employer by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
SELECT e.*, u.email, u.first_name, u.last_name
FROM employers e
JOIN users u ON e.user_id = u.id
WHERE e.id = $1
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Employer not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Get employer error:', error);
res.status(500).json({ error: 'Failed to fetch employer' });
}
});
// Create employer profile
router.post('/', authenticateToken, requireRole(['employer']), [
body('companyName').notEmpty().trim(),
body('industry').optional().trim(),
body('companySize').optional().trim(),
body('website').optional().isURL(),
body('description').optional().trim(),
body('address').optional().trim(),
body('phone').optional().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
companyName,
industry,
companySize,
website,
description,
address,
phone
} = req.body;
// Check if employer profile already exists for this user
const existingEmployer = await pool.query(
'SELECT id FROM employers WHERE user_id = $1',
[req.user.id]
);
if (existingEmployer.rows.length > 0) {
return res.status(400).json({ error: 'Employer profile already exists' });
}
const result = await pool.query(`
INSERT INTO employers (user_id, company_name, industry, company_size, website, description, address, phone)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
`, [req.user.id, companyName, industry, companySize, website, description, address, phone]);
res.status(201).json({
message: 'Employer profile created successfully',
employer: result.rows[0]
});
} catch (error) {
console.error('Create employer error:', error);
res.status(500).json({ error: 'Failed to create employer profile' });
}
});
// Update employer profile
router.put('/:id', authenticateToken, [
body('companyName').optional().notEmpty().trim(),
body('industry').optional().trim(),
body('companySize').optional().trim(),
body('website').optional().isURL(),
body('description').optional().trim(),
body('address').optional().trim(),
body('phone').optional().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const {
companyName,
industry,
companySize,
website,
description,
address,
phone
} = req.body;
// Check if employer exists and user has permission
const employerResult = await pool.query(
'SELECT user_id FROM employers WHERE id = $1',
[id]
);
if (employerResult.rows.length === 0) {
return res.status(404).json({ error: 'Employer not found' });
}
// Users can only update their own employer profile unless they're admin
if (employerResult.rows[0].user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
const updateFields = [];
const updateValues = [];
let paramCount = 1;
if (companyName) {
updateFields.push(`company_name = $${paramCount}`);
updateValues.push(companyName);
paramCount++;
}
if (industry !== undefined) {
updateFields.push(`industry = $${paramCount}`);
updateValues.push(industry);
paramCount++;
}
if (companySize !== undefined) {
updateFields.push(`company_size = $${paramCount}`);
updateValues.push(companySize);
paramCount++;
}
if (website !== undefined) {
updateFields.push(`website = $${paramCount}`);
updateValues.push(website);
paramCount++;
}
if (description !== undefined) {
updateFields.push(`description = $${paramCount}`);
updateValues.push(description);
paramCount++;
}
if (address !== undefined) {
updateFields.push(`address = $${paramCount}`);
updateValues.push(address);
paramCount++;
}
if (phone !== undefined) {
updateFields.push(`phone = $${paramCount}`);
updateValues.push(phone);
paramCount++;
}
if (updateFields.length === 0) {
return res.status(400).json({ error: 'No fields to update' });
}
updateValues.push(id);
const query = `UPDATE employers SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING *`;
const result = await pool.query(query, updateValues);
res.json({
message: 'Employer profile updated successfully',
employer: result.rows[0]
});
} catch (error) {
console.error('Update employer error:', error);
res.status(500).json({ error: 'Failed to update employer profile' });
}
});
// Get employer's jobs
router.get('/:id/jobs', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const { status, page = 1, limit = 10 } = req.query;
let query = `
SELECT * FROM jobs
WHERE employer_id = $1
`;
const queryParams = [id];
let paramCount = 1;
if (status) {
paramCount++;
query += ` AND status = $${paramCount}`;
queryParams.push(status);
}
query += ` ORDER BY created_at DESC`;
// Add pagination
const offset = (page - 1) * limit;
paramCount++;
query += ` LIMIT $${paramCount}`;
queryParams.push(limit);
paramCount++;
query += ` OFFSET $${paramCount}`;
queryParams.push(offset);
const result = await pool.query(query, queryParams);
// Get total count for pagination
let countQuery = 'SELECT COUNT(*) FROM jobs WHERE employer_id = $1';
const countParams = [id];
if (status) {
countQuery += ' AND status = $2';
countParams.push(status);
}
const countResult = await pool.query(countQuery, countParams);
res.json({
jobs: result.rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: parseInt(countResult.rows[0].count),
pages: Math.ceil(countResult.rows[0].count / limit)
}
});
} catch (error) {
console.error('Get employer jobs error:', error);
res.status(500).json({ error: 'Failed to fetch employer jobs' });
}
});
module.exports = router;

478
backend/src/routes/jobs.js Normal file
View File

@@ -0,0 +1,478 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const pool = require('../database/connection');
const { authenticateToken, requireRole } = require('../middleware/auth');
const router = express.Router();
// Get all jobs with filtering and search
router.get('/', authenticateToken, async (req, res) => {
try {
const {
search,
location,
employmentType,
experienceLevel,
skills,
salaryMin,
salaryMax,
remoteAllowed,
status = 'active',
page = 1,
limit = 10
} = req.query;
let query = `
SELECT j.*, e.company_name, e.industry, e.company_size
FROM jobs j
JOIN employers e ON j.employer_id = e.id
`;
const queryParams = [];
let paramCount = 0;
const conditions = [];
// Always filter by status
paramCount++;
conditions.push(`j.status = $${paramCount}`);
queryParams.push(status);
if (search) {
paramCount++;
conditions.push(`(j.title ILIKE $${paramCount} OR j.description ILIKE $${paramCount})`);
queryParams.push(`%${search}%`);
}
if (location) {
paramCount++;
conditions.push(`j.location ILIKE $${paramCount}`);
queryParams.push(`%${location}%`);
}
if (employmentType) {
paramCount++;
conditions.push(`j.employment_type = $${paramCount}`);
queryParams.push(employmentType);
}
if (experienceLevel) {
paramCount++;
conditions.push(`j.experience_level = $${paramCount}`);
queryParams.push(experienceLevel);
}
if (skills) {
const skillArray = skills.split(',').map(s => s.trim());
paramCount++;
conditions.push(`j.skills_required && $${paramCount}`);
queryParams.push(skillArray);
}
if (salaryMin) {
paramCount++;
conditions.push(`j.salary_max >= $${paramCount}`);
queryParams.push(parseInt(salaryMin));
}
if (salaryMax) {
paramCount++;
conditions.push(`j.salary_min <= $${paramCount}`);
queryParams.push(parseInt(salaryMax));
}
if (remoteAllowed === 'true') {
conditions.push(`j.remote_allowed = true`);
}
if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`;
}
query += ` ORDER BY j.created_at DESC`;
// Add pagination
const offset = (page - 1) * limit;
paramCount++;
query += ` LIMIT $${paramCount}`;
queryParams.push(limit);
paramCount++;
query += ` OFFSET $${paramCount}`;
queryParams.push(offset);
const result = await pool.query(query, queryParams);
// Get total count for pagination
let countQuery = `
SELECT COUNT(*)
FROM jobs j
JOIN employers e ON j.employer_id = e.id
`;
const countParams = [];
let countParamCount = 0;
const countConditions = [];
countParamCount++;
countConditions.push(`j.status = $${countParamCount}`);
countParams.push(status);
if (search) {
countParamCount++;
countConditions.push(`(j.title ILIKE $${countParamCount} OR j.description ILIKE $${countParamCount})`);
countParams.push(`%${search}%`);
}
if (location) {
countParamCount++;
countConditions.push(`j.location ILIKE $${countParamCount}`);
countParams.push(`%${location}%`);
}
if (employmentType) {
countParamCount++;
countConditions.push(`j.employment_type = $${countParamCount}`);
countParams.push(employmentType);
}
if (experienceLevel) {
countParamCount++;
countConditions.push(`j.experience_level = $${countParamCount}`);
countParams.push(experienceLevel);
}
if (skills) {
const skillArray = skills.split(',').map(s => s.trim());
countParamCount++;
countConditions.push(`j.skills_required && $${countParamCount}`);
countParams.push(skillArray);
}
if (salaryMin) {
countParamCount++;
countConditions.push(`j.salary_max >= $${countParamCount}`);
countParams.push(parseInt(salaryMin));
}
if (salaryMax) {
countParamCount++;
countConditions.push(`j.salary_min <= $${countParamCount}`);
countParams.push(parseInt(salaryMax));
}
if (remoteAllowed === 'true') {
countConditions.push(`j.remote_allowed = true`);
}
if (countConditions.length > 0) {
countQuery += ` WHERE ${countConditions.join(' AND ')}`;
}
const countResult = await pool.query(countQuery, countParams);
res.json({
jobs: result.rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: parseInt(countResult.rows[0].count),
pages: Math.ceil(countResult.rows[0].count / limit)
}
});
} catch (error) {
console.error('Get jobs error:', error);
res.status(500).json({ error: 'Failed to fetch jobs' });
}
});
// Get job by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
SELECT j.*, e.company_name, e.industry, e.company_size, e.website, e.description as company_description
FROM jobs j
JOIN employers e ON j.employer_id = e.id
WHERE j.id = $1
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Get job error:', error);
res.status(500).json({ error: 'Failed to fetch job' });
}
});
// Create job posting
router.post('/', authenticateToken, requireRole(['employer', 'recruiter']), [
body('title').notEmpty().trim(),
body('description').notEmpty().trim(),
body('requirements').isArray(),
body('responsibilities').isArray(),
body('location').notEmpty().trim(),
body('employmentType').isIn(['full-time', 'part-time', 'contract', 'internship']),
body('salaryMin').optional().isInt({ min: 0 }),
body('salaryMax').optional().isInt({ min: 0 }),
body('currency').optional().isLength({ min: 3, max: 3 }),
body('remoteAllowed').optional().isBoolean(),
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
body('skillsRequired').optional().isArray(),
body('benefits').optional().isArray(),
body('applicationDeadline').optional().isISO8601()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
title,
description,
requirements,
responsibilities,
location,
employmentType,
salaryMin,
salaryMax,
currency = 'USD',
remoteAllowed = false,
experienceLevel,
skillsRequired,
benefits,
applicationDeadline
} = req.body;
// Get employer_id for the current user
let employerId;
if (req.user.role === 'employer') {
const employerResult = await pool.query(
'SELECT id FROM employers WHERE user_id = $1',
[req.user.id]
);
if (employerResult.rows.length === 0) {
return res.status(400).json({ error: 'Employer profile not found' });
}
employerId = employerResult.rows[0].id;
} else {
// For recruiters, they need to specify which employer
const { employerId: providedEmployerId } = req.body;
if (!providedEmployerId) {
return res.status(400).json({ error: 'Employer ID required for recruiters' });
}
employerId = providedEmployerId;
}
const result = await pool.query(`
INSERT INTO jobs (employer_id, title, description, requirements, responsibilities, location, employment_type, salary_min, salary_max, currency, remote_allowed, experience_level, skills_required, benefits, application_deadline)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING *
`, [employerId, title, description, requirements, responsibilities, location, employmentType, salaryMin, salaryMax, currency, remoteAllowed, experienceLevel, skillsRequired, benefits, applicationDeadline]);
res.status(201).json({
message: 'Job posting created successfully',
job: result.rows[0]
});
} catch (error) {
console.error('Create job error:', error);
res.status(500).json({ error: 'Failed to create job posting' });
}
});
// Update job posting
router.put('/:id', authenticateToken, [
body('title').optional().notEmpty().trim(),
body('description').optional().notEmpty().trim(),
body('requirements').optional().isArray(),
body('responsibilities').optional().isArray(),
body('location').optional().notEmpty().trim(),
body('employmentType').optional().isIn(['full-time', 'part-time', 'contract', 'internship']),
body('salaryMin').optional().isInt({ min: 0 }),
body('salaryMax').optional().isInt({ min: 0 }),
body('currency').optional().isLength({ min: 3, max: 3 }),
body('status').optional().isIn(['active', 'paused', 'closed', 'draft']),
body('remoteAllowed').optional().isBoolean(),
body('experienceLevel').optional().isIn(['entry', 'mid', 'senior', 'lead', 'executive']),
body('skillsRequired').optional().isArray(),
body('benefits').optional().isArray(),
body('applicationDeadline').optional().isISO8601()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const {
title,
description,
requirements,
responsibilities,
location,
employmentType,
salaryMin,
salaryMax,
currency,
status,
remoteAllowed,
experienceLevel,
skillsRequired,
benefits,
applicationDeadline
} = req.body;
// Check if job exists and user has permission
const jobResult = await pool.query(`
SELECT j.*, e.user_id as employer_user_id
FROM jobs j
JOIN employers e ON j.employer_id = e.id
WHERE j.id = $1
`, [id]);
if (jobResult.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
const job = jobResult.rows[0];
// Users can only update jobs from their own employer unless they're admin
if (job.employer_user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
const updateFields = [];
const updateValues = [];
let paramCount = 1;
if (title) {
updateFields.push(`title = $${paramCount}`);
updateValues.push(title);
paramCount++;
}
if (description) {
updateFields.push(`description = $${paramCount}`);
updateValues.push(description);
paramCount++;
}
if (requirements) {
updateFields.push(`requirements = $${paramCount}`);
updateValues.push(requirements);
paramCount++;
}
if (responsibilities) {
updateFields.push(`responsibilities = $${paramCount}`);
updateValues.push(responsibilities);
paramCount++;
}
if (location) {
updateFields.push(`location = $${paramCount}`);
updateValues.push(location);
paramCount++;
}
if (employmentType) {
updateFields.push(`employment_type = $${paramCount}`);
updateValues.push(employmentType);
paramCount++;
}
if (salaryMin !== undefined) {
updateFields.push(`salary_min = $${paramCount}`);
updateValues.push(salaryMin);
paramCount++;
}
if (salaryMax !== undefined) {
updateFields.push(`salary_max = $${paramCount}`);
updateValues.push(salaryMax);
paramCount++;
}
if (currency) {
updateFields.push(`currency = $${paramCount}`);
updateValues.push(currency);
paramCount++;
}
if (status) {
updateFields.push(`status = $${paramCount}`);
updateValues.push(status);
paramCount++;
}
if (remoteAllowed !== undefined) {
updateFields.push(`remote_allowed = $${paramCount}`);
updateValues.push(remoteAllowed);
paramCount++;
}
if (experienceLevel) {
updateFields.push(`experience_level = $${paramCount}`);
updateValues.push(experienceLevel);
paramCount++;
}
if (skillsRequired) {
updateFields.push(`skills_required = $${paramCount}`);
updateValues.push(skillsRequired);
paramCount++;
}
if (benefits) {
updateFields.push(`benefits = $${paramCount}`);
updateValues.push(benefits);
paramCount++;
}
if (applicationDeadline) {
updateFields.push(`application_deadline = $${paramCount}`);
updateValues.push(applicationDeadline);
paramCount++;
}
if (updateFields.length === 0) {
return res.status(400).json({ error: 'No fields to update' });
}
updateValues.push(id);
const query = `UPDATE jobs SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING *`;
const result = await pool.query(query, updateValues);
res.json({
message: 'Job posting updated successfully',
job: result.rows[0]
});
} catch (error) {
console.error('Update job error:', error);
res.status(500).json({ error: 'Failed to update job posting' });
}
});
// Delete job posting
router.delete('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
// Check if job exists and user has permission
const jobResult = await pool.query(`
SELECT j.*, e.user_id as employer_user_id
FROM jobs j
JOIN employers e ON j.employer_id = e.id
WHERE j.id = $1
`, [id]);
if (jobResult.rows.length === 0) {
return res.status(404).json({ error: 'Job not found' });
}
const job = jobResult.rows[0];
// Users can only delete jobs from their own employer unless they're admin
if (job.employer_user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
await pool.query('DELETE FROM jobs WHERE id = $1', [id]);
res.json({ message: 'Job posting deleted successfully' });
} catch (error) {
console.error('Delete job error:', error);
res.status(500).json({ error: 'Failed to delete job posting' });
}
});
module.exports = router;

View File

@@ -0,0 +1,298 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const pool = require('../database/connection');
const { authenticateToken, requireRole } = require('../middleware/auth');
const router = express.Router();
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(__dirname, '../../uploads/resumes');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueName = `${uuidv4()}-${file.originalname}`;
cb(null, uniqueName);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
},
fileFilter: (req, file, cb) => {
const allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only PDF, DOC, DOCX, and TXT files are allowed.'));
}
}
});
// Get all resumes for a candidate
router.get('/candidate/:candidateId', authenticateToken, async (req, res) => {
try {
const { candidateId } = req.params;
// Check permissions
if (req.user.role === 'candidate') {
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0 || candidateResult.rows[0].id !== candidateId) {
return res.status(403).json({ error: 'Access denied' });
}
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter' && req.user.role !== 'employer') {
return res.status(403).json({ error: 'Access denied' });
}
const result = await pool.query(
'SELECT * FROM resumes WHERE candidate_id = $1 ORDER BY uploaded_at DESC',
[candidateId]
);
res.json(result.rows);
} catch (error) {
console.error('Get resumes error:', error);
res.status(500).json({ error: 'Failed to fetch resumes' });
}
});
// Get resume by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(
'SELECT * FROM resumes WHERE id = $1',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Resume not found' });
}
const resume = result.rows[0];
// Check permissions
if (req.user.role === 'candidate') {
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0 || candidateResult.rows[0].id !== resume.candidate_id) {
return res.status(403).json({ error: 'Access denied' });
}
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter' && req.user.role !== 'employer') {
return res.status(403).json({ error: 'Access denied' });
}
res.json(resume);
} catch (error) {
console.error('Get resume error:', error);
res.status(500).json({ error: 'Failed to fetch resume' });
}
});
// Upload resume
router.post('/upload', authenticateToken, requireRole(['candidate']), upload.single('resume'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Get candidate ID for the current user
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0) {
return res.status(400).json({ error: 'Candidate profile not found' });
}
const candidateId = candidateResult.rows[0].id;
// If this is set as primary, unset other primary resumes
if (req.body.isPrimary === 'true') {
await pool.query(
'UPDATE resumes SET is_primary = false WHERE candidate_id = $1',
[candidateId]
);
}
const result = await pool.query(`
INSERT INTO resumes (candidate_id, filename, original_name, file_path, file_size, mime_type, is_primary)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`, [
candidateId,
req.file.filename,
req.file.originalname,
req.file.path,
req.file.size,
req.file.mimetype,
req.body.isPrimary === 'true'
]);
res.status(201).json({
message: 'Resume uploaded successfully',
resume: result.rows[0]
});
} catch (error) {
console.error('Upload resume error:', error);
res.status(500).json({ error: 'Failed to upload resume' });
}
});
// Download resume
router.get('/:id/download', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(
'SELECT * FROM resumes WHERE id = $1',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Resume not found' });
}
const resume = result.rows[0];
// Check permissions
if (req.user.role === 'candidate') {
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0 || candidateResult.rows[0].id !== resume.candidate_id) {
return res.status(403).json({ error: 'Access denied' });
}
} else if (req.user.role !== 'admin' && req.user.role !== 'recruiter' && req.user.role !== 'employer') {
return res.status(403).json({ error: 'Access denied' });
}
// Check if file exists
if (!fs.existsSync(resume.file_path)) {
return res.status(404).json({ error: 'Resume file not found' });
}
res.download(resume.file_path, resume.original_name);
} catch (error) {
console.error('Download resume error:', error);
res.status(500).json({ error: 'Failed to download resume' });
}
});
// Set primary resume
router.put('/:id/primary', authenticateToken, requireRole(['candidate']), async (req, res) => {
try {
const { id } = req.params;
// Get candidate ID for the current user
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0) {
return res.status(400).json({ error: 'Candidate profile not found' });
}
const candidateId = candidateResult.rows[0].id;
// Check if resume exists and belongs to the candidate
const resumeResult = await pool.query(
'SELECT id FROM resumes WHERE id = $1 AND candidate_id = $2',
[id, candidateId]
);
if (resumeResult.rows.length === 0) {
return res.status(404).json({ error: 'Resume not found' });
}
// Unset other primary resumes
await pool.query(
'UPDATE resumes SET is_primary = false WHERE candidate_id = $1',
[candidateId]
);
// Set this resume as primary
const result = await pool.query(
'UPDATE resumes SET is_primary = true WHERE id = $1 RETURNING *',
[id]
);
res.json({
message: 'Primary resume updated successfully',
resume: result.rows[0]
});
} catch (error) {
console.error('Set primary resume error:', error);
res.status(500).json({ error: 'Failed to set primary resume' });
}
});
// Delete resume
router.delete('/:id', authenticateToken, requireRole(['candidate']), async (req, res) => {
try {
const { id } = req.params;
// Get candidate ID for the current user
const candidateResult = await pool.query(
'SELECT id FROM candidates WHERE user_id = $1',
[req.user.id]
);
if (candidateResult.rows.length === 0) {
return res.status(400).json({ error: 'Candidate profile not found' });
}
const candidateId = candidateResult.rows[0].id;
// Check if resume exists and belongs to the candidate
const resumeResult = await pool.query(
'SELECT * FROM resumes WHERE id = $1 AND candidate_id = $2',
[id, candidateId]
);
if (resumeResult.rows.length === 0) {
return res.status(404).json({ error: 'Resume not found' });
}
const resume = resumeResult.rows[0];
// Delete file from filesystem
if (fs.existsSync(resume.file_path)) {
fs.unlinkSync(resume.file_path);
}
// Delete from database
await pool.query('DELETE FROM resumes WHERE id = $1', [id]);
res.json({ message: 'Resume deleted successfully' });
} catch (error) {
console.error('Delete resume error:', error);
res.status(500).json({ error: 'Failed to delete resume' });
}
});
module.exports = router;

165
backend/src/routes/users.js Normal file
View File

@@ -0,0 +1,165 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const pool = require('../database/connection');
const { authenticateToken, requireRole } = require('../middleware/auth');
const router = express.Router();
// Get all users (admin only)
router.get('/', authenticateToken, requireRole(['admin']), async (req, res) => {
try {
const result = await pool.query(
'SELECT id, email, first_name, last_name, role, is_active, created_at FROM users ORDER BY created_at DESC'
);
res.json(result.rows);
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({ error: 'Failed to fetch users' });
}
});
// Get user by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
// Users can only view their own profile unless they're admin
if (req.user.id !== id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
const result = await pool.query(
'SELECT id, email, first_name, last_name, role, is_active, created_at FROM users WHERE id = $1',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(result.rows[0]);
} catch (error) {
console.error('Get user error:', error);
res.status(500).json({ error: 'Failed to fetch user' });
}
});
// Update user profile
router.put('/:id', authenticateToken, [
body('firstName').optional().notEmpty().trim(),
body('lastName').optional().notEmpty().trim(),
body('email').optional().isEmail().normalizeEmail()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const { firstName, lastName, email } = req.body;
// Users can only update their own profile unless they're admin
if (req.user.id !== id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
// Check if email is already taken by another user
if (email) {
const existingUser = await pool.query(
'SELECT id FROM users WHERE email = $1 AND id != $2',
[email, id]
);
if (existingUser.rows.length > 0) {
return res.status(400).json({ error: 'Email already in use' });
}
}
const updateFields = [];
const updateValues = [];
let paramCount = 1;
if (firstName) {
updateFields.push(`first_name = $${paramCount}`);
updateValues.push(firstName);
paramCount++;
}
if (lastName) {
updateFields.push(`last_name = $${paramCount}`);
updateValues.push(lastName);
paramCount++;
}
if (email) {
updateFields.push(`email = $${paramCount}`);
updateValues.push(email);
paramCount++;
}
if (updateFields.length === 0) {
return res.status(400).json({ error: 'No fields to update' });
}
updateValues.push(id);
const query = `UPDATE users SET ${updateFields.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = $${paramCount} RETURNING id, email, first_name, last_name, role, is_active, updated_at`;
const result = await pool.query(query, updateValues);
res.json({
message: 'User updated successfully',
user: result.rows[0]
});
} catch (error) {
console.error('Update user error:', error);
res.status(500).json({ error: 'Failed to update user' });
}
});
// Deactivate user (admin only)
router.put('/:id/deactivate', authenticateToken, requireRole(['admin']), async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(
'UPDATE users SET is_active = false, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING id, email, first_name, last_name, role, is_active',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
message: 'User deactivated successfully',
user: result.rows[0]
});
} catch (error) {
console.error('Deactivate user error:', error);
res.status(500).json({ error: 'Failed to deactivate user' });
}
});
// Activate user (admin only)
router.put('/:id/activate', authenticateToken, requireRole(['admin']), async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(
'UPDATE users SET is_active = true, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING id, email, first_name, last_name, role, is_active',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
message: 'User activated successfully',
user: result.rows[0]
});
} catch (error) {
console.error('Activate user error:', error);
res.status(500).json({ error: 'Failed to activate user' });
}
});
module.exports = router;

57
backend/src/server.js Normal file
View 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;

View 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');
});
});
});

View 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');
});
});
});