diff --git a/backend/server.js b/backend/server.js index 990b930..2c462c1 100755 --- a/backend/server.js +++ b/backend/server.js @@ -109,9 +109,10 @@ app.use((req, res, next) => { }); // =============== USER REGISTRATION (MySQL) =============== +// /api/register app.post('/api/register', async (req, res) => { const { - userId, + userId, // random ID from the front end username, password, firstname, @@ -123,62 +124,54 @@ app.post('/api/register', async (req, res) => { career_situation } = req.body; - if ( - !userId || - !username || - !password || - !firstname || - !lastname || - !email || - !zipcode || - !state || - !area || - !career_situation - ) { - return res.status(400).json({ error: 'All fields including career_situation are required' }); - } - try { const hashedPassword = await bcrypt.hash(password, 10); - // 1) Insert into user_profile first - const profileQuery = ` - INSERT INTO user_profile (user_id, firstname, lastname, email, zipcode, state, area, career_situation) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + // Insert row in user_profile, storing both user_id (random) and letting id auto-increment + const profileQuery = ` + INSERT INTO user_profile + (user_id, firstname, lastname, email, zipcode, state, area, career_situation) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `; + pool.query( + profileQuery, + [userId, firstname, lastname, email, zipcode, state, area, career_situation], + (errProfile, resultProfile) => { + if (errProfile) { + console.error('Error inserting user_profile:', errProfile.message); + return res.status(500).json({ error: 'Failed to create user profile' }); + } + + const newProfileAutoId = resultProfile.insertId; // auto-increment PK + + // Insert into user_auth, referencing the auto-increment PK + const authQuery = ` + INSERT INTO user_auth (user_id, username, hashed_password) + VALUES (?, ?, ?) `; - pool.query( - profileQuery, - [userId, firstname, lastname, email, zipcode, state, area, career_situation], - (err2, results2) => { - if (err2) { - console.error('Error inserting into user_profile:', err2.message); - return res.status(500).json({ error: 'Failed to create user profile' }); + pool.query(authQuery, [newProfileAutoId, username, hashedPassword], (errAuth) => { + if (errAuth) { + console.error('Error inserting user_auth:', errAuth.message); + if (errAuth.code === 'ER_DUP_ENTRY') { + return res.status(400).json({ error: 'Username already exists' }); } - - // 2) Insert into user_auth - const authQuery = ` - INSERT INTO user_auth (user_id, username, hashed_password) - VALUES (?, ?, ?) - `; - pool.query(authQuery, [userId, username, hashedPassword], (err, results) => { - if (err) { - console.error('Error inserting into user_auth:', err.message); - if (err.code === 'ER_DUP_ENTRY') { - return res.status(400).json({ error: 'Username already exists' }); - } - return res.status(500).json({ error: 'Failed to register user' }); - } - return res.status(201).json({ message: 'User registered successfully', userId }); - }); + return res.status(500).json({ error: 'Failed to register user' }); } - ); - - } catch (error) { - console.error('Error during registration:', error.message); + return res.status(201).json({ + message: 'User registered successfully', + dbId: newProfileAutoId, // the auto-increment PK + customId: userId, // the random ID + }); + }); + } + ); + } catch (err) { + console.error('Error during registration:', err.message); return res.status(500).json({ error: 'Internal server error' }); } }); + // =============== SIGN-IN (MySQL) =============== app.post('/api/signin', async (req, res) => { const { username, password } = req.body; @@ -190,6 +183,7 @@ app.post('/api/signin', async (req, res) => { SELECT user_auth.user_id, user_auth.hashed_password, + user_profile.id, user_profile.zipcode, user_profile.is_premium, user_profile.is_pro_premium, @@ -200,7 +194,7 @@ app.post('/api/signin', async (req, res) => { user_profile.career_priorities, user_profile.career_list FROM user_auth - LEFT JOIN user_profile ON user_auth.user_id = user_profile.user_id + LEFT JOIN user_profile ON user_auth.user_id = user_profile.id WHERE user_auth.username = ? `; pool.query(query, [username], async (err, results) => { @@ -219,11 +213,11 @@ app.post('/api/signin', async (req, res) => { return res.status(401).json({ error: 'Invalid username or password' }); } - const token = jwt.sign({ userId: row.user_id }, SECRET_KEY, { expiresIn: '2h' }); + const token = jwt.sign({ userId: row.id }, SECRET_KEY, { expiresIn: '2h' }); res.status(200).json({ message: 'Login successful', token, - userId: row.user_id, + userId: row.id, user: { user_id: row.user_id, firstname: row.firstname, diff --git a/backend/server2.js b/backend/server2.js index ff431a6..4db43c5 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -1,16 +1,20 @@ +/************************************************** + * server2.js - MySQL version + **************************************************/ import express from 'express'; import axios from 'axios'; import cors from 'cors'; -import helmet from 'helmet'; // Import helmet for HTTP security headers +import helmet from 'helmet'; // For HTTP security headers import dotenv from 'dotenv'; -import xlsx from 'xlsx'; // Keep for CIP->SOC mapping only +import xlsx from 'xlsx'; // For CIP->SOC mapping only import path from 'path'; import { fileURLToPath } from 'url'; -import { open } from 'sqlite'; -import sqlite3 from 'sqlite3'; import fs from 'fs'; import readline from 'readline'; +// ********** NEW: use mysql2/promise for async/await queries ********** +import mysql from 'mysql2/promise'; + // --- Basic file init --- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -33,41 +37,31 @@ const mappingFilePath = '/home/jcoakley/aptiva-dev1-app/public/CIP_to_ONET_SOC.x // Institution data const institutionFilePath = path.resolve(rootPath, 'public/Institution_data.json'); -// Create Express app +// ********** CREATE TWO POOLS FOR TWO DATABASES ********** +// salary_data_db => poolSalary +// user_profile_db => poolProfile + +const poolSalary = mysql.createPool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: 'salary_data_db', + connectionLimit: 10 +}); + +const poolProfile = mysql.createPool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: 'user_profile_db', + connectionLimit: 10 +}); + const app = express(); const PORT = process.env.PORT || 5001; -/************************************************** - * DB connections - **************************************************/ -let db; -const initDB = async () => { - try { - db = await open({ - filename: '/home/jcoakley/aptiva-dev1-app/salary_info.db', - driver: sqlite3.Database, - }); - console.log('Connected to SQLite database.'); - } catch (error) { - console.error('Error connecting to database:', error); - } -}; -initDB(); - -let userProfileDb; -const initUserProfileDb = async () => { - try { - userProfileDb = await open({ - filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db', - driver: sqlite3.Database - }); - console.log('Connected to user_profile.db.'); - } catch (error) { - console.error('Error connecting to user_profile.db:', error); - } -}; -initUserProfileDb(); - /************************************************** * Security, CORS, JSON Body **************************************************/ @@ -141,13 +135,7 @@ if (socToCipMapping.length === 0) { /************************************************** * Load single JSON with all states + US - * Replaces old GA-only approach **************************************************/ -// const projectionsFilePath = path.resolve(__dirname, '..', 'public', 'occprj.xlsx'); -// const workbook = xlsx.readFile(projectionsFilePath); -// const sheet = workbook.Sheets['GAOccProj 2022-2032']; -// const projectionsData = xlsx.utils.sheet_to_json(sheet, { header: 1 }); - const singleProjFile = path.resolve(__dirname, '..', 'public', 'economicproj.json'); let allProjections = []; try { @@ -205,7 +193,7 @@ app.get('/api/onet/questions', async (req, res) => { } }); -// geocode +// Geocode async function geocodeZipCode(zipCode) { const apiKey = process.env.GOOGLE_MAPS_API_KEY; if (!apiKey) { @@ -394,7 +382,7 @@ app.get('/api/schools', (req, res) => { // 1) Read `cipCodes` from query (comma-separated string) const { cipCodes } = req.query; - if (!cipCodes ) { + if (!cipCodes) { return res.status(400).json({ error: 'cipCodes (comma-separated) and state are required.' }); } @@ -422,8 +410,7 @@ app.get('/api/schools', (req, res) => { return cipArray.some((cip) => scip.startsWith(cip)); }); - // 5) (Optional) Deduplicate if you suspect overlaps among CIP codes. - // E.g. by a “UNITID” or unique property: + // 5) (Optional) Deduplicate if you suspect overlaps among CIP codes. const uniqueMap = new Map(); for (const school of filtered) { const key = school.UNITID || school.INSTNM; // pick your unique field @@ -441,7 +428,6 @@ app.get('/api/schools', (req, res) => { } }); - // tuition app.get('/api/tuition', (req, res) => { const { cipCodes, state } = req.query; @@ -489,11 +475,9 @@ app.get('/api/tuition', (req, res) => { } }); - /************************************************** * SINGLE route for projections from economicproj.json **************************************************/ -// Remove old GA Excel approach; unify with single JSON approach app.get('/api/projections/:socCode', (req, res) => { const { socCode } = req.params; const { state } = req.query; @@ -548,7 +532,7 @@ app.get('/api/projections/:socCode', (req, res) => { }); /************************************************** - * Salary route + * Salary route (uses poolSalary) **************************************************/ app.get('/api/salary', async (req, res) => { const { socCode, area } = req.query; @@ -576,10 +560,17 @@ app.get('/api/salary', async (req, res) => { try { let regionalRow = null; let nationalRow = null; + + // If area is provided, fetch regional if (area) { - regionalRow = await db.get(regionalQuery, [socCode, area]); + const [regRows] = await poolSalary.query(regionalQuery, [socCode, area]); + regionalRow = regRows.length ? regRows[0] : null; } - nationalRow = await db.get(nationalQuery, [socCode]); + + // Always fetch national + const [natRows] = await poolSalary.query(nationalQuery, [socCode]); + nationalRow = natRows.length ? natRows[0] : null; + if (!regionalRow && !nationalRow) { console.log('No salary data found for:', { socCode, area }); return res.status(404).json({ error: 'No salary data found' }); @@ -597,12 +588,12 @@ app.get('/api/salary', async (req, res) => { }); /************************************************** - * job-zones route + * job-zones route (uses poolSalary) **************************************************/ app.post('/api/job-zones', async (req, res) => { const { socCodes } = req.body; if (!socCodes || !Array.isArray(socCodes) || socCodes.length === 0) { - return res.status(400).json({ error: "SOC Codes are required." }); + return res.status(400).json({ error: 'SOC Codes are required.' }); } try { // Format them @@ -614,28 +605,34 @@ app.post('/api/job-zones', async (req, res) => { return cleaned.slice(0, 7); }); const placeholders = formattedSocCodes.map(() => '?').join(','); + const q = ` SELECT OCC_CODE, JOB_ZONE, A_MEDIAN, A_PCT10, A_PCT25, A_PCT75 FROM salary_data WHERE OCC_CODE IN (${placeholders}) `; - const rows = await db.all(q, formattedSocCodes); - console.log("Salary Data Query Results:", rows); + + // Use spread operator for the array + const [rows] = await poolSalary.query(q, [...formattedSocCodes]); + console.log('Salary Data Query Results:', rows); + const jobZoneMapping = rows.reduce((acc, row) => { - const isMissing = [row.A_MEDIAN, row.A_PCT10, row.A_PCT25, row.A_PCT75] - .some((v) => !v || v === '#' || v === '*'); + const isMissing = [row.A_MEDIAN, row.A_PCT10, row.A_PCT25, row.A_PCT75].some( + (v) => !v || v === '#' || v === '*' + ); acc[row.OCC_CODE] = { job_zone: row.JOB_ZONE, limited_data: isMissing ? 1 : 0 }; return acc; }, {}); - console.log("Job Zone & Limited Data:", jobZoneMapping); + + console.log('Job Zone & Limited Data:', jobZoneMapping); res.json(jobZoneMapping); } catch (error) { - console.error("Error fetching job zones:", error); - res.status(500).json({ error: "Failed to fetch job zones." }); + console.error('Error fetching job zones:', error); + res.status(500).json({ error: 'Failed to fetch job zones.' }); } }); @@ -665,26 +662,10 @@ app.get('/api/skills/:socCode', async (req, res) => { const data = response.data || {}; - // 3) O*NET returns: - // { - // "code": "17-1011.00", - // "group": [ - // { - // "title": { "id": "2.A", "name": "Basic Skills" }, - // "element": [ - // { "id": "2.A.1.a", "name": "reading work related information" }, - // ... - // ] - // }, - // ... - // ] - // } - - // Instead of data.characteristic, parse data.group const groups = data.group || []; - - // 4) Flatten out the group[].element[] into a single skills array const skills = []; + + // Flatten out the group[].element[] into a single skills array groups.forEach((groupItem) => { const groupName = groupItem?.title?.name || 'Unknown Group'; @@ -702,43 +683,42 @@ app.get('/api/skills/:socCode', async (req, res) => { res.json({ skills }); } catch (error) { - console.error('Error fetching O*NET skills:', error.message); + console.error('Error fetching O*NET skills:', error.message); - if (error.response) { - console.error('O*NET error status:', error.response.status); - console.error('O*NET error data:', error.response.data); - } else if (error.request) { - // The request was made but no response was received - console.error('No response received from O*NET. Possibly a network or credentials error.'); - console.error('Axios error.request:', error.request); - } else { - // Something else happened in setting up the request - console.error('Request setup error:', error.message); + if (error.response) { + console.error('O*NET error status:', error.response.status); + console.error('O*NET error data:', error.response.data); + } else if (error.request) { + console.error('No response received from O*NET.'); + console.error('Axios error.request:', error.request); + } else { + console.error('Request setup error:', error.message); + } + + return res.status(500).json({ error: 'Failed to fetch O*NET skills' }); } - - return res.status(500).json({ error: 'Failed to fetch O*NET skills' }); -} - }); - /************************************************** - * user-profile by ID route + * user-profile by ID route (uses poolProfile) **************************************************/ -app.get('/api/user-profile/:id', (req, res) => { +app.get('/api/user-profile/:id', async (req, res) => { const { id } = req.params; if (!id) return res.status(400).json({ error: 'Profile ID is required' }); + const query = `SELECT area, zipcode 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) { + + try { + const [rows] = await poolProfile.query(query, [id]); + if (!rows.length) { return res.status(404).json({ error: 'Profile not found' }); } + const row = rows[0]; res.json({ area: row.area, zipcode: row.zipcode }); - }); + } catch (err) { + console.error('Error fetching user profile:', err.message); + return res.status(500).json({ error: 'Failed to fetch user profile' }); + } }); /************************************************** diff --git a/src/components/SignUp.js b/src/components/SignUp.js index 50ad145..1801a07 100644 --- a/src/components/SignUp.js +++ b/src/components/SignUp.js @@ -48,6 +48,7 @@ function SignUp() { const [areas, setAreas] = useState([]); const [error, setError] = useState(''); const [loadingAreas, setLoadingAreas] = useState(false); + const [frontendUserId] = useState(() => Math.floor(Math.random() * 1000000000)); // new states const [showCareerSituations, setShowCareerSituations] = useState(false); @@ -168,23 +169,30 @@ function SignUp() { const handleSituationConfirm = async () => { + try { - // Verify payload clearly: console.log("Payload sent to backend:", { - userId: Math.floor(Math.random() * 1000000000), + userId: frontendUserId, username, password, firstname, lastname, email, zipcode, state, area, career_situation: selectedSituation.id }); const response = await fetch('/api/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - userId: Math.floor(Math.random() * 1000000000), - username, password, firstname, lastname, email, zipcode, state, area, - career_situation: selectedSituation.id - }), - }); + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: frontendUserId, + username, + password, + firstname, + lastname, + email, + zipcode, + state, + area, + career_situation: selectedSituation.id + }), +}); const data = await response.json();