diff --git a/backend/server.js b/backend/server.js index 99d843c..d9ab4cc 100755 --- a/backend/server.js +++ b/backend/server.js @@ -5,17 +5,13 @@ import helmet from 'helmet'; import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import fs from 'fs'; - import path from 'path'; import bodyParser from 'body-parser'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; // For token-based authentication - import mysql from 'mysql2'; - -import sqlite3 from 'sqlite3'; - +import sqlite3 from 'sqlite3'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -33,7 +29,6 @@ const DB_USER = process.env.DB_USER || 'sqluser'; const DB_PASSWORD = process.env.DB_PASSWORD || ''; const DB_NAME = process.env.DB_NAME || 'user_profile_db'; - // Create a MySQL pool for user_profile data const pool = mysql.createPool({ host: DB_HOST, @@ -41,7 +36,7 @@ const pool = mysql.createPool({ user: DB_USER, password: DB_PASSWORD, database: DB_NAME, - connectionLimit: 10, // optional + connectionLimit: 10, }); // Test a quick query (optional) @@ -55,7 +50,13 @@ pool.query('SELECT 1', (err) => { const app = express(); const PORT = 5000; -const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'https://dev1.aptivaai.com']; + +// Allowed origins for CORS +const allowedOrigins = [ + 'http://localhost:3000', + 'http://34.16.120.118:3000', + 'https://dev1.aptivaai.com', +]; app.disable('x-powered-by'); app.use(bodyParser.json()); @@ -79,7 +80,13 @@ app.use( } }, methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Authorization', 'Content-Type', 'Accept', 'Origin', 'X-Requested-With'], + allowedHeaders: [ + 'Authorization', + 'Content-Type', + 'Accept', + 'Origin', + 'X-Requested-With', + ], credentials: true, }) ); @@ -88,15 +95,21 @@ app.use( app.options('*', (req, res) => { 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'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Authorization, Content-Type, Accept, Origin, X-Requested-With' + ); res.status(200).end(); }); -// Add HTTP headers for security and caching +// Add HTTP headers for security app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); - res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + res.setHeader( + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains' + ); res.setHeader('Content-Security-Policy', "default-src 'self';"); res.removeHeader('X-Powered-By'); next(); @@ -108,11 +121,16 @@ app.use((req, res, next) => { next(); }); -// =============== USER REGISTRATION (MySQL) =============== -// /api/register +/* ------------------------------------------------------------------ + USER REGISTRATION (MySQL) + ------------------------------------------------------------------ */ +/** + * POST /api/register + * Body: + * username, password, firstname, lastname, email, zipcode, state, area, career_situation + */ app.post('/api/register', async (req, res) => { const { - userId, // random ID from the front end username, password, firstname, @@ -121,48 +139,70 @@ app.post('/api/register', async (req, res) => { zipcode, state, area, - career_situation + career_situation, } = req.body; + if ( + !username || + !password || + !firstname || + !lastname || + !email || + !zipcode || + !state || + !area + ) { + return res.status(400).json({ error: 'Missing required fields.' }); + } + try { const hashedPassword = await bcrypt.hash(password, 10); - // Insert row in user_profile, storing both user_id (random) and letting id auto-increment + // 1) Insert into user_profile const profileQuery = ` INSERT INTO user_profile - (user_id, firstname, lastname, email, zipcode, state, area, career_situation) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + (firstname, lastname, email, zipcode, state, area, career_situation) + VALUES (?, ?, ?, ?, ?, ?, ?) `; + pool.query( profileQuery, - [userId, firstname, lastname, email, zipcode, state, area, career_situation], + [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' }); + return res + .status(500) + .json({ error: 'Failed to create user profile' }); } - const newProfileAutoId = resultProfile.insertId; // auto-increment PK + const newProfileId = resultProfile.insertId; // auto-increment PK - // Insert into user_auth, referencing the auto-increment PK + // 2) Insert into user_auth, referencing user_profile.id const authQuery = ` INSERT INTO user_auth (user_id, username, hashed_password) VALUES (?, ?, ?) `; - 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' }); + pool.query( + authQuery, + [newProfileId, 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' }); + } + return res.status(500).json({ error: 'Failed to register user' }); } - return res.status(500).json({ error: 'Failed to register user' }); + + return res.status(201).json({ + message: 'User registered successfully', + profileId: newProfileId, // the user_profile.id + }); } - return res.status(201).json({ - message: 'User registered successfully', - dbId: newProfileAutoId, // the auto-increment PK - customId: userId, // the random ID - }); - }); + ); } ); } catch (err) { @@ -171,36 +211,47 @@ app.post('/api/register', async (req, res) => { } }); - -// =============== SIGN-IN (MySQL) =============== -app.post('/api/signin', async (req, res) => { +/* ------------------------------------------------------------------ + SIGN-IN (MySQL) + ------------------------------------------------------------------ */ +/** + * POST /api/signin + * Body: { username, password } + * Returns JWT signed with user_profile.id + */ +app.post('/api/signin', (req, res) => { const { username, password } = req.body; if (!username || !password) { - return res.status(400).json({ error: 'Both username and password are required' }); + return res + .status(400) + .json({ error: 'Both username and password are required' }); } const query = ` - SELECT - user_auth.user_id, - user_auth.hashed_password, - user_profile.id, + SELECT + user_auth.user_id, + user_auth.hashed_password, + user_profile.firstname, + user_profile.lastname, + user_profile.email, user_profile.zipcode, + user_profile.state, + user_profile.area, user_profile.is_premium, user_profile.is_pro_premium, user_profile.career_situation, - user_profile.email, - user_profile.firstname, - user_profile.lastname, user_profile.career_priorities, user_profile.career_list FROM user_auth - LEFT JOIN user_profile ON user_auth.user_id = user_profile.id + LEFT JOIN user_profile ON user_auth.user_id = user_profile.id WHERE user_auth.username = ? `; pool.query(query, [username], async (err, results) => { if (err) { console.error('Error querying user_auth:', err.message); - return res.status(500).json({ error: 'Failed to query user authentication data' }); + return res + .status(500) + .json({ error: 'Failed to query user authentication data' }); } if (!results || results.length === 0) { @@ -208,22 +259,29 @@ app.post('/api/signin', async (req, res) => { } const row = results[0]; + // Compare password const isMatch = await bcrypt.compare(password, row.hashed_password); if (!isMatch) { return res.status(401).json({ error: 'Invalid username or password' }); } - const token = jwt.sign({ userId: row.id }, SECRET_KEY, { expiresIn: '2h' }); + // The user_profile id is stored in user_auth.user_id + const token = jwt.sign({ id: row.user_id }, SECRET_KEY, { + expiresIn: '2h', + }); + + // Return the user info + token res.status(200).json({ message: 'Login successful', token, - userId: row.id, + userId: row.user_id, // The user_profile.id user: { - user_id: row.user_id, firstname: row.firstname, lastname: row.lastname, email: row.email, zipcode: row.zipcode, + state: row.state, + area: row.area, is_premium: row.is_premium, is_pro_premium: row.is_pro_premium, career_situation: row.career_situation, @@ -234,7 +292,9 @@ app.post('/api/signin', async (req, res) => { }); }); -// =============== CHECK USERNAME (MySQL) =============== +/* ------------------------------------------------------------------ + CHECK USERNAME (MySQL) + ------------------------------------------------------------------ */ app.get('/api/check-username/:username', (req, res) => { const { username } = req.params; const query = `SELECT username FROM user_auth WHERE username = ?`; @@ -251,17 +311,27 @@ app.get('/api/check-username/:username', (req, res) => { }); }); -// =============== UPSERT USER PROFILE (MySQL) =============== +/* ------------------------------------------------------------------ + UPSERT USER PROFILE (MySQL) + ------------------------------------------------------------------ */ +/** + * POST /api/user-profile + * Headers: { Authorization: Bearer } + * Body: { firstName, lastName, email, zipCode, state, area, ... } + * + * If user_profile row exists (id = token.id), update + * else insert + */ app.post('/api/user-profile', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Authorization token is required' }); } - let userId; + let profileId; try { const decoded = jwt.verify(token, SECRET_KEY); - userId = decoded.userId; + profileId = decoded.id; // user_profile.id from sign-in } catch (error) { console.error('JWT verification failed:', error); return res.status(401).json({ error: 'Invalid or expired token' }); @@ -280,110 +350,134 @@ app.post('/api/user-profile', (req, res) => { career_list, } = req.body; - // Check existing profile - pool.query(`SELECT * FROM user_profile WHERE user_id = ?`, [userId], (err, results) => { - if (err) { - console.error('Error checking profile:', err.message); - return res.status(500).json({ error: 'Database error' }); + // Check if profile row exists + pool.query( + `SELECT * FROM user_profile WHERE id = ?`, + [profileId], + (err, results) => { + if (err) { + console.error('Error checking profile:', err.message); + return res.status(500).json({ error: 'Database error' }); + } + const existingRow = results && results.length > 0 ? results[0] : null; + + // If creating a brand-new profile, ensure required fields + if ( + !existingRow && + (!firstName || !lastName || !email || !zipCode || !state || !area) + ) { + return res + .status(400) + .json({ error: 'All fields are required for initial profile creation.' }); + } + + const finalAnswers = + interest_inventory_answers !== undefined + ? interest_inventory_answers + : existingRow?.interest_inventory_answers || null; + + const finalCareerPriorities = + career_priorities !== undefined + ? career_priorities + : existingRow?.career_priorities || null; + + const finalCareerList = + career_list !== undefined + ? career_list + : existingRow?.career_list || null; + + if (existingRow) { + // Update the existing user_profile + const updateQuery = ` + UPDATE user_profile + SET firstname = ?, lastname = ?, email = ?, zipcode = ?, state = ?, area = ?, + career_situation = ?, interest_inventory_answers = ?, career_priorities = ?, + career_list = ? + WHERE id = ? + `; + const params = [ + firstName || existingRow.firstname, + lastName || existingRow.lastname, + email || existingRow.email, + zipCode || existingRow.zipcode, + state || existingRow.state, + area || existingRow.area, + careerSituation || existingRow.career_situation, + finalAnswers, + finalCareerPriorities, + finalCareerList, + profileId, + ]; + + pool.query(updateQuery, params, (err2) => { + if (err2) { + console.error('Update error:', err2.message); + return res + .status(500) + .json({ error: 'Failed to update user profile' }); + } + return res + .status(200) + .json({ message: 'User profile updated successfully' }); + }); + } else { + // Insert a new profile (the user_auth record exists, but the user_profile row is missing) + const insertQuery = ` + INSERT INTO user_profile + (id, firstname, lastname, email, zipcode, state, area, career_situation, + interest_inventory_answers, career_priorities, career_list) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + const params = [ + profileId, // Force the row's primary key to match the existing user ID + firstName, + lastName, + email, + zipCode, + state, + area, + careerSituation || null, + finalAnswers, + finalCareerPriorities, + finalCareerList, + ]; + + pool.query(insertQuery, params, (err3) => { + if (err3) { + console.error('Insert error:', err3.message); + return res + .status(500) + .json({ error: 'Failed to create user profile' }); + } + return res + .status(201) + .json({ message: 'User profile created successfully', id: profileId }); + }); + } } - const existingRow = results && results.length > 0 ? results[0] : null; - - if (!existingRow && (!firstName || !lastName || !email || !zipCode || !state || !area)) { - return res.status(400).json({ error: 'All fields are required for initial profile creation.' }); - } - - const finalAnswers = - interest_inventory_answers !== undefined - ? interest_inventory_answers - : existingRow?.interest_inventory_answers || null; - - const finalCareerPriorities = - career_priorities !== undefined - ? career_priorities - : existingRow?.career_priorities || null; - - const finalCareerList = - career_list !== undefined - ? career_list - : existingRow?.career_list || null; - - if (existingRow) { - // Update - const updateQuery = ` - UPDATE user_profile - SET firstname = ?, lastname = ?, email = ?, zipcode = ?, state = ?, area = ?, career_situation = ?, - interest_inventory_answers = ?, career_priorities = ?, career_list = ? - WHERE user_id = ? - `; - const params = [ - firstName || existingRow.firstname, - lastName || existingRow.lastname, - email || existingRow.email, - zipCode || existingRow.zipcode, - state || existingRow.state, - area || existingRow.area, - careerSituation || existingRow.career_situation, - finalAnswers, - finalCareerPriorities, - finalCareerList, - userId - ]; - pool.query(updateQuery, params, (err2, result2) => { - if (err2) { - console.error('Update error:', err2.message); - return res.status(500).json({ error: 'Failed to update user profile' }); - } - res.status(200).json({ message: 'User profile updated successfully' }); - }); - } else { - // Insert new profile - const insertQuery = ` - INSERT INTO user_profile - (user_id, firstname, lastname, email, zipcode, state, area, career_situation, - interest_inventory_answers, career_priorities, career_list) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `; - const params = [ - userId, - firstName, - lastName, - email, - zipCode, - state, - area, - careerSituation || null, - finalAnswers, - finalCareerPriorities, - finalCareerList, - ]; - pool.query(insertQuery, params, (err3, result3) => { - if (err3) { - console.error('Insert error:', err3.message); - return res.status(500).json({ error: 'Failed to create user profile' }); - } - res.status(201).json({ message: 'User profile created successfully', id: result3.insertId }); - }); - } - }); + ); }); -// =============== FETCH USER PROFILE (MySQL) =============== +/* ------------------------------------------------------------------ + FETCH USER PROFILE (MySQL) + ------------------------------------------------------------------ */ app.get('/api/user-profile', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Authorization token is required' }); } - let userId; + + let profileId; try { const decoded = jwt.verify(token, SECRET_KEY); - userId = decoded.userId; + profileId = decoded.id; // user_profile.id } catch (error) { console.error('Error verifying token:', error.message); return res.status(401).json({ error: 'Invalid or expired token' }); } - const query = 'SELECT * FROM user_profile WHERE user_id = ?'; - pool.query(query, [userId], (err, results) => { + const query = 'SELECT * FROM user_profile WHERE id = ?'; + pool.query(query, [profileId], (err, results) => { if (err) { console.error('Error fetching user profile:', err.message); return res.status(500).json({ error: 'Internal server error' }); @@ -395,20 +489,28 @@ app.get('/api/user-profile', (req, res) => { }); }); -// =============== SALARY_INFO REMAINS IN SQLITE =============== +/* ------------------------------------------------------------------ + SALARY_INFO REMAINS IN SQLITE + ------------------------------------------------------------------ */ app.get('/api/areas', (req, res) => { const { state } = req.query; if (!state) { return res.status(400).json({ error: 'State parameter is required' }); } - const salaryDbPath = path.resolve('/home/jcoakley/aptiva-dev1-app/salary_info.db'); - const salaryDb = new sqlite3.Database(salaryDbPath, sqlite3.OPEN_READONLY, (err) => { - if (err) { - console.error('Error connecting to database:', err.message); - return res.status(500).json({ error: 'Failed to connect to database' }); + const salaryDbPath = path.resolve( + '/home/jcoakley/aptiva-dev1-app/salary_info.db' + ); + const salaryDb = new sqlite3.Database( + salaryDbPath, + sqlite3.OPEN_READONLY, + (err) => { + if (err) { + console.error('Error connecting to database:', err.message); + return res.status(500).json({ error: 'Failed to connect to database' }); + } } - }); + ); const query = `SELECT DISTINCT AREA_TITLE FROM salary_data WHERE PRIM_STATE = ?`; salaryDb.all(query, [state], (err, rows) => { @@ -428,30 +530,29 @@ app.get('/api/areas', (req, res) => { }); /* ------------------------------------------------------------------ -PREMIUM UPGRADE ENDPOINT ------------------------------------------------------------------- */ + PREMIUM UPGRADE ENDPOINT + ------------------------------------------------------------------ */ app.post('/api/activate-premium', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Authorization token is required' }); } - let userId; // Will hold the auto-increment "id" from user_profile + let profileId; try { const decoded = jwt.verify(token, SECRET_KEY); - userId = decoded.userId; // => user_profile.id (auto-inc PK) + profileId = decoded.id; } catch (error) { return res.status(401).json({ error: 'Invalid or expired token' }); } - // Use MySQL pool.query instead of db.run (which is for SQLite) const query = ` UPDATE user_profile SET is_premium = 1, is_pro_premium = 1 WHERE id = ? `; - pool.query(query, [userId], (err, results) => { + pool.query(query, [profileId], (err) => { if (err) { console.error('Error updating premium status:', err.message); return res.status(500).json({ error: 'Failed to activate premium' }); @@ -460,7 +561,9 @@ app.post('/api/activate-premium', (req, res) => { }); }); -// =============== START SERVER =============== +/* ------------------------------------------------------------------ + START SERVER + ------------------------------------------------------------------ */ app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); diff --git a/src/App.css b/src/App.css index 1cd1509..09423ca 100644 --- a/src/App.css +++ b/src/App.css @@ -39,6 +39,13 @@ body, #root { color: white; } +.dialog-overlay, +[data-state="open"].dialog-overlay { + background-color: transparent !important; + pointer-events: none !important; + z-index: 0 !important; +} + /* Animation for the logo */ .App-logo { height: 40vmin; diff --git a/src/components/CareerExplorer.js b/src/components/CareerExplorer.js index a22aad0..f00fbd1 100644 --- a/src/components/CareerExplorer.js +++ b/src/components/CareerExplorer.js @@ -249,9 +249,14 @@ function CareerExplorer() { if (profileData.career_list) { setCareerList(JSON.parse(profileData.career_list)); } - } else { - setShowModal(true); - } + + if (!profileData.career_priorities) { + setShowModal(true); + } + +} else { + setShowModal(true); +} } catch (err) { console.error('Error fetching user profile:', err); setShowModal(true); diff --git a/src/components/EducationalProgramsPage.js b/src/components/EducationalProgramsPage.js index 5cb1924..bf27e43 100644 --- a/src/components/EducationalProgramsPage.js +++ b/src/components/EducationalProgramsPage.js @@ -406,7 +406,7 @@ function EducationalProgramsPage() { Why no courses? diff --git a/src/components/SignUp.js b/src/components/SignUp.js index 1801a07..81091db 100644 --- a/src/components/SignUp.js +++ b/src/components/SignUp.js @@ -48,8 +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); const [selectedSituation, setSelectedSituation] = useState(null); @@ -172,7 +171,6 @@ function SignUp() { try { console.log("Payload sent to backend:", { - userId: frontendUserId, username, password, firstname, lastname, email, zipcode, state, area, career_situation: selectedSituation.id }); @@ -181,7 +179,6 @@ function SignUp() { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - userId: frontendUserId, username, password, firstname, @@ -212,83 +209,154 @@ function SignUp() { - return ( -
+return ( +
+ {!showCareerSituations ? (
- {!showCareerSituations ? ( - <> -

Sign Up

- {error && ( -
- {error} -
- )} -
- setUsername(e.target.value)} /> - setPassword(e.target.value)} /> - setConfirmPassword(e.target.value)}/> - setFirstname(e.target.value)} /> - setLastname(e.target.value)} /> - setEmail(e.target.value)} /> - setConfirmEmail(e.target.value)}/> - setZipcode(e.target.value)} /> - - -
- - {loadingAreas && ( - - Loading... - - )} -
- - -
- - ) : ( - <> -

Where are you in your career journey right now?

-
- {careerSituations.map((situation) => ( - { - setSelectedSituation(situation); - setShowPrompt(true); - }} - /> - ))} -
- {showPrompt &&
Modal should be open!
} - setShowPrompt(false)} - /> - +

Sign Up

+ {error && ( +
+ {error} +
)} +
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + setConfirmPassword(e.target.value)} + /> + setFirstname(e.target.value)} + /> + setLastname(e.target.value)} + /> + setEmail(e.target.value)} + /> + setConfirmEmail(e.target.value)} + /> + setZipcode(e.target.value)} + /> + + +
+ + + {loadingAreas && ( + + Loading... + + )} +
+ + +
-
- ); + ) : ( +
+

+ Where are you in your career journey right now? +

+
+ {careerSituations.map((situation) => ( + { + setSelectedSituation(situation); + setShowPrompt(true); + }} + /> + ))} +
+
+ )} + + {showPrompt && ( + setShowPrompt(false)} + /> +)} +
+); + } + export default SignUp; \ No newline at end of file diff --git a/src/components/ui/FadingPromptModal.js b/src/components/ui/FadingPromptModal.js new file mode 100644 index 0000000..a16b148 --- /dev/null +++ b/src/components/ui/FadingPromptModal.js @@ -0,0 +1,21 @@ +// FadingPromptModal.jsx +import React from 'react'; +import PromptModal from './PromptModal.js'; + +export default function FadingPromptModal({ open, ...props }) { + return ( +
+
+
+ +
+
+ ); +}