diff --git a/backend/server.js b/backend/server.js index d9ab4cc..23795be 100755 --- a/backend/server.js +++ b/backend/server.js @@ -180,7 +180,7 @@ app.post('/api/register', async (req, res) => { // 2) Insert into user_auth, referencing user_profile.id const authQuery = ` - INSERT INTO user_auth (user_id, username, hashed_password) + INSERT INTO user_auth (id, username, hashed_password) VALUES (?, ?, ?) `; pool.query( @@ -229,7 +229,7 @@ app.post('/api/signin', (req, res) => { const query = ` SELECT - user_auth.user_id, + user_auth.id, user_auth.hashed_password, user_profile.firstname, user_profile.lastname, @@ -243,7 +243,7 @@ app.post('/api/signin', (req, res) => { 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.id = user_profile.id WHERE user_auth.username = ? `; pool.query(query, [username], async (err, results) => { @@ -265,8 +265,8 @@ app.post('/api/signin', (req, res) => { return res.status(401).json({ error: 'Invalid username or password' }); } - // The user_profile id is stored in user_auth.user_id - const token = jwt.sign({ id: row.user_id }, SECRET_KEY, { + // The user_profile id is stored in user_auth.id + const token = jwt.sign({ id: row.id }, SECRET_KEY, { expiresIn: '2h', }); @@ -274,7 +274,7 @@ app.post('/api/signin', (req, res) => { res.status(200).json({ message: 'Login successful', token, - userId: row.user_id, // The user_profile.id + id: row.id, // The user_profile.id user: { firstname: row.firstname, lastname: row.lastname, diff --git a/backend/server3.js b/backend/server3.js index b664559..41c8fe9 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -1,22 +1,22 @@ -// server3.js -import express from 'express'; +// +// server3.js - MySQL Version +// +import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import dotenv from 'dotenv'; -import { open } from 'sqlite'; -import sqlite3 from 'sqlite3'; -import jwt from 'jsonwebtoken'; -import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import fs from 'fs/promises'; import multer from 'multer'; import mammoth from 'mammoth'; import { fileURLToPath } from 'url'; +import jwt from 'jsonwebtoken'; +import { v4 as uuidv4 } from 'uuid'; import pkg from 'pdfjs-dist'; - +import mysql from 'mysql2/promise'; // <-- MySQL instead of SQLite import OpenAI from 'openai'; -// --- Basic file init --- +// Basic file init const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -25,41 +25,39 @@ const env = process.env.NODE_ENV?.trim() || 'development'; const envPath = path.resolve(rootPath, `.env.${env}`); dotenv.config({ path: envPath }); // Load .env file - const app = express(); const PORT = process.env.PREMIUM_PORT || 5002; - const { getDocument } = pkg; -let db; -const initDB = async () => { - try { - db = await open({ - filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db', - driver: sqlite3.Database - }); - console.log('Connected to user_profile.db for Premium Services.'); - } catch (error) { - console.error('Error connecting to premium database:', error); - } -}; -initDB(); +// 1) Create a MySQL pool using your environment variables +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'user_profile_db', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); +// 2) Basic middlewares app.use(helmet()); app.use(express.json({ limit: '5mb' })); - const allowedOrigins = ['https://dev1.aptivaai.com']; app.use(cors({ origin: allowedOrigins, credentials: true })); +// 3) Authentication middleware const authenticatePremiumUser = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; - if (!token) return res.status(401).json({ error: 'Premium authorization required' }); + if (!token) { + return res.status(401).json({ error: 'Premium authorization required' }); + } try { const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey'; - const { userId } = jwt.verify(token, SECRET_KEY); - req.userId = userId; + const { id } = jwt.verify(token, SECRET_KEY); + req.id = id; // store user ID in request next(); } catch (error) { return res.status(403).json({ error: 'Invalid or expired token' }); @@ -73,14 +71,15 @@ const authenticatePremiumUser = (req, res, next) => { // GET the latest selected career profile app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => { try { - const row = await db.get(` + const [rows] = await pool.query(` SELECT * - FROM career_paths + FROM career_profiles WHERE user_id = ? ORDER BY start_date DESC LIMIT 1 - `, [req.userId]); - res.json(row || {}); + `, [req.id]); + + res.json(rows[0] || {}); } catch (error) { console.error('Error fetching latest career profile:', error); res.status(500).json({ error: 'Failed to fetch latest career profile' }); @@ -90,13 +89,14 @@ app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (re // GET all career profiles for the user app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => { try { - const rows = await db.all(` + const [rows] = await pool.query(` SELECT * - FROM career_paths + FROM career_profiles WHERE user_id = ? ORDER BY start_date ASC - `, [req.userId]); - res.json({ careerPaths: rows }); + `, [req.id]); + + res.json({ careerProfiles: rows }); } catch (error) { console.error('Error fetching career profiles:', error); res.status(500).json({ error: 'Failed to fetch career profiles' }); @@ -104,21 +104,20 @@ app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, }); // GET a single career profile (scenario) by ID -app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.params; +app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { + const { careerProfileId } = req.params; try { - const row = await db.get(` + const [rows] = await pool.query(` SELECT * - FROM career_paths + FROM career_profiles WHERE id = ? AND user_id = ? - `, [careerPathId, req.userId]); + `, [careerProfileId, req.id]); - if (!row) { - return res.status(404).json({ error: 'Career path (scenario) not found or not yours.' }); + if (!rows[0]) { + return res.status(404).json({ error: 'Career profile not found or not yours.' }); } - - res.json(row); + res.json(rows[0]); } catch (error) { console.error('Error fetching single career profile:', error); res.status(500).json({ error: 'Failed to fetch career profile by ID.' }); @@ -135,7 +134,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res projected_end_date, college_enrollment_status, currently_working, - planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, @@ -150,11 +148,10 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res } try { - const newCareerPathId = uuidv4(); - const now = new Date().toISOString(); + const newId = uuidv4(); - await db.run(` - INSERT INTO career_paths ( + const sql = ` + INSERT INTO career_profiles ( id, user_id, scenario_title, @@ -170,69 +167,54 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res planned_monthly_emergency_contribution, planned_surplus_emergency_pct, planned_surplus_retirement_pct, - planned_additional_income, - created_at, - updated_at + planned_additional_income ) - VALUES ( - ?, ?, ?, ?, ?, - ?, ?, ?, ?, - ?, ?, ?, ?, - ?, ?, ?, - ?, ? - ) - ON CONFLICT(user_id, career_name) - DO UPDATE SET - status = excluded.status, - start_date = excluded.start_date, - projected_end_date = excluded.projected_end_date, - college_enrollment_status = excluded.college_enrollment_status, - currently_working = excluded.currently_working, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + status = VALUES(status), + start_date = VALUES(start_date), + projected_end_date = VALUES(projected_end_date), + college_enrollment_status = VALUES(college_enrollment_status), + currently_working = VALUES(currently_working), + planned_monthly_expenses = VALUES(planned_monthly_expenses), + planned_monthly_debt_payments = VALUES(planned_monthly_debt_payments), + planned_monthly_retirement_contribution = VALUES(planned_monthly_retirement_contribution), + planned_monthly_emergency_contribution = VALUES(planned_monthly_emergency_contribution), + planned_surplus_emergency_pct = VALUES(planned_surplus_emergency_pct), + planned_surplus_retirement_pct = VALUES(planned_surplus_retirement_pct), + planned_additional_income = VALUES(planned_additional_income), + updated_at = CURRENT_TIMESTAMP + `; - planned_monthly_expenses = excluded.planned_monthly_expenses, - planned_monthly_debt_payments = excluded.planned_monthly_debt_payments, - planned_monthly_retirement_contribution = excluded.planned_monthly_retirement_contribution, - planned_monthly_emergency_contribution = excluded.planned_monthly_emergency_contribution, - planned_surplus_emergency_pct = excluded.planned_surplus_emergency_pct, - planned_surplus_retirement_pct = excluded.planned_surplus_retirement_pct, - planned_additional_income = excluded.planned_additional_income, - - updated_at = ? - `, [ - newCareerPathId, - req.userId, + await pool.query(sql, [ + newId, + req.id, scenario_title || null, career_name, status || 'planned', - start_date || now, + start_date || null, projected_end_date || null, college_enrollment_status || null, currently_working || null, - planned_monthly_expenses ?? null, planned_monthly_debt_payments ?? null, planned_monthly_retirement_contribution ?? null, planned_monthly_emergency_contribution ?? null, planned_surplus_emergency_pct ?? null, planned_surplus_retirement_pct ?? null, - planned_additional_income ?? null, - - now, - now, - - now + planned_additional_income ?? null ]); - const result = await db.get(` + // re-fetch to confirm ID + const [rows] = await pool.query(` SELECT id - FROM career_paths - WHERE user_id = ? - AND career_name = ? - `, [req.userId, career_name]); + FROM career_profiles + WHERE id = ? + `, [newId]); - res.status(200).json({ + return res.status(200).json({ message: 'Career profile upserted.', - career_path_id: result?.id + career_profile_id: rows[0]?.id || newId }); } catch (error) { console.error('Error upserting career profile:', error); @@ -240,77 +222,76 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res } }); -// DELETE a career path (scenario) by ID (and associated data) -app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.params; - +// DELETE a career profile (scenario) by ID +app.delete('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { + const { careerProfileId } = req.params; try { - // 1) Confirm that this career_path belongs to the user - const existing = await db.get(` + // confirm ownership + const [rows] = await pool.query(` SELECT id - FROM career_paths + FROM career_profiles WHERE id = ? AND user_id = ? - `, [careerPathId, req.userId]); + `, [careerProfileId, req.id]); - if (!existing) { - return res.status(404).json({ error: 'Career path not found or not yours.' }); + if (!rows[0]) { + return res.status(404).json({ error: 'Career profile not found or not yours.' }); } - // 2) Delete the college_profile for this scenario - await db.run(` + // delete college_profiles + await pool.query(` DELETE FROM college_profiles WHERE user_id = ? - AND career_path_id = ? - `, [req.userId, careerPathId]); + AND career_profile_id = ? + `, [req.id, careerProfileId]); - // 3) Delete scenario’s milestones (and tasks/impacts) - const scenarioMilestones = await db.all(` + // delete scenario’s milestones + tasks + impacts + const [mils] = await pool.query(` SELECT id FROM milestones WHERE user_id = ? - AND career_path_id = ? - `, [req.userId, careerPathId]); - const milestoneIds = scenarioMilestones.map((m) => m.id); + AND career_profile_id = ? + `, [req.id, careerProfileId]); + const milestoneIds = mils.map(m => m.id); if (milestoneIds.length > 0) { const placeholders = milestoneIds.map(() => '?').join(','); - // Delete tasks - await db.run(` + // tasks + await pool.query(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) `, milestoneIds); - // Delete impacts - await db.run(` + // impacts + await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) `, milestoneIds); - // Finally delete the milestones themselves - await db.run(` + // milestones + await pool.query(` DELETE FROM milestones WHERE id IN (${placeholders}) `, milestoneIds); } - // 4) Delete the career_path row - await db.run(` - DELETE FROM career_paths - WHERE user_id = ? - AND id = ? - `, [req.userId, careerPathId]); + // delete the career_profiles row + await pool.query(` + DELETE FROM career_profiles + WHERE id = ? + AND user_id = ? + `, [careerProfileId, req.id]); - res.json({ message: 'Career path and related data successfully deleted.' }); + res.json({ message: 'Career profile and related data successfully deleted.' }); } catch (error) { - console.error('Error deleting career path:', error); - res.status(500).json({ error: 'Failed to delete career path.' }); + console.error('Error deleting career profile:', error); + res.status(500).json({ error: 'Failed to delete career profile.' }); } }); /* ------------------------------------------------------------------ - Milestone ENDPOINTS + MILESTONE ENDPOINTS ------------------------------------------------------------------ */ // CREATE one or more milestones @@ -318,8 +299,8 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => try { const body = req.body; - // CASE 1: If client sent { milestones: [ ... ] }, do a bulk insert if (Array.isArray(body.milestones)) { + // Bulk insert const createdMilestones = []; for (const m of body.milestones) { const { @@ -327,14 +308,14 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => title, description, date, - career_path_id, + career_profile_id, progress, status, new_salary, is_universal } = m; - if (!milestone_type || !title || !date || !career_path_id) { + if (!milestone_type || !title || !date || !career_profile_id) { return res.status(400).json({ error: 'One or more milestones missing required fields', details: m @@ -342,13 +323,11 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => } const id = uuidv4(); - const now = new Date().toISOString(); - - await db.run(` + await pool.query(` INSERT INTO milestones ( id, user_id, - career_path_id, + career_profile_id, milestone_type, title, description, @@ -356,14 +335,13 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress, status, new_salary, - is_universal, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + is_universal + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, - req.userId, - career_path_id, + req.id, + career_profile_id, milestone_type, title, description || '', @@ -371,15 +349,13 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress || 0, status || 'planned', new_salary || null, - is_universal ? 1 : 0, - now, - now + is_universal ? 1 : 0 ]); createdMilestones.push({ id, - user_id: req.userId, - career_path_id, + user_id: req.id, + career_profile_id, milestone_type, title, description: description || '', @@ -394,34 +370,32 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => return res.status(201).json(createdMilestones); } - // CASE 2: Single milestone creation + // single milestone const { milestone_type, title, description, date, - career_path_id, + career_profile_id, progress, status, new_salary, is_universal } = body; - if (!milestone_type || !title || !date || !career_path_id) { + if (!milestone_type || !title || !date || !career_profile_id) { return res.status(400).json({ error: 'Missing required fields', - details: { milestone_type, title, date, career_path_id } + details: { milestone_type, title, date, career_profile_id } }); } const id = uuidv4(); - const now = new Date().toISOString(); - - await db.run(` + await pool.query(` INSERT INTO milestones ( id, user_id, - career_path_id, + career_profile_id, milestone_type, title, description, @@ -429,14 +403,13 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress, status, new_salary, - is_universal, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + is_universal + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, - req.userId, - career_path_id, + req.id, + career_profile_id, milestone_type, title, description || '', @@ -444,15 +417,13 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress || 0, status || 'planned', new_salary || null, - is_universal ? 1 : 0, - now, - now + is_universal ? 1 : 0 ]); const newMilestone = { id, - user_id: req.userId, - career_path_id, + user_id: req.id, + career_profile_id, milestone_type, title, description: description || '', @@ -463,8 +434,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => is_universal: is_universal ? 1 : 0, tasks: [] }; - - res.status(201).json(newMilestone); + return res.status(201).json(newMilestone); } catch (err) { console.error('Error creating milestone(s):', err); res.status(500).json({ error: 'Failed to create milestone(s).' }); @@ -480,51 +450,48 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( title, description, date, - career_path_id, + career_profile_id, progress, status, new_salary, is_universal } = req.body; - // Check if milestone exists and belongs to user - const existing = await db.get(` + const [existing] = await pool.query(` SELECT * FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); + `, [milestoneId, req.id]); - if (!existing) { + if (!existing[0]) { return res.status(404).json({ error: 'Milestone not found or not yours.' }); } + const row = existing[0]; - const now = new Date().toISOString(); + const finalMilestoneType = milestone_type || row.milestone_type; + const finalTitle = title || row.title; + const finalDesc = description || row.description; + const finalDate = date || row.date; + const finalCareerProfileId = career_profile_id || row.career_profile_id; + const finalProgress = progress != null ? progress : row.progress; + const finalStatus = status || row.status; + const finalSalary = new_salary != null ? new_salary : row.new_salary; + const finalIsUniversal = is_universal != null ? (is_universal ? 1 : 0) : row.is_universal; - const finalMilestoneType = milestone_type || existing.milestone_type; - const finalTitle = title || existing.title; - const finalDesc = description || existing.description; - const finalDate = date || existing.date; - const finalCareerPath = career_path_id || existing.career_path_id; - const finalProgress = progress != null ? progress : existing.progress; - const finalStatus = status || existing.status; - const finalSalary = new_salary != null ? new_salary : existing.new_salary; - const finalIsUniversal = - is_universal != null ? (is_universal ? 1 : 0) : existing.is_universal; - - await db.run(` + await pool.query(` UPDATE milestones SET milestone_type = ?, title = ?, description = ?, date = ?, - career_path_id = ?, + career_profile_id = ?, progress = ?, status = ?, new_salary = ?, is_universal = ?, - updated_at = ? + updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `, [ @@ -532,62 +499,62 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( finalTitle, finalDesc, finalDate, - finalCareerPath, + finalCareerProfileId, finalProgress, finalStatus, finalSalary, finalIsUniversal, - now, milestoneId, - req.userId + req.id ]); // Return the updated milestone with tasks - const updatedMilestoneRow = await db.get(` + const [[updatedMilestoneRow]] = await pool.query(` SELECT * FROM milestones WHERE id = ? `, [milestoneId]); - const tasks = await db.all(` + + const [tasks] = await pool.query(` SELECT * FROM tasks WHERE milestone_id = ? `, [milestoneId]); - const updatedMilestone = { + res.json({ ...updatedMilestoneRow, tasks: tasks || [] - }; - - res.json(updatedMilestone); + }); } catch (err) { console.error('Error updating milestone:', err); res.status(500).json({ error: 'Failed to update milestone.' }); } }); -// GET all milestones for a given careerPathId +// GET all milestones for a given careerProfileId app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.query; + const { careerProfileId } = req.query; try { - // universal milestones - if (careerPathId === 'universal') { - const universalRows = await db.all(` + if (careerProfileId === 'universal') { + // universal + const [universalRows] = await pool.query(` SELECT * FROM milestones WHERE user_id = ? AND is_universal = 1 - `, [req.userId]); + `, [req.id]); const milestoneIds = universalRows.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { - const tasks = await db.all(` + const placeholders = milestoneIds.map(() => '?').join(','); + const [taskRows] = await pool.query(` SELECT * FROM tasks - WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')}) + WHERE milestone_id IN (${placeholders}) `, milestoneIds); - tasksByMilestone = tasks.reduce((acc, t) => { + + tasksByMilestone = taskRows.reduce((acc, t) => { if (!acc[t.milestone_id]) acc[t.milestone_id] = []; acc[t.milestone_id].push(t); return acc; @@ -601,24 +568,25 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => return res.json({ milestones: uniMils }); } - // else fetch by careerPathId - const milestones = await db.all(` + // else by careerProfileId + const [milestones] = await pool.query(` SELECT * FROM milestones WHERE user_id = ? - AND career_path_id = ? - `, [req.userId, careerPathId]); + AND career_profile_id = ? + `, [req.id, careerProfileId]); const milestoneIds = milestones.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { - const tasks = await db.all(` + const placeholders = milestoneIds.map(() => '?').join(','); + const [taskRows] = await pool.query(` SELECT * FROM tasks - WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')}) + WHERE milestone_id IN (${placeholders}) `, milestoneIds); - tasksByMilestone = tasks.reduce((acc, t) => { + tasksByMilestone = taskRows.reduce((acc, t) => { if (!acc[t.milestone_id]) acc[t.milestone_id] = []; acc[t.milestone_id].push(t); return acc; @@ -629,7 +597,6 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => ...m, tasks: tasksByMilestone[m.id] || [] })); - res.json({ milestones: milestonesWithTasks }); } catch (err) { console.error('Error fetching milestones with tasks:', err); @@ -645,62 +612,65 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res return res.status(400).json({ error: 'Missing milestoneId or scenarioIds.' }); } - const original = await db.get(` + // check ownership + const [origRows] = await pool.query(` SELECT * FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); - if (!original) { + `, [milestoneId, req.id]); + + if (!origRows[0]) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } + const original = origRows[0]; + // if not universal => set universal = 1 if (original.is_universal !== 1) { - await db.run(` + await pool.query(` UPDATE milestones SET is_universal = 1 WHERE id = ? AND user_id = ? - `, [ milestoneId, req.userId ]); + `, [milestoneId, req.id]); original.is_universal = 1; } let originId = original.origin_milestone_id || original.id; if (!original.origin_milestone_id) { - await db.run(` + await pool.query(` UPDATE milestones SET origin_milestone_id = ? WHERE id = ? AND user_id = ? - `, [ originId, milestoneId, req.userId ]); + `, [originId, milestoneId, req.id]); } - const tasks = await db.all(` + // fetch tasks + const [taskRows] = await pool.query(` SELECT * FROM tasks WHERE milestone_id = ? `, [milestoneId]); - const impacts = await db.all(` + // fetch impacts + const [impactRows] = await pool.query(` SELECT * FROM milestone_impacts WHERE milestone_id = ? `, [milestoneId]); - const now = new Date().toISOString(); const copiesCreated = []; for (let scenarioId of scenarioIds) { - if (scenarioId === original.career_path_id) continue; // skip if same scenario + if (scenarioId === original.career_profile_id) continue; const newMilestoneId = uuidv4(); - const isUniversal = 1; - - await db.run(` + await pool.query(` INSERT INTO milestones ( id, user_id, - career_path_id, + career_profile_id, milestone_type, title, description, @@ -709,14 +679,12 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res status, new_salary, is_universal, - origin_milestone_id, - created_at, - updated_at + origin_milestone_id ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ newMilestoneId, - req.userId, + req.id, scenarioId, original.milestone_type, original.title, @@ -725,16 +693,14 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res original.progress, original.status, original.new_salary, - isUniversal, - originId, - now, - now + 1, + originId ]); // copy tasks - for (let t of tasks) { + for (let t of taskRows) { const newTaskId = uuidv4(); - await db.run(` + await pool.query(` INSERT INTO tasks ( id, milestone_id, @@ -742,27 +708,23 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res title, description, due_date, - status, - created_at, - updated_at + status ) - VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?) + VALUES (?, ?, ?, ?, ?, ?, 'not_started') `, [ newTaskId, newMilestoneId, - req.userId, + req.id, t.title, t.description, - t.due_date || null, - now, - now + t.due_date || null ]); } // copy impacts - for (let imp of impacts) { + for (let imp of impactRows) { const newImpactId = uuidv4(); - await db.run(` + await pool.query(` INSERT INTO milestone_impacts ( id, milestone_id, @@ -770,11 +732,9 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res direction, amount, start_date, - end_date, - created_at, - updated_at + end_date ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?) `, [ newImpactId, newMilestoneId, @@ -782,9 +742,7 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res imp.direction, imp.amount, imp.start_date || null, - imp.end_date || null, - now, - now + imp.end_date || null ]); } @@ -804,54 +762,51 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res // DELETE milestone from ALL scenarios app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, async (req, res) => { - const { milestoneId } = req.params; - try { - // 1) Fetch the milestone - const existing = await db.get(` + const { milestoneId } = req.params; + const [existingRows] = await pool.query(` SELECT id, user_id, origin_milestone_id FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); + `, [milestoneId, req.id]); - if (!existing) { + if (!existingRows[0]) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } - - // We'll remove all milestones (the original + copies) referencing the same originId + const existing = existingRows[0]; const originId = existing.origin_milestone_id || existing.id; - // Find all those milestone IDs - const allMilsToDelete = await db.all(` + // find all + const [allMils] = await pool.query(` SELECT id FROM milestones WHERE user_id = ? AND (id = ? OR origin_milestone_id = ?) - `, [req.userId, originId, originId]); + `, [req.id, originId, originId]); - const milIDs = allMilsToDelete.map(m => m.id); + const milIDs = allMils.map(m => m.id); if (milIDs.length > 0) { const placeholders = milIDs.map(() => '?').join(','); - // Delete tasks for those milestones - await db.run(` + // tasks + await pool.query(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) `, milIDs); - // Delete impacts for those milestones - await db.run(` + // impacts + await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) `, milIDs); - // Finally remove the milestones themselves - await db.run(` + // remove milestones + await pool.query(` DELETE FROM milestones WHERE user_id = ? AND (id = ? OR origin_milestone_id = ?) - `, [req.userId, originId, originId]); + `, [req.id, originId, originId]); } res.json({ message: 'Deleted from all scenarios' }); @@ -863,39 +818,34 @@ app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, // DELETE milestone from THIS scenario only app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => { - const { milestoneId } = req.params; - try { - // 1) check user ownership - const existing = await db.get(` + const { milestoneId } = req.params; + + const [rows] = await pool.query(` SELECT id, user_id FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); - - if (!existing) { + `, [milestoneId, req.id]); + if (!rows[0]) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } - // 2) Delete tasks associated with this milestone - await db.run(` + await pool.query(` DELETE FROM tasks WHERE milestone_id = ? `, [milestoneId]); - // 3) Delete milestone impacts - await db.run(` + await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id = ? `, [milestoneId]); - // 4) Finally remove the milestone - await db.run(` + await pool.query(` DELETE FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); + `, [milestoneId, req.id]); res.json({ message: 'Milestone deleted from this scenario.' }); } catch (err) { @@ -907,15 +857,15 @@ app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, asyn /* ------------------------------------------------------------------ FINANCIAL PROFILES ------------------------------------------------------------------ */ + app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { try { - const row = await db.get(` + const [rows] = await pool.query(` SELECT * FROM financial_profiles WHERE user_id = ? - `, [req.userId]); - - res.json(row || {}); + `, [req.id]); + res.json(rows[0] || {}); } catch (error) { console.error('Error fetching financial profile:', error); res.status(500).json({ error: 'Failed to fetch financial profile' }); @@ -937,14 +887,16 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, } = req.body; try { - const existing = await db.get(` + // see if profile exists + const [existingRows] = await pool.query(` SELECT user_id FROM financial_profiles WHERE user_id = ? - `, [req.userId]); + `, [req.id]); - if (!existing) { - await db.run(` + if (!existingRows[0]) { + // insert => let MySQL do created_at + await pool.query(` INSERT INTO financial_profiles ( user_id, current_salary, @@ -956,12 +908,11 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, retirement_contribution, emergency_contribution, extra_cash_emergency_pct, - extra_cash_retirement_pct, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + extra_cash_retirement_pct + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ - req.userId, + req.id, current_salary || 0, additional_income || 0, monthly_expenses || 0, @@ -974,7 +925,8 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, extra_cash_retirement_pct || 0 ]); } else { - await db.run(` + // update => updated_at = CURRENT_TIMESTAMP + await pool.query(` UPDATE financial_profiles SET current_salary = ?, @@ -1000,7 +952,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, emergency_contribution || 0, extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0, - req.userId + req.id ]); } @@ -1014,9 +966,10 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, /* ------------------------------------------------------------------ COLLEGE PROFILES ------------------------------------------------------------------ */ + app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { const { - career_path_id, + career_profile_id, selected_school, selected_program, program_type, @@ -1042,14 +995,13 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re } = req.body; try { - const user_id = req.userId; const newId = uuidv4(); - await db.run(` + const sql = ` INSERT INTO college_profiles ( id, user_id, - career_path_id, + career_profile_id, selected_school, selected_program, program_type, @@ -1071,92 +1023,67 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re expected_salary, academic_calendar, tuition, - tuition_paid, - created_at, - updated_at + tuition_paid ) VALUES ( - :id, - :user_id, - :career_path_id, - :selected_school, - :selected_program, - :program_type, - :is_in_state, - :is_in_district, - :college_enrollment_status, - :annual_financial_aid, - :is_online, - :credit_hours_per_year, - :hours_completed, - :program_length, - :credit_hours_required, - :expected_graduation, - :existing_college_debt, - :interest_rate, - :loan_term, - :loan_deferral_until_graduation, - :extra_payment, - :expected_salary, - :academic_calendar, - :tuition, - :tuition_paid, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP + ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ? ) - ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type) - DO UPDATE SET - is_in_state = excluded.is_in_state, - is_in_district = excluded.is_in_district, - college_enrollment_status = excluded.college_enrollment_status, - annual_financial_aid = excluded.annual_financial_aid, - is_online = excluded.is_online, - credit_hours_per_year = excluded.credit_hours_per_year, - hours_completed = excluded.hours_completed, - program_length = excluded.program_length, - credit_hours_required = excluded.credit_hours_required, - expected_graduation = excluded.expected_graduation, - existing_college_debt = excluded.existing_college_debt, - interest_rate = excluded.interest_rate, - loan_term = excluded.loan_term, - loan_deferral_until_graduation = excluded.loan_deferral_until_graduation, - extra_payment = excluded.extra_payment, - expected_salary = excluded.expected_salary, - academic_calendar = excluded.academic_calendar, - tuition = excluded.tuition, - tuition_paid = excluded.tuition_paid, + ON DUPLICATE KEY UPDATE + is_in_state = VALUES(is_in_state), + is_in_district = VALUES(is_in_district), + college_enrollment_status = VALUES(college_enrollment_status), + annual_financial_aid = VALUES(annual_financial_aid), + is_online = VALUES(is_online), + credit_hours_per_year = VALUES(credit_hours_per_year), + hours_completed = VALUES(hours_completed), + program_length = VALUES(program_length), + credit_hours_required = VALUES(credit_hours_required), + expected_graduation = VALUES(expected_graduation), + existing_college_debt = VALUES(existing_college_debt), + interest_rate = VALUES(interest_rate), + loan_term = VALUES(loan_term), + loan_deferral_until_graduation = VALUES(loan_deferral_until_graduation), + extra_payment = VALUES(extra_payment), + expected_salary = VALUES(expected_salary), + academic_calendar = VALUES(academic_calendar), + tuition = VALUES(tuition), + tuition_paid = VALUES(tuition_paid), updated_at = CURRENT_TIMESTAMP - `, { - ':id': newId, - ':user_id': user_id, - ':career_path_id': career_path_id, - ':selected_school': selected_school, - ':selected_program': selected_program, - ':program_type': program_type || null, - ':is_in_state': is_in_state ? 1 : 0, - ':is_in_district': is_in_district ? 1 : 0, - ':college_enrollment_status': college_enrollment_status || null, - ':annual_financial_aid': annual_financial_aid || 0, - ':is_online': is_online ? 1 : 0, - ':credit_hours_per_year': credit_hours_per_year || 0, - ':hours_completed': hours_completed || 0, - ':program_length': program_length || 0, - ':credit_hours_required': credit_hours_required || 0, - ':expected_graduation': expected_graduation || null, - ':existing_college_debt': existing_college_debt || 0, - ':interest_rate': interest_rate || 0, - ':loan_term': loan_term || 10, - ':loan_deferral_until_graduation': loan_deferral_until_graduation ? 1 : 0, - ':extra_payment': extra_payment || 0, - ':expected_salary': expected_salary || 0, - ':academic_calendar': academic_calendar || 'semester', - ':tuition': tuition || 0, - ':tuition_paid': tuition_paid || 0 - }); + `; - res.status(201).json({ - message: 'College profile upsert done.' - }); + await pool.query(sql, [ + newId, + req.id, + career_profile_id, + selected_school, + selected_program, + program_type || null, + is_in_state ? 1 : 0, + is_in_district ? 1 : 0, + college_enrollment_status || null, + annual_financial_aid || 0, + is_online ? 1 : 0, + credit_hours_per_year || 0, + hours_completed || 0, + program_length || 0, + credit_hours_required || 0, + expected_graduation || null, + existing_college_debt || 0, + interest_rate || 0, + loan_term || 10, + loan_deferral_until_graduation ? 1 : 0, + extra_payment || 0, + expected_salary || 0, + academic_calendar || 'semester', + tuition || 0, + tuition_paid || 0 + ]); + + res.status(201).json({ message: 'College profile upsert done.' }); } catch (error) { console.error('Error saving college profile:', error); res.status(500).json({ error: 'Failed to save college profile.' }); @@ -1164,48 +1091,74 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re }); app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.query; + const { careerProfileId } = req.query; try { - const row = await db.get(` + const [rows] = await pool.query(` SELECT * FROM college_profiles WHERE user_id = ? - AND career_path_id = ? + AND career_profile_id = ? ORDER BY created_at DESC LIMIT 1 - `, [req.userId, careerPathId]); - res.json(row || {}); + `, [req.id, careerProfileId]); + + res.json(rows[0] || {}); } catch (error) { console.error('Error fetching college profile:', error); res.status(500).json({ error: 'Failed to fetch college profile.' }); } }); +/* ------------------------------------------------------------------ + AI-SUGGESTED MILESTONES + ------------------------------------------------------------------ */ app.post('/api/premium/milestone/ai-suggestions', authenticatePremiumUser, async (req, res) => { - const { career, projectionData, existingMilestones, careerPathId, regenerate } = req.body; + const { career, projectionData, existingMilestones, careerProfileId, regenerate } = req.body; - if (!career || !careerPathId || !projectionData || projectionData.length === 0) { - return res.status(400).json({ error: 'career, careerPathId, and valid projectionData are required.' }); + if (!career || !careerProfileId || !projectionData || projectionData.length === 0) { + return res.status(400).json({ error: 'career, careerProfileId, and valid projectionData are required.' }); } - if (!regenerate) { - const existingSuggestion = await db.get(` - SELECT suggested_milestones FROM ai_suggested_milestones - WHERE user_id = ? AND career_path_id = ? - `, [req.userId, careerPathId]); + // Possibly define "careerGoals" or "previousSuggestionsContext" + const careerGoals = ''; // placeholder + const previousSuggestionsContext = ''; // placeholder - if (existingSuggestion) { - return res.json({ suggestedMilestones: JSON.parse(existingSuggestion.suggested_milestones) }); + // If not regenerating, see if we have an existing suggestion + if (!regenerate) { + const [rows] = await pool.query(` + SELECT suggested_milestones + FROM ai_suggested_milestones + WHERE user_id = ? + AND career_profile_id = ? + `, [req.id, careerProfileId]); + + if (rows[0]) { + return res.json({ suggestedMilestones: JSON.parse(rows[0].suggested_milestones) }); } } - // Explicitly regenerate (delete existing cached suggestions if any) - await db.run(` - DELETE FROM ai_suggested_milestones WHERE user_id = ? AND career_path_id = ? - `, [req.userId, careerPathId]); + // delete existing suggestions if any + await pool.query(` + DELETE FROM ai_suggested_milestones + WHERE user_id = ? + AND career_profile_id = ? + `, [req.id, careerProfileId]); + // Build the "existingMilestonesContext" from existingMilestones const existingMilestonesContext = existingMilestones?.map(m => `- ${m.title} (${m.date})`).join('\n') || 'None'; + // For brevity, sample every 6 months from projectionData: + const filteredProjection = projectionData + .filter((_, i) => i % 6 === 0) + .map(m => ` +- Month: ${m.month} + Salary: ${m.salary} + Loan Balance: ${m.loanBalance} + Emergency Savings: ${m.totalEmergencySavings} + Retirement Savings: ${m.totalRetirementSavings}`) + .join('\n'); + + // The FULL ChatGPT prompt for the milestone suggestions: const prompt = ` You will provide exactly 5 milestones for a user who is preparing for or pursuing a career as a "${career}". @@ -1219,12 +1172,7 @@ Immediately Previous Suggestions (MUST explicitly avoid these): ${previousSuggestionsContext} Financial Projection Snapshot (every 6 months, for brevity): -${projectionData.filter((_, i) => i % 6 === 0).map(m => ` -- Month: ${m.month} - Salary: ${m.salary} - Loan Balance: ${m.loanBalance} - Emergency Savings: ${m.totalEmergencySavings} - Retirement Savings: ${m.totalRetirementSavings}`).join('\n')} +${filteredProjection} Milestone Requirements: 1. Provide exactly 3 SHORT-TERM milestones (within next 1-2 years). @@ -1254,25 +1202,29 @@ IMPORTANT: - No additional commentary or text beyond the JSON array. `; - - try { + const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const completion = await openai.chat.completions.create({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: prompt }], - temperature: 0.2, + temperature: 0.2 }); let content = completion?.choices?.[0]?.message?.content?.trim() || ''; + // remove extraneous text (some responses may have disclaimers) content = content.replace(/^[^{[]+/, '').replace(/[^}\]]+$/, ''); - const suggestedMilestones = JSON.parse(content); const newId = uuidv4(); - await db.run(` - INSERT INTO ai_suggested_milestones (id, user_id, career_path_id, suggested_milestones) + await pool.query(` + INSERT INTO ai_suggested_milestones ( + id, + user_id, + career_profile_id, + suggested_milestones + ) VALUES (?, ?, ?, ?) - `, [newId, req.userId, careerPathId, JSON.stringify(suggestedMilestones)]); + `, [newId, req.id, careerProfileId, JSON.stringify(suggestedMilestones)]); res.json({ suggestedMilestones }); } catch (error) { @@ -1281,13 +1233,11 @@ IMPORTANT: } }); - - /* ------------------------------------------------------------------ FINANCIAL PROJECTIONS ------------------------------------------------------------------ */ -app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.params; +app.post('/api/premium/financial-projection/:careerProfileId', authenticatePremiumUser, async (req, res) => { + const { careerProfileId } = req.params; const { projectionData, loanPaidOffMonth, @@ -1298,17 +1248,23 @@ app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumU try { const projectionId = uuidv4(); - await db.run(` + // let MySQL handle created_at / updated_at + await pool.query(` INSERT INTO financial_projections ( - id, user_id, career_path_id, projection_data, - loan_paid_off_month, final_emergency_savings, - final_retirement_savings, final_loan_balance, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + id, + user_id, + career_profile_id, + projection_data, + loan_paid_off_month, + final_emergency_savings, + final_retirement_savings, + final_loan_balance + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ projectionId, - req.userId, - careerPathId, + req.id, + careerProfileId, JSON.stringify(projectionData), loanPaidOffMonth || null, finalEmergencySavings || 0, @@ -1323,26 +1279,30 @@ app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumU } }); -app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.params; +app.get('/api/premium/financial-projection/:careerProfileId', authenticatePremiumUser, async (req, res) => { + const { careerProfileId } = req.params; try { - const row = await db.get(` - SELECT projection_data, loan_paid_off_month, - final_emergency_savings, final_retirement_savings, final_loan_balance + const [rows] = await pool.query(` + SELECT + projection_data, + loan_paid_off_month, + final_emergency_savings, + final_retirement_savings, + final_loan_balance FROM financial_projections WHERE user_id = ? - AND career_path_id = ? + AND career_profile_id = ? ORDER BY created_at DESC LIMIT 1 - `, [req.userId, careerPathId]); + `, [req.id, careerProfileId]); - if (!row) { + if (!rows[0]) { return res.status(404).json({ error: 'Projection not found.' }); } - const parsedProjectionData = JSON.parse(row.projection_data); + const row = rows[0]; res.status(200).json({ - projectionData: parsedProjectionData, + projectionData: JSON.parse(row.projection_data), loanPaidOffMonth: row.loan_paid_off_month, finalEmergencySavings: row.final_emergency_savings, finalRetirementSavings: row.final_retirement_savings, @@ -1358,7 +1318,7 @@ app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUs TASK ENDPOINTS ------------------------------------------------------------------ */ -// CREATE a new task (already existed, repeated here for clarity) +// CREATE a new task app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { try { const { milestone_id, title, description, due_date } = req.body; @@ -1369,20 +1329,19 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { }); } - // Confirm milestone is owned by this user - const milestone = await db.get(` - SELECT user_id + // confirm milestone is owned by user + const [milRows] = await pool.query(` + SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); - if (!milestone || milestone.user_id !== req.userId) { + + if (!milRows[0] || milRows[0].user_id !== req.id) { return res.status(403).json({ error: 'Milestone not found or not yours.' }); } const taskId = uuidv4(); - const now = new Date().toISOString(); - - await db.run(` + await pool.query(` INSERT INTO tasks ( id, milestone_id, @@ -1390,25 +1349,22 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { title, description, due_date, - status, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?) + status + ) + VALUES (?, ?, ?, ?, ?, ?, 'not_started') `, [ taskId, milestone_id, - req.userId, + req.id, title, description || '', - due_date || null, - now, - now + due_date || null ]); const newTask = { id: taskId, milestone_id, - user_id: req.userId, + user_id: req.id, title, description: description || '', due_date: due_date || null, @@ -1427,43 +1383,37 @@ app.put('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) const { taskId } = req.params; const { title, description, due_date, status } = req.body; - // Check ownership - const existing = await db.get(` - SELECT user_id + const [rows] = await pool.query(` + SELECT id, user_id FROM tasks WHERE id = ? `, [taskId]); - - if (!existing || existing.user_id !== req.userId) { + if (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Task not found or not owned by you.' }); } - const now = new Date().toISOString(); - await db.run(` + await pool.query(` UPDATE tasks SET title = COALESCE(?, title), description = COALESCE(?, description), due_date = COALESCE(?, due_date), status = COALESCE(?, status), - updated_at = ? + updated_at = CURRENT_TIMESTAMP WHERE id = ? `, [ title || null, description || null, due_date || null, status || null, - now, taskId ]); - // Return the updated task - const updatedTask = await db.get(` + const [[updatedTask]] = await pool.query(` SELECT * FROM tasks WHERE id = ? `, [taskId]); - res.json(updatedTask); } catch (err) { console.error('Error updating task:', err); @@ -1476,18 +1426,16 @@ app.delete('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, re try { const { taskId } = req.params; - // Verify ownership - const existing = await db.get(` - SELECT user_id + const [rows] = await pool.query(` + SELECT id, user_id FROM tasks WHERE id = ? `, [taskId]); - - if (!existing || existing.user_id !== req.userId) { + if (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Task not found or not owned by you.' }); } - await db.run(` + await pool.query(` DELETE FROM tasks WHERE id = ? `, [taskId]); @@ -1510,18 +1458,18 @@ app.get('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, r return res.status(400).json({ error: 'milestone_id is required.' }); } - // Verify the milestone belongs to this user - const milestoneRow = await db.get(` - SELECT user_id + // verify user owns the milestone + const [mRows] = await pool.query(` + SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); - if (!milestoneRow || milestoneRow.user_id !== req.userId) { - return res.status(404).json({ error: 'Milestone not found or not owned by this user.' }); + if (!mRows[0] || mRows[0].user_id !== req.id) { + return res.status(404).json({ error: 'Milestone not found or not yours.' }); } - const impacts = await db.all(` - SELECT + const [impacts] = await pool.query(` + SELECT id, milestone_id, impact_type, @@ -1551,33 +1499,25 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, direction = 'subtract', amount = 0, start_date = null, - end_date = null, - created_at, - updated_at + end_date = null } = req.body; if (!milestone_id || !impact_type) { - return res.status(400).json({ - error: 'milestone_id and impact_type are required.' - }); + return res.status(400).json({ error: 'milestone_id and impact_type are required.' }); } - // Confirm user owns the milestone - const milestoneRow = await db.get(` - SELECT user_id + // confirm user owns the milestone + const [mRows] = await pool.query(` + SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); - if (!milestoneRow || milestoneRow.user_id !== req.userId) { + if (!mRows[0] || mRows[0].user_id !== req.id) { return res.status(403).json({ error: 'Milestone not found or not owned by this user.' }); } const newUUID = uuidv4(); - const now = new Date().toISOString(); - const finalCreated = created_at || now; - const finalUpdated = updated_at || now; - - await db.run(` + await pool.query(` INSERT INTO milestone_impacts ( id, milestone_id, @@ -1585,11 +1525,9 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, direction, amount, start_date, - end_date, - created_at, - updated_at + end_date ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?) `, [ newUUID, milestone_id, @@ -1597,22 +1535,11 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, direction, amount, start_date, - end_date, - finalCreated, - finalUpdated + end_date ]); - const insertedRow = await db.get(` - SELECT - id, - milestone_id, - impact_type, - direction, - amount, - start_date, - end_date, - created_at, - updated_at + const [[insertedRow]] = await pool.query(` + SELECT * FROM milestone_impacts WHERE id = ? `, [newUUID]); @@ -1638,18 +1565,17 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy } = req.body; // check ownership - const existing = await db.get(` - SELECT mi.id, m.user_id + const [rows] = await pool.query(` + SELECT mi.id AS impact_id, m.user_id FROM milestone_impacts mi JOIN milestones m ON mi.milestone_id = m.id WHERE mi.id = ? `, [impactId]); - if (!existing || existing.user_id !== req.userId) { - return res.status(404).json({ error: 'Impact not found or not owned by user.' }); + if (!rows[0] || rows[0].user_id !== req.id) { + return res.status(404).json({ error: 'Impact not found or not yours.' }); } - const now = new Date().toISOString(); - await db.run(` + await pool.query(` UPDATE milestone_impacts SET milestone_id = ?, @@ -1658,7 +1584,7 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy amount = ?, start_date = ?, end_date = ?, - updated_at = ? + updated_at = CURRENT_TIMESTAMP WHERE id = ? `, [ milestone_id, @@ -1667,21 +1593,11 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy amount, start_date, end_date, - now, impactId ]); - const updatedRow = await db.get(` - SELECT - id, - milestone_id, - impact_type, - direction, - amount, - start_date, - end_date, - created_at, - updated_at + const [[updatedRow]] = await pool.query(` + SELECT * FROM milestone_impacts WHERE id = ? `, [impactId]); @@ -1699,18 +1615,18 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, const { impactId } = req.params; // check ownership - const existing = await db.get(` - SELECT mi.id, m.user_id + const [rows] = await pool.query(` + SELECT mi.id AS impact_id, m.user_id FROM milestone_impacts mi JOIN milestones m ON mi.milestone_id = m.id WHERE mi.id = ? `, [impactId]); - if (!existing || existing.user_id !== req.userId) { + if (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Impact not found or not owned by user.' }); } - await db.run(` + await pool.query(` DELETE FROM milestone_impacts WHERE id = ? `, [impactId]); @@ -1726,17 +1642,12 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, RESUME OPTIMIZATION ENDPOINT ------------------------------------------------------------------ */ - -const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); - // Setup file upload via multer const upload = multer({ dest: 'uploads/' }); -// Basic usage gating config -const MAX_MONTHLY_REWRITES_PREMIUM = 2; - -// Helper: build GPT prompt -const buildResumePrompt = (resumeText, jobTitle, jobDescription) => ` +function buildResumePrompt(resumeText, jobTitle, jobDescription) { + // Full ChatGPT prompt for resume optimization: + return ` You are an expert resume writer specialized in precisely tailoring existing resumes for optimal ATS compatibility and explicit alignment with provided job descriptions. STRICT GUIDELINES: @@ -1759,12 +1670,13 @@ ${resumeText} Precisely Tailored, ATS-Optimized Resume: `; +} async function extractTextFromPDF(filePath) { const fileBuffer = await fs.readFile(filePath); - const uint8Array = new Uint8Array(fileBuffer); // Convert Buffer explicitly + const uint8Array = new Uint8Array(fileBuffer); const pdfDoc = await getDocument({ data: uint8Array }).promise; - + let text = ''; for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); @@ -1774,8 +1686,6 @@ async function extractTextFromPDF(filePath) { return text; } - -// Your corrected endpoint with limits correctly returned: app.post( '/api/premium/resume/optimize', upload.single('resumeFile'), @@ -1787,19 +1697,27 @@ app.post( return res.status(400).json({ error: 'Missing required fields.' }); } - const userId = req.userId; + const id = req.id; const now = new Date(); - const userProfile = await db.get( - `SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count - FROM user_profile - WHERE user_id = ?`, - [userId] - ); + // fetch user_profile row + const [profileRows] = await pool.query(` + SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count + FROM user_profile + WHERE id = ? + `, [id]); + const userProfile = profileRows[0]; + if (!userProfile) { + return res.status(404).json({ error: 'User not found.' }); + } + // figure out usage limit let userPlan = 'basic'; - if (userProfile?.is_pro_premium) userPlan = 'pro'; - else if (userProfile?.is_premium) userPlan = 'premium'; + if (userProfile.is_pro_premium) { + userPlan = 'pro'; + } else if (userProfile.is_premium) { + userPlan = 'premium'; + } const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; const userWeeklyLimit = weeklyLimits[userPlan] || 0; @@ -1808,19 +1726,23 @@ app.post( if (!userProfile.resume_limit_reset || now > resetDate) { resetDate = new Date(now); resetDate.setDate(now.getDate() + 7); - await db.run( - `UPDATE user_profile SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE user_id = ?`, - [resetDate.toISOString(), userId] - ); + + await pool.query(` + UPDATE user_profile + SET resume_optimizations_used = 0, + resume_limit_reset = ? + WHERE id = ? + `, [resetDate.toISOString(), id]); + userProfile.resume_optimizations_used = 0; } const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0); - if (userProfile.resume_optimizations_used >= totalLimit) { - return res.status(403).json({ error: 'Weekly resume optimization limit reached. Consider purchasing a booster pack.' }); + return res.status(403).json({ error: 'Weekly resume optimization limit reached.' }); } + // parse file const filePath = req.file.path; const mimeType = req.file.mimetype; @@ -1838,30 +1760,36 @@ app.post( return res.status(400).json({ error: 'Unsupported or corrupted file upload.' }); } + // Build GPT prompt const prompt = buildResumePrompt(resumeText, jobTitle, jobDescription); + // Call OpenAI + const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const completion = await openai.chat.completions.create({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: prompt }], - temperature: 0.7, + temperature: 0.7 }); const optimizedResume = completion?.choices?.[0]?.message?.content?.trim() || ''; - await db.run( - `UPDATE user_profile SET resume_optimizations_used = resume_optimizations_used + 1 WHERE user_id = ?`, - [userId] - ); + // increment usage + await pool.query(` + UPDATE user_profile + SET resume_optimizations_used = resume_optimizations_used + 1 + WHERE id = ? + `, [id]); const remainingOptimizations = totalLimit - (userProfile.resume_optimizations_used + 1); + // remove uploaded file await fs.unlink(filePath); + res.json({ optimizedResume, remainingOptimizations, - resetDate: resetDate.toISOString() // <-- explicitly returned here! + resetDate: resetDate.toISOString() }); - } catch (err) { console.error('Error optimizing resume:', err); res.status(500).json({ error: 'Failed to optimize resume.' }); @@ -1869,67 +1797,65 @@ app.post( } ); -app.get( - '/api/premium/resume/remaining', - authenticatePremiumUser, - async (req, res) => { - try { - const userId = req.userId; - const now = new Date(); +app.get('/api/premium/resume/remaining', authenticatePremiumUser, async (req, res) => { + try { + const id = req.id; + const now = new Date(); - const userProfile = await db.get( - `SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count - FROM user_profile - WHERE user_id = ?`, - [userId] - ); - - let userPlan = 'basic'; - if (userProfile?.is_pro_premium) userPlan = 'pro'; - else if (userProfile?.is_premium) userPlan = 'premium'; - - const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; - const userWeeklyLimit = weeklyLimits[userPlan] || 0; - - let resetDate = new Date(userProfile.resume_limit_reset); - if (!userProfile.resume_limit_reset || now > resetDate) { - resetDate = new Date(now); - resetDate.setDate(now.getDate() + 7); - await db.run( - `UPDATE user_profile SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE user_id = ?`, - [resetDate.toISOString(), userId] - ); - userProfile.resume_optimizations_used = 0; - } - - const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0); - const remainingOptimizations = totalLimit - userProfile.resume_optimizations_used; - - res.json({ remainingOptimizations, resetDate }); - } catch (err) { - console.error('Error fetching remaining optimizations:', err); - res.status(500).json({ error: 'Failed to fetch remaining optimizations.' }); + const [rows] = await pool.query(` + SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count + FROM user_profile + WHERE id = ? + `, [id]); + const userProfile = rows[0]; + if (!userProfile) { + return res.status(404).json({ error: 'User not found.' }); } + + let userPlan = 'basic'; + if (userProfile.is_pro_premium) { + userPlan = 'pro'; + } else if (userProfile.is_premium) { + userPlan = 'premium'; + } + + const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; + const userWeeklyLimit = weeklyLimits[userPlan] || 0; + + let resetDate = new Date(userProfile.resume_limit_reset); + if (!userProfile.resume_limit_reset || now > resetDate) { + resetDate = new Date(now); + resetDate.setDate(now.getDate() + 7); + + await pool.query(` + UPDATE user_profile + SET resume_optimizations_used = 0, + resume_limit_reset = ? + WHERE id = ? + `, [resetDate.toISOString(), id]); + + userProfile.resume_optimizations_used = 0; + } + + const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0); + const remainingOptimizations = totalLimit - userProfile.resume_optimizations_used; + + res.json({ remainingOptimizations, resetDate }); + } catch (err) { + console.error('Error fetching remaining optimizations:', err); + res.status(500).json({ error: 'Failed to fetch remaining optimizations.' }); } -); +}); - -// Helper function to get the week number -function getWeekNumber(date) { - const oneJan = new Date(date.getFullYear(), 0, 1); - const numberOfDays = Math.floor((date - oneJan) / (24 * 60 * 60 * 1000)); - return Math.ceil((date.getDay() + 1 + numberOfDays) / 7); -} - - /* ------------------------------------------------------------------ - FALLBACK (404 for unmatched routes) + FALLBACK 404 ------------------------------------------------------------------ */ app.use((req, res) => { console.warn(`No route matched for ${req.method} ${req.originalUrl}`); res.status(404).json({ error: 'Not found' }); }); +// Start server app.listen(PORT, () => { - console.log(`Premium server running on http://localhost:${PORT}`); + console.log(`Premium server (MySQL) running on http://localhost:${PORT}`); }); diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/App.js b/src/App.js index 405b32c..db7f8ec 100644 --- a/src/App.js +++ b/src/App.js @@ -131,6 +131,7 @@ function App() { text-xs sm:text-sm md:text-base font-semibold `} + onClick={() => navigate('/planning')} > Find Your Career @@ -163,16 +164,12 @@ function App() { text-xs sm:text-sm md:text-base font-semibold `} + onClick={() => navigate('/preparing')} > Prepare for Your Career
- AptivaAI helps you advance your career. Plan career milestones, enhance your skill set, optimize your resume, and prepare for promotions or transitions. -
-+ Identify your next career milestones with AI-driven recommendations and start advancing your career today. +
+ ++ Use our comprehensive planning tools to visualize career paths, optimize your resume, and explore financial scenarios. +
++ You are at {Math.round(fraction)}% between the 10th and 90th percentiles ( + {prefix}). +
++ 10th percentile:{' '} + ${salaryData.regional.regional_PCT10?.toLocaleString() ?? 'N/A'} +
++ 25th percentile:{' '} + ${salaryData.regional.regional_PCT25?.toLocaleString() ?? 'N/A'} +
++ Median:{' '} + ${salaryData.regional.regional_MEDIAN?.toLocaleString() ?? 'N/A'} +
++ 75th percentile:{' '} + ${salaryData.regional.regional_PCT75?.toLocaleString() ?? 'N/A'} +
++ 90th percentile:{' '} + ${salaryData.regional.regional_PCT90?.toLocaleString() ?? 'N/A'} +
++ 10th percentile:{' '} + ${salaryData.national.national_PCT10?.toLocaleString() ?? 'N/A'} +
++ 25th percentile:{' '} + ${salaryData.national.national_PCT25?.toLocaleString() ?? 'N/A'} +
++ Median:{' '} + ${salaryData.national.national_MEDIAN?.toLocaleString() ?? 'N/A'} +
++ 75th percentile:{' '} + ${salaryData.national.national_PCT75?.toLocaleString() ?? 'N/A'} +
++ 90th percentile:{' '} + ${salaryData.national.national_PCT90?.toLocaleString() ?? 'N/A'} +
++ Your current salary: ${userSalary.toLocaleString()} +
++ Growth Outlook: {economicProjections.growthOutlook || 'N/A'} +
++ AI Automation Risk: {economicProjections.aiRisk || 'N/A'} +
+ {economicProjections.chatGPTAnalysis && ( +{economicProjections.chatGPTAnalysis}
+