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(); }); app.use('/CIP_institution_mapping_fixed.json', (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); res.sendFile(path.join(__dirname, 'CIP_institution_mapping_fixed.json')); }); // 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}`); });