520 lines
18 KiB
JavaScript
Executable File
520 lines
18 KiB
JavaScript
Executable File
import express from 'express';
|
|
import axios from 'axios';
|
|
import cors from 'cors';
|
|
import helmet from 'helmet'; // Import helmet for HTTP security headers
|
|
import dotenv from 'dotenv';
|
|
import xlsx from 'xlsx'; // Import xlsx to read the Excel file
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url'; // Import fileURLToPath to handle the current file's URL
|
|
import sqlite3 from 'sqlite3';
|
|
import { open } from 'sqlite';
|
|
|
|
|
|
dotenv.config({ path: '/home/jcoakley/backend/.env' }); // Load environment variables
|
|
sqlite3.verbose();
|
|
|
|
// Get the current directory path (workaround for __dirname in ES modules)
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'https://dev.aptivaai.com'];
|
|
const mappingFilePath = '/home/jcoakley/public/CIP_to_ONET_SOC.xlsx'
|
|
const app = express();
|
|
const PORT = process.env.PORT || 5001;
|
|
|
|
// Initialize database connection
|
|
let db;
|
|
const initDB = async () => {
|
|
try {
|
|
db = await open({
|
|
filename: '/home/jcoakley/salary_info.db', // Correct file path
|
|
driver: sqlite3.Database,
|
|
});
|
|
console.log('Connected to SQLite database.');
|
|
} catch (error) {
|
|
console.error('Error connecting to database:', error);
|
|
}
|
|
};
|
|
|
|
// Initialize database before starting the server
|
|
initDB();
|
|
|
|
// Add security headers using helmet
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: false, // Disable CSP for now; enable as needed later
|
|
crossOriginEmbedderPolicy: false,
|
|
})
|
|
);
|
|
|
|
// Updated CORS Middleware for dynamic handling of origins and static files
|
|
app.use((req, res, next) => {
|
|
const origin = req.headers.origin;
|
|
const allowedOrigins = [
|
|
'http://localhost:3000',
|
|
'http://34.16.120.118:3000',
|
|
'https://dev.aptivaai.com'
|
|
];
|
|
|
|
if (origin && allowedOrigins.includes(origin)) {
|
|
// Allow requests from whitelisted frontend origins
|
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
res.setHeader(
|
|
'Access-Control-Allow-Headers',
|
|
'Authorization, Content-Type, Accept, Origin, X-Requested-With, Access-Control-Allow-Methods'
|
|
);
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
} else if (req.path.includes('CIP_institution_mapping_fixed.json')) {
|
|
// Handle static JSON file CORS
|
|
res.setHeader('Access-Control-Allow-Origin', '*'); // Allow all origins for static file
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
res.setHeader(
|
|
'Access-Control-Allow-Headers',
|
|
'Content-Type, Accept, Origin, X-Requested-With'
|
|
);
|
|
} else {
|
|
// Default headers for other unhandled cases
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
res.setHeader(
|
|
'Access-Control-Allow-Headers',
|
|
'Authorization, Content-Type, Accept, Origin, X-Requested-With'
|
|
);
|
|
}
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
// Handle preflight requests
|
|
return res.status(200).end();
|
|
}
|
|
|
|
next();
|
|
});
|
|
|
|
|
|
|
|
// Middleware to parse JSON bodies
|
|
app.use(express.json());
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
app.use((req, res, next) => {
|
|
console.log(`Path: ${req.path}, Method: ${req.method}, Body:`, req.body);
|
|
next();
|
|
});
|
|
|
|
|
|
// Route to fetch O*Net Interest Inventory questions with pagination
|
|
app.get('/api/onet/questions', async (req, res) => {
|
|
const { start, end } = req.query;
|
|
|
|
if (!start || !end) {
|
|
return res.status(400).json({ error: 'Start and end parameters are required' });
|
|
}
|
|
|
|
try {
|
|
const questions = [];
|
|
let currentStart = parseInt(start, 10);
|
|
let currentEnd = parseInt(end, 10);
|
|
|
|
while (currentStart <= currentEnd) {
|
|
// Fetch questions from O*Net API for the current range
|
|
const response = await axios.get(
|
|
`https://services.onetcenter.org/ws/mnm/interestprofiler/questions?start=${currentStart}&end=${Math.min(currentEnd, currentStart + 11)}`,
|
|
{
|
|
auth: {
|
|
username: process.env.ONET_USERNAME,
|
|
password: process.env.ONET_PASSWORD,
|
|
},
|
|
}
|
|
);
|
|
|
|
// Add questions to the result set
|
|
if (response.data.question && Array.isArray(response.data.question)) {
|
|
questions.push(...response.data.question);
|
|
}
|
|
|
|
// Check if there's a next page
|
|
const nextLink = response.data.link?.find((link) => link.rel === 'next');
|
|
if (nextLink) {
|
|
// Update start and end based on the "next" link
|
|
const nextParams = new URLSearchParams(nextLink.href.split('?')[1]);
|
|
currentStart = parseInt(nextParams.get('start'), 10);
|
|
currentEnd = parseInt(nextParams.get('end'), 10);
|
|
} else {
|
|
break; // Stop if there are no more pages
|
|
}
|
|
}
|
|
|
|
res.status(200).json({ questions });
|
|
} catch (error) {
|
|
console.error('Error fetching O*Net questions:', error.message);
|
|
res.status(500).json({ error: 'Failed to fetch O*Net questions' });
|
|
}
|
|
});
|
|
|
|
// Route to fetch O*Net Careers list
|
|
app.get('/api/onet/questions', async (req, res) => {
|
|
const { start, end } = req.query;
|
|
|
|
if (!start || !end) {
|
|
return res.status(400).json({ error: 'Start and end parameters are required' });
|
|
}
|
|
|
|
try {
|
|
const questions = [];
|
|
let currentStart = parseInt(start, 10);
|
|
let currentEnd = parseInt(end, 10);
|
|
|
|
while (currentStart <= currentEnd) {
|
|
// Fetch questions from O*Net API for the current range
|
|
const response = await axios.get(
|
|
`https://services.onetcenter.org/ws/mnm/interestprofiler/questions?start=${currentStart}&end=${Math.min(currentEnd, currentStart + 11)}`,
|
|
{
|
|
auth: {
|
|
username: process.env.ONET_USERNAME,
|
|
password: process.env.ONET_PASSWORD,
|
|
},
|
|
}
|
|
);
|
|
|
|
// Add questions to the result set
|
|
if (response.data.question && Array.isArray(response.data.question)) {
|
|
questions.push(...response.data.question);
|
|
}
|
|
|
|
// Check if there's a next page
|
|
const nextLink = response.data.link?.find((link) => link.rel === 'next');
|
|
if (nextLink) {
|
|
// Update start and end based on the "next" link
|
|
const nextParams = new URLSearchParams(nextLink.href.split('?')[1]);
|
|
currentStart = parseInt(nextParams.get('start'), 10);
|
|
currentEnd = parseInt(nextParams.get('end'), 10);
|
|
} else {
|
|
break; // Stop if there are no more pages
|
|
}
|
|
}
|
|
|
|
res.status(200).json({ questions });
|
|
} catch (error) {
|
|
console.error('Error fetching O*Net questions:', error.message);
|
|
res.status(500).json({ error: 'Failed to fetch O*Net questions' });
|
|
}
|
|
});
|
|
|
|
|
|
// New route to handle Google Maps geocoding
|
|
app.get('/api/maps/distance', async (req, res) => {
|
|
const { origins, destinations } = req.query;
|
|
console.log('Query parameters received:', req.query); // Log the entire query object
|
|
|
|
if (!origins || !destinations) {
|
|
console.error('Missing parameters:', { origins, destinations });
|
|
return res
|
|
.status(400)
|
|
.json({ error: 'Origin and destination parameters are required.' });
|
|
}
|
|
|
|
const apiKey = process.env.GOOGLE_MAPS_API_KEY; // Use the Google Maps API key from the environment variable
|
|
const distanceUrl = `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent(
|
|
origins
|
|
)}&destinations=${encodeURIComponent(destinations)}&units=imperial&key=${apiKey}`;
|
|
console.log('Constructed Distance Matrix API URL:', distanceUrl);
|
|
|
|
|
|
try {
|
|
const response = await axios.get(distanceUrl);
|
|
res.status(200).json(response.data);
|
|
} catch (error) {
|
|
console.error('Error fetching distance data:', error.message);
|
|
res.status(500).json({ error: 'Failed to fetch distance data', details: error.message });
|
|
}
|
|
});
|
|
|
|
|
|
// Load the economic projections data from the Excel file
|
|
const projectionsFilePath = path.resolve(__dirname, '..', 'public', 'occprj.xlsx'); // Adjusted path
|
|
const workbook = xlsx.readFile(projectionsFilePath);
|
|
const sheet = workbook.Sheets['GAOccProj 2022-2032']; // The sheet with your data
|
|
const projectionsData = xlsx.utils.sheet_to_json(sheet, { header: 1 }); // Convert the sheet to JSON
|
|
|
|
const loadMapping = () => {
|
|
try {
|
|
const workbook = xlsx.readFile(mappingFilePath); // Read the Excel file
|
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]; // Assuming the first sheet
|
|
const data = xlsx.utils.sheet_to_json(sheet); // Convert the sheet to JSON
|
|
return data; // Return the data from the Excel sheet
|
|
} catch (error) {
|
|
console.error('Error reading the CIP to ONET SOC mapping file:', error);
|
|
return []; // Return an empty array if there's an issue loading the mapping
|
|
}
|
|
};
|
|
|
|
// Load the mapping data from the Excel file
|
|
const socToCipMapping = loadMapping();
|
|
if (socToCipMapping.length === 0) {
|
|
console.error("SOC to CIP mapping data is empty.");
|
|
}
|
|
|
|
// Route to handle submission of answers to O*Net API for career suggestions and RIASEC scores
|
|
app.post('/api/onet/submit_answers', async (req, res) => {
|
|
console.log('POST /api/onet/submit_answers hit');
|
|
const { answers } = req.body; // Get answers string from the request body
|
|
|
|
if (!answers || answers.length !== 60) {
|
|
console.error('Invalid answers provided:', answers);
|
|
return res.status(400).json({ error: 'Answers parameter must be a 60-character string.' });
|
|
}
|
|
|
|
try {
|
|
// URLs for career suggestions and RIASEC scores
|
|
const careerUrl = `https://services.onetcenter.org/ws/mnm/interestprofiler/careers?answers=${answers}`;
|
|
const resultsUrl = `https://services.onetcenter.org/ws/mnm/interestprofiler/results?answers=${answers}`;
|
|
|
|
// Fetch career suggestions
|
|
console.log('Fetching career suggestions from:', careerUrl);
|
|
const careerResponse = await axios.get(careerUrl, {
|
|
auth: {
|
|
username: process.env.ONET_USERNAME,
|
|
password: process.env.ONET_PASSWORD
|
|
},
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
// Fetch RIASEC scores
|
|
console.log('Fetching RIASEC scores from:', resultsUrl);
|
|
const resultsResponse = await axios.get(resultsUrl, {
|
|
auth: {
|
|
username: process.env.ONET_USERNAME,
|
|
password: process.env.ONET_PASSWORD
|
|
},
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
// Extract career suggestions and RIASEC scores
|
|
const careerSuggestions = careerResponse.data.career || []; // Correctly initialize the array
|
|
const riaSecScores = resultsResponse.data.result || [];
|
|
|
|
console.log('Raw Career Suggestions:', careerSuggestions);
|
|
console.log('RIASEC Scores:', riaSecScores);
|
|
|
|
// Filter out careers that do not require higher education and ensure `fit` is preserved
|
|
const filterHigherEducationCareers = (careers) => {
|
|
return careers.map((career) => {
|
|
// Add a check for the required education field
|
|
const educationLevel = career.education; // Adjust this field based on actual API response
|
|
|
|
// Return careers only if they meet criteria
|
|
if (
|
|
!["No formal education", "High school", "Some college, no degree"].includes(educationLevel)
|
|
) {
|
|
return {
|
|
href: career.href,
|
|
fit: career.fit, // Ensure the fit field is included
|
|
code: career.code,
|
|
title: career.title,
|
|
tags: career.tags,
|
|
};
|
|
}
|
|
}).filter((career) => career); // Remove undefined values
|
|
};
|
|
|
|
// Apply the filter
|
|
const filteredCareers = filterHigherEducationCareers(careerSuggestions);
|
|
|
|
console.log('Final Response to Frontend:', {
|
|
careers: filteredCareers,
|
|
riaSecScores: riaSecScores,
|
|
});
|
|
// Send the combined data to the frontend
|
|
res.status(200).json({
|
|
careers: filteredCareers,
|
|
riaSecScores: riaSecScores
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching data from O*Net API:', error.response?.data || error.message);
|
|
res.status(500).json({
|
|
error: 'Failed to fetch data from O*Net API',
|
|
details: error.response?.data || error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get('/api/onet/career-details/:socCode', async (req, res) => {
|
|
const { socCode } = req.params;
|
|
|
|
if (!socCode) {
|
|
return res.status(400).json({ error: 'SOC Code is required.' });
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(
|
|
`https://services.onetcenter.org/ws/mnm/careers/${socCode}`,
|
|
{
|
|
auth: {
|
|
username: process.env.ONET_USERNAME,
|
|
password: process.env.ONET_PASSWORD,
|
|
},
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
res.status(200).json(response.data); // Forward the API response to the frontend
|
|
} catch (error) {
|
|
console.error('Error fetching career details:', error.message);
|
|
res.status(500).json({ error: 'Failed to fetch career details from O*NET API.' });
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// Route to handle fetching CIP code based on SOC code
|
|
app.get('/api/cip/:socCode', (req, res) => {
|
|
const { socCode } = req.params;
|
|
console.log(`Received SOC Code: ${socCode.trim()}`);
|
|
|
|
for (let row of socToCipMapping) {
|
|
const mappedSOC = row['O*NET-SOC 2019 Code']?.trim(); // Trim spaces
|
|
if (mappedSOC === socCode.trim()) {
|
|
console.log('Found matching CIP Code:', row['2020 CIP Code']);
|
|
return res.json({ cipCode: row['2020 CIP Code'] });
|
|
}
|
|
}
|
|
|
|
console.error('SOC code not found in mapping:', socCode);
|
|
res.status(404).json({ error: 'CIP code not found for this SOC code' });
|
|
});
|
|
|
|
app.get('/CIP_institution_mapping_fixed.json', (req, res) => {
|
|
const filePath = path.join(__dirname, 'CIP_institution_mapping_fixed.json'); // Adjust the path if needed
|
|
res.sendFile(filePath);
|
|
});
|
|
|
|
|
|
|
|
// Route to handle fetching tuition data using CIP code from College Scorecard API
|
|
app.get('/api/tuition/:cipCode', async (req, res) => {
|
|
const { cipCode } = req.params;
|
|
const { state } = req.query;
|
|
|
|
console.log(`Received CIP Code: ${cipCode} for state: ${state}`);
|
|
|
|
try {
|
|
const apiKey = process.env.COLLEGE_SCORECARD_KEY;
|
|
const url = `https://api.data.gov/ed/collegescorecard/v1/schools?api_key=${apiKey}&programs.cip_4_digit.code=${cipCode}&school.state=${state}&fields=id,school.name,latest.cost.tuition.in_state,latest.cost.tuition.out_of_state`;
|
|
|
|
console.log(`Constructed URL: ${url}`);
|
|
|
|
const response = await axios.get(url);
|
|
const data = response.data.results;
|
|
|
|
console.log('College Scorecard API Response:', data); // Log the API response
|
|
|
|
if (data?.length > 0) {
|
|
res.status(200).json(data);
|
|
} else {
|
|
res.status(404).json({ error: 'No tuition data found for the given CIP code and state.' });
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
error: 'Failed to fetch tuition data from College Scorecard API',
|
|
details: error.response?.data || error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Route to handle fetching economic projections for SOC code
|
|
app.get('/api/projections/:socCode', (req, res) => {
|
|
const { socCode } = req.params;
|
|
console.log('Received SOC Code:', socCode);
|
|
|
|
const socRow = projectionsData.find(row => row[0] === socCode);
|
|
|
|
if (socRow) {
|
|
const projections = {
|
|
"SOC Code": socRow[0],
|
|
"Occupation": socRow[2],
|
|
"2022 Employment": socRow[3],
|
|
"2032 Employment": socRow[4],
|
|
"Total Change": socRow[5],
|
|
"Annual Openings": socRow[7],
|
|
"Labor Force Exits": socRow[8],
|
|
"Projected Growth": socRow[9]
|
|
};
|
|
|
|
res.status(200).json(projections);
|
|
} else {
|
|
res.status(404).json({ error: 'SOC Code not found' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/salary', async (req, res) => {
|
|
const { socCode, area } = req.query;
|
|
|
|
console.log('Received /api/salary request:', { socCode, area });
|
|
|
|
if (!socCode || !area) {
|
|
console.error('Missing required parameters:', { socCode, area });
|
|
return res.status(400).json({ error: 'SOC Code and Area are required' });
|
|
}
|
|
|
|
const query = `
|
|
SELECT A_PCT10, A_PCT25, A_MEDIAN, A_PCT75, A_PCT90
|
|
FROM salary_data
|
|
WHERE OCC_CODE = ? AND AREA_TITLE = ?
|
|
`;
|
|
|
|
try {
|
|
console.log('Executing query:', query, 'with params:', [socCode, area]);
|
|
|
|
// Use async/await for better error handling
|
|
const row = await db.get(query, [socCode, area]);
|
|
|
|
if (!row) {
|
|
console.log('No salary data found for:', { socCode, area });
|
|
return res.status(404).json({ error: 'No salary data found for the given SOC Code and Area' });
|
|
}
|
|
|
|
console.log('Salary data retrieved:', row);
|
|
res.json(row);
|
|
} catch (error) {
|
|
console.error('Error executing query:', error.message);
|
|
res.status(500).json({ error: 'Failed to fetch salary data' });
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// Route to fetch user profile by ID
|
|
app.get('/api/user-profile/:id', (req, res) => {
|
|
const { id } = req.params;
|
|
|
|
if (!id) {
|
|
return res.status(400).json({ error: 'Profile ID is required' });
|
|
}
|
|
|
|
const query = `SELECT area FROM user_profile WHERE id = ?`;
|
|
db.get(query, [id], (err, row) => {
|
|
if (err) {
|
|
console.error('Error fetching user profile:', err.message);
|
|
return res.status(500).json({ error: 'Failed to fetch user profile' });
|
|
}
|
|
|
|
if (!row) {
|
|
return res.status(404).json({ error: 'Profile not found' });
|
|
}
|
|
|
|
res.json({ area: row.area });
|
|
});
|
|
});
|
|
|
|
|
|
// Start the Express server
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running on https://34.16.120.118:${PORT}`);
|
|
});
|