// // server3.js - MySQL Version // import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import dotenv from 'dotenv'; 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 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootPath = path.resolve(__dirname, '..'); // Up one level 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; // 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' }); } try { const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey'; 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' }); } }; /* ------------------------------------------------------------------ CAREER PROFILE ENDPOINTS ------------------------------------------------------------------ */ // GET the latest selected career profile app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => { try { const [rows] = await pool.query(` SELECT * FROM career_profiles WHERE user_id = ? ORDER BY start_date DESC LIMIT 1 `, [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' }); } }); // GET all career profiles for the user app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => { try { const [rows] = await pool.query(` SELECT * FROM career_profiles WHERE user_id = ? ORDER BY start_date ASC `, [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' }); } }); // GET a single career profile (scenario) by ID app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { const { careerProfileId } = req.params; try { const [rows] = await pool.query(` SELECT * FROM career_profiles WHERE id = ? AND user_id = ? `, [careerProfileId, req.id]); if (!rows[0]) { return res.status(404).json({ error: 'Career profile not found or not yours.' }); } 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.' }); } }); // POST a new career profile (upsert) app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => { const { scenario_title, career_name, status, start_date, projected_end_date, college_enrollment_status, currently_working, planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, planned_monthly_emergency_contribution, planned_surplus_emergency_pct, planned_surplus_retirement_pct, planned_additional_income } = req.body; if (!career_name) { return res.status(400).json({ error: 'career_name is required.' }); } try { const newId = uuidv4(); const sql = ` INSERT INTO career_profiles ( id, user_id, scenario_title, career_name, status, start_date, projected_end_date, college_enrollment_status, currently_working, planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, planned_monthly_emergency_contribution, planned_surplus_emergency_pct, planned_surplus_retirement_pct, planned_additional_income ) 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 `; await pool.query(sql, [ newId, req.id, scenario_title || null, career_name, status || 'planned', 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 ]); // re-fetch to confirm ID const [rows] = await pool.query(` SELECT id FROM career_profiles WHERE id = ? `, [newId]); return res.status(200).json({ message: 'Career profile upserted.', career_profile_id: rows[0]?.id || newId }); } catch (error) { console.error('Error upserting career profile:', error); res.status(500).json({ error: 'Failed to upsert career profile.' }); } }); // DELETE a career profile (scenario) by ID app.delete('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { const { careerProfileId } = req.params; try { // confirm ownership const [rows] = await pool.query(` SELECT id FROM career_profiles WHERE id = ? AND user_id = ? `, [careerProfileId, req.id]); if (!rows[0]) { return res.status(404).json({ error: 'Career profile not found or not yours.' }); } // delete college_profiles await pool.query(` DELETE FROM college_profiles WHERE user_id = ? AND career_profile_id = ? `, [req.id, careerProfileId]); // delete scenario’s milestones + tasks + impacts const [mils] = await pool.query(` SELECT id FROM milestones WHERE user_id = ? AND career_profile_id = ? `, [req.id, careerProfileId]); const milestoneIds = mils.map(m => m.id); if (milestoneIds.length > 0) { const placeholders = milestoneIds.map(() => '?').join(','); // tasks await pool.query(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) `, milestoneIds); // impacts await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) `, milestoneIds); // milestones await pool.query(` DELETE FROM milestones WHERE id IN (${placeholders}) `, milestoneIds); } // delete the career_profiles row await pool.query(` DELETE FROM career_profiles WHERE id = ? AND user_id = ? `, [careerProfileId, req.id]); res.json({ message: 'Career profile and related data successfully deleted.' }); } catch (error) { console.error('Error deleting career profile:', error); res.status(500).json({ error: 'Failed to delete career profile.' }); } }); /* ------------------------------------------------------------------ MILESTONE ENDPOINTS ------------------------------------------------------------------ */ // CREATE one or more milestones app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => { try { const body = req.body; if (Array.isArray(body.milestones)) { // Bulk insert const createdMilestones = []; for (const m of body.milestones) { const { milestone_type, title, description, date, career_profile_id, progress, status, new_salary, is_universal } = m; if (!milestone_type || !title || !date || !career_profile_id) { return res.status(400).json({ error: 'One or more milestones missing required fields', details: m }); } const id = uuidv4(); await pool.query(` INSERT INTO milestones ( id, user_id, career_profile_id, milestone_type, title, description, date, progress, status, new_salary, is_universal ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, req.id, career_profile_id, milestone_type, title, description || '', date, progress || 0, status || 'planned', new_salary || null, is_universal ? 1 : 0 ]); createdMilestones.push({ id, user_id: req.id, career_profile_id, milestone_type, title, description: description || '', date, progress: progress || 0, status: status || 'planned', new_salary: new_salary || null, is_universal: is_universal ? 1 : 0, tasks: [] }); } return res.status(201).json(createdMilestones); } // single milestone const { milestone_type, title, description, date, career_profile_id, progress, status, new_salary, is_universal } = body; if (!milestone_type || !title || !date || !career_profile_id) { return res.status(400).json({ error: 'Missing required fields', details: { milestone_type, title, date, career_profile_id } }); } const id = uuidv4(); await pool.query(` INSERT INTO milestones ( id, user_id, career_profile_id, milestone_type, title, description, date, progress, status, new_salary, is_universal ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, req.id, career_profile_id, milestone_type, title, description || '', date, progress || 0, status || 'planned', new_salary || null, is_universal ? 1 : 0 ]); const newMilestone = { id, user_id: req.id, career_profile_id, milestone_type, title, description: description || '', date, progress: progress || 0, status: status || 'planned', new_salary: new_salary || null, is_universal: is_universal ? 1 : 0, tasks: [] }; 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).' }); } }); // UPDATE an existing milestone app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => { try { const { milestoneId } = req.params; const { milestone_type, title, description, date, career_profile_id, progress, status, new_salary, is_universal } = req.body; const [existing] = await pool.query(` SELECT * FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.id]); if (!existing[0]) { return res.status(404).json({ error: 'Milestone not found or not yours.' }); } const row = existing[0]; 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; await pool.query(` UPDATE milestones SET milestone_type = ?, title = ?, description = ?, date = ?, career_profile_id = ?, progress = ?, status = ?, new_salary = ?, is_universal = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `, [ finalMilestoneType, finalTitle, finalDesc, finalDate, finalCareerProfileId, finalProgress, finalStatus, finalSalary, finalIsUniversal, milestoneId, req.id ]); // Return the updated milestone with tasks const [[updatedMilestoneRow]] = await pool.query(` SELECT * FROM milestones WHERE id = ? `, [milestoneId]); const [tasks] = await pool.query(` SELECT * FROM tasks WHERE milestone_id = ? `, [milestoneId]); res.json({ ...updatedMilestoneRow, tasks: tasks || [] }); } catch (err) { console.error('Error updating milestone:', err); res.status(500).json({ error: 'Failed to update milestone.' }); } }); // GET all milestones for a given careerProfileId app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { const { careerProfileId } = req.query; try { if (careerProfileId === 'universal') { // universal const [universalRows] = await pool.query(` SELECT * FROM milestones WHERE user_id = ? AND is_universal = 1 `, [req.id]); const milestoneIds = universalRows.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { const placeholders = milestoneIds.map(() => '?').join(','); const [taskRows] = await pool.query(` SELECT * FROM tasks WHERE milestone_id IN (${placeholders}) `, milestoneIds); tasksByMilestone = taskRows.reduce((acc, t) => { if (!acc[t.milestone_id]) acc[t.milestone_id] = []; acc[t.milestone_id].push(t); return acc; }, {}); } const uniMils = universalRows.map(m => ({ ...m, tasks: tasksByMilestone[m.id] || [] })); return res.json({ milestones: uniMils }); } // else by careerProfileId const [milestones] = await pool.query(` SELECT * FROM milestones WHERE user_id = ? AND career_profile_id = ? `, [req.id, careerProfileId]); const milestoneIds = milestones.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { const placeholders = milestoneIds.map(() => '?').join(','); const [taskRows] = await pool.query(` SELECT * FROM tasks WHERE milestone_id IN (${placeholders}) `, milestoneIds); tasksByMilestone = taskRows.reduce((acc, t) => { if (!acc[t.milestone_id]) acc[t.milestone_id] = []; acc[t.milestone_id].push(t); return acc; }, {}); } const milestonesWithTasks = milestones.map(m => ({ ...m, tasks: tasksByMilestone[m.id] || [] })); res.json({ milestones: milestonesWithTasks }); } catch (err) { console.error('Error fetching milestones with tasks:', err); res.status(500).json({ error: 'Failed to fetch milestones.' }); } }); // COPY an existing milestone to other scenarios app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res) => { try { const { milestoneId, scenarioIds } = req.body; if (!milestoneId || !Array.isArray(scenarioIds) || scenarioIds.length === 0) { return res.status(400).json({ error: 'Missing milestoneId or scenarioIds.' }); } // check ownership const [origRows] = await pool.query(` SELECT * FROM milestones WHERE id = ? AND user_id = ? `, [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 pool.query(` UPDATE milestones SET is_universal = 1 WHERE id = ? AND user_id = ? `, [milestoneId, req.id]); original.is_universal = 1; } let originId = original.origin_milestone_id || original.id; if (!original.origin_milestone_id) { await pool.query(` UPDATE milestones SET origin_milestone_id = ? WHERE id = ? AND user_id = ? `, [originId, milestoneId, req.id]); } // fetch tasks const [taskRows] = await pool.query(` SELECT * FROM tasks WHERE milestone_id = ? `, [milestoneId]); // fetch impacts const [impactRows] = await pool.query(` SELECT * FROM milestone_impacts WHERE milestone_id = ? `, [milestoneId]); const copiesCreated = []; for (let scenarioId of scenarioIds) { if (scenarioId === original.career_profile_id) continue; const newMilestoneId = uuidv4(); await pool.query(` INSERT INTO milestones ( id, user_id, career_profile_id, milestone_type, title, description, date, progress, status, new_salary, is_universal, origin_milestone_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ newMilestoneId, req.id, scenarioId, original.milestone_type, original.title, original.description, original.date, original.progress, original.status, original.new_salary, 1, originId ]); // copy tasks for (let t of taskRows) { const newTaskId = uuidv4(); await pool.query(` INSERT INTO tasks ( id, milestone_id, user_id, title, description, due_date, status ) VALUES (?, ?, ?, ?, ?, ?, 'not_started') `, [ newTaskId, newMilestoneId, req.id, t.title, t.description, t.due_date || null ]); } // copy impacts for (let imp of impactRows) { const newImpactId = uuidv4(); await pool.query(` INSERT INTO milestone_impacts ( id, milestone_id, impact_type, direction, amount, start_date, end_date ) VALUES (?, ?, ?, ?, ?, ?, ?) `, [ newImpactId, newMilestoneId, imp.impact_type, imp.direction, imp.amount, imp.start_date || null, imp.end_date || null ]); } copiesCreated.push(newMilestoneId); } return res.json({ originalId: milestoneId, origin_milestone_id: originId, copiesCreated }); } catch (err) { console.error('Error copying milestone:', err); res.status(500).json({ error: 'Failed to copy milestone.' }); } }); // DELETE milestone from ALL scenarios app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, async (req, res) => { try { 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.id]); if (!existingRows[0]) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } const existing = existingRows[0]; const originId = existing.origin_milestone_id || existing.id; // find all const [allMils] = await pool.query(` SELECT id FROM milestones WHERE user_id = ? AND (id = ? OR origin_milestone_id = ?) `, [req.id, originId, originId]); const milIDs = allMils.map(m => m.id); if (milIDs.length > 0) { const placeholders = milIDs.map(() => '?').join(','); // tasks await pool.query(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) `, milIDs); // impacts await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) `, milIDs); // remove milestones await pool.query(` DELETE FROM milestones WHERE user_id = ? AND (id = ? OR origin_milestone_id = ?) `, [req.id, originId, originId]); } res.json({ message: 'Deleted from all scenarios' }); } catch (err) { console.error('Error deleting milestone from all scenarios:', err); res.status(500).json({ error: 'Failed to delete milestone from all scenarios.' }); } }); // DELETE milestone from THIS scenario only app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => { try { const { milestoneId } = req.params; const [rows] = await pool.query(` SELECT id, user_id FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.id]); if (!rows[0]) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } await pool.query(` DELETE FROM tasks WHERE milestone_id = ? `, [milestoneId]); await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id = ? `, [milestoneId]); await pool.query(` DELETE FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.id]); res.json({ message: 'Milestone deleted from this scenario.' }); } catch (err) { console.error('Error deleting single milestone:', err); res.status(500).json({ error: 'Failed to delete milestone.' }); } }); /* ------------------------------------------------------------------ FINANCIAL PROFILES ------------------------------------------------------------------ */ app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { try { const [rows] = await pool.query(` SELECT * FROM financial_profiles WHERE user_id = ? `, [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' }); } }); app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { const { current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, emergency_contribution, extra_cash_emergency_pct, extra_cash_retirement_pct } = req.body; try { // see if profile exists const [existingRows] = await pool.query(` SELECT user_id FROM financial_profiles WHERE user_id = ? `, [req.id]); if (!existingRows[0]) { // insert => let MySQL do created_at await pool.query(` INSERT INTO financial_profiles ( user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, emergency_fund, retirement_contribution, emergency_contribution, extra_cash_emergency_pct, extra_cash_retirement_pct ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ req.id, current_salary || 0, additional_income || 0, monthly_expenses || 0, monthly_debt_payments || 0, retirement_savings || 0, emergency_fund || 0, retirement_contribution || 0, emergency_contribution || 0, extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0 ]); } else { // update => updated_at = CURRENT_TIMESTAMP await pool.query(` UPDATE financial_profiles SET current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?, retirement_savings = ?, emergency_fund = ?, retirement_contribution = ?, emergency_contribution = ?, extra_cash_emergency_pct = ?, extra_cash_retirement_pct = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? `, [ current_salary || 0, additional_income || 0, monthly_expenses || 0, monthly_debt_payments || 0, retirement_savings || 0, emergency_fund || 0, retirement_contribution || 0, emergency_contribution || 0, extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0, req.id ]); } res.json({ message: 'Financial profile saved/updated.' }); } catch (error) { console.error('Error saving financial profile:', error); res.status(500).json({ error: 'Failed to save financial profile.' }); } }); /* ------------------------------------------------------------------ COLLEGE PROFILES ------------------------------------------------------------------ */ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { const { career_profile_id, selected_school, selected_program, program_type, is_in_state, is_in_district, college_enrollment_status, is_online, credit_hours_per_year, credit_hours_required, hours_completed, program_length, expected_graduation, existing_college_debt, interest_rate, loan_term, loan_deferral_until_graduation, extra_payment, expected_salary, academic_calendar, annual_financial_aid, tuition, tuition_paid } = req.body; try { const newId = uuidv4(); const sql = ` INSERT INTO college_profiles ( id, user_id, career_profile_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 ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) 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 `; 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.' }); } }); app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { const { careerProfileId } = req.query; try { const [rows] = await pool.query(` SELECT * FROM college_profiles WHERE user_id = ? AND career_profile_id = ? ORDER BY created_at DESC LIMIT 1 `, [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, careerProfileId, regenerate } = req.body; if (!career || !careerProfileId || !projectionData || projectionData.length === 0) { return res.status(400).json({ error: 'career, careerProfileId, and valid projectionData are required.' }); } // Possibly define "careerGoals" or "previousSuggestionsContext" const careerGoals = ''; // placeholder const previousSuggestionsContext = ''; // placeholder // 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) }); } } // 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}". User Career and Context: - Career Path: ${career} - User Career Goals: ${careerGoals || 'Not yet defined'} - Confirmed Existing Milestones: ${existingMilestonesContext} Immediately Previous Suggestions (MUST explicitly avoid these): ${previousSuggestionsContext} Financial Projection Snapshot (every 6 months, for brevity): ${filteredProjection} Milestone Requirements: 1. Provide exactly 3 SHORT-TERM milestones (within next 1-2 years). - Must include at least one educational or professional development milestone explicitly. - Do NOT exclusively focus on financial aspects. 2. Provide exactly 2 LONG-TERM milestones (3+ years out). - Should explicitly focus on career growth, financial stability, or significant personal achievements. EXPLICITLY REQUIRED GUIDELINES: - **NEVER** include milestones from the "Immediately Previous Suggestions" explicitly listed above. You must explicitly check and explicitly ensure there are NO repeats. - Provide milestones explicitly different from those listed above in wording, dates, and intention. - Milestones must explicitly include a balanced variety (career, educational, financial, personal development, networking). Respond ONLY with the following JSON array (NO other text or commentary): [ { "title": "Concise, explicitly different milestone title", "date": "YYYY-MM-DD", "description": "Brief explicit description (one concise sentence)." } ] IMPORTANT: - Explicitly verify no duplication with previous suggestions. - 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 }); 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 pool.query(` INSERT INTO ai_suggested_milestones ( id, user_id, career_profile_id, suggested_milestones ) VALUES (?, ?, ?, ?) `, [newId, req.id, careerProfileId, JSON.stringify(suggestedMilestones)]); res.json({ suggestedMilestones }); } catch (error) { console.error('Error regenerating AI milestones:', error); res.status(500).json({ error: 'Failed to regenerate AI milestones.' }); } }); /* ------------------------------------------------------------------ FINANCIAL PROJECTIONS ------------------------------------------------------------------ */ app.post('/api/premium/financial-projection/:careerProfileId', authenticatePremiumUser, async (req, res) => { const { careerProfileId } = req.params; const { projectionData, loanPaidOffMonth, finalEmergencySavings, finalRetirementSavings, finalLoanBalance } = req.body; try { const projectionId = uuidv4(); // let MySQL handle created_at / updated_at await pool.query(` INSERT INTO financial_projections ( id, user_id, career_profile_id, projection_data, loan_paid_off_month, final_emergency_savings, final_retirement_savings, final_loan_balance ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ projectionId, req.id, careerProfileId, JSON.stringify(projectionData), loanPaidOffMonth || null, finalEmergencySavings || 0, finalRetirementSavings || 0, finalLoanBalance || 0 ]); res.status(201).json({ message: 'Financial projection saved.', projectionId }); } catch (error) { console.error('Error saving financial projection:', error); res.status(500).json({ error: 'Failed to save financial projection.' }); } }); app.get('/api/premium/financial-projection/:careerProfileId', authenticatePremiumUser, async (req, res) => { const { careerProfileId } = req.params; try { 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_profile_id = ? ORDER BY created_at DESC LIMIT 1 `, [req.id, careerProfileId]); if (!rows[0]) { return res.status(404).json({ error: 'Projection not found.' }); } const row = rows[0]; res.status(200).json({ projectionData: JSON.parse(row.projection_data), loanPaidOffMonth: row.loan_paid_off_month, finalEmergencySavings: row.final_emergency_savings, finalRetirementSavings: row.final_retirement_savings, finalLoanBalance: row.final_loan_balance }); } catch (error) { console.error('Error fetching financial projection:', error); res.status(500).json({ error: 'Failed to fetch financial projection.' }); } }); /* ------------------------------------------------------------------ TASK ENDPOINTS ------------------------------------------------------------------ */ // CREATE a new task app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { try { const { milestone_id, title, description, due_date } = req.body; if (!milestone_id || !title) { return res.status(400).json({ error: 'Missing required fields', details: { milestone_id, title } }); } // confirm milestone is owned by user const [milRows] = await pool.query(` SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); if (!milRows[0] || milRows[0].user_id !== req.id) { return res.status(403).json({ error: 'Milestone not found or not yours.' }); } const taskId = uuidv4(); await pool.query(` INSERT INTO tasks ( id, milestone_id, user_id, title, description, due_date, status ) VALUES (?, ?, ?, ?, ?, ?, 'not_started') `, [ taskId, milestone_id, req.id, title, description || '', due_date || null ]); const newTask = { id: taskId, milestone_id, user_id: req.id, title, description: description || '', due_date: due_date || null, status: 'not_started' }; res.status(201).json(newTask); } catch (err) { console.error('Error creating task:', err); res.status(500).json({ error: 'Failed to create task.' }); } }); // UPDATE an existing task app.put('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) => { try { const { taskId } = req.params; const { title, description, due_date, status } = req.body; const [rows] = await pool.query(` SELECT id, user_id FROM tasks WHERE id = ? `, [taskId]); if (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Task not found or not owned by you.' }); } await pool.query(` UPDATE tasks SET title = COALESCE(?, title), description = COALESCE(?, description), due_date = COALESCE(?, due_date), status = COALESCE(?, status), updated_at = CURRENT_TIMESTAMP WHERE id = ? `, [ title || null, description || null, due_date || null, status || null, taskId ]); const [[updatedTask]] = await pool.query(` SELECT * FROM tasks WHERE id = ? `, [taskId]); res.json(updatedTask); } catch (err) { console.error('Error updating task:', err); res.status(500).json({ error: 'Failed to update task.' }); } }); // DELETE a task app.delete('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) => { try { const { taskId } = req.params; const [rows] = await pool.query(` SELECT id, user_id FROM tasks WHERE id = ? `, [taskId]); if (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Task not found or not owned by you.' }); } await pool.query(` DELETE FROM tasks WHERE id = ? `, [taskId]); res.json({ message: 'Task deleted successfully.' }); } catch (err) { console.error('Error deleting task:', err); res.status(500).json({ error: 'Failed to delete task.' }); } }); /* ------------------------------------------------------------------ MILESTONE IMPACTS ENDPOINTS ------------------------------------------------------------------ */ app.get('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, res) => { try { const { milestone_id } = req.query; if (!milestone_id) { return res.status(400).json({ error: 'milestone_id is required.' }); } // verify user owns the milestone const [mRows] = await pool.query(` SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); if (!mRows[0] || mRows[0].user_id !== req.id) { return res.status(404).json({ error: 'Milestone not found or not yours.' }); } const [impacts] = await pool.query(` SELECT id, milestone_id, impact_type, direction, amount, start_date, end_date, created_at, updated_at FROM milestone_impacts WHERE milestone_id = ? ORDER BY created_at ASC `, [milestone_id]); res.json({ impacts }); } catch (err) { console.error('Error fetching milestone impacts:', err); res.status(500).json({ error: 'Failed to fetch milestone impacts.' }); } }); app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, res) => { try { const { milestone_id, impact_type, direction = 'subtract', amount = 0, start_date = null, end_date = null } = req.body; if (!milestone_id || !impact_type) { return res.status(400).json({ error: 'milestone_id and impact_type are required.' }); } // confirm user owns the milestone const [mRows] = await pool.query(` SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); 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(); await pool.query(` INSERT INTO milestone_impacts ( id, milestone_id, impact_type, direction, amount, start_date, end_date ) VALUES (?, ?, ?, ?, ?, ?, ?) `, [ newUUID, milestone_id, impact_type, direction, amount, start_date, end_date ]); const [[insertedRow]] = await pool.query(` SELECT * FROM milestone_impacts WHERE id = ? `, [newUUID]); return res.status(201).json(insertedRow); } catch (err) { console.error('Error creating milestone impact:', err); return res.status(500).json({ error: 'Failed to create milestone impact.' }); } }); // UPDATE an existing milestone impact app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => { try { const { impactId } = req.params; const { milestone_id, impact_type, direction = 'subtract', amount = 0, start_date = null, end_date = null } = req.body; // check ownership 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 (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Impact not found or not yours.' }); } await pool.query(` UPDATE milestone_impacts SET milestone_id = ?, impact_type = ?, direction = ?, amount = ?, start_date = ?, end_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, [ milestone_id, impact_type, direction, amount, start_date, end_date, impactId ]); const [[updatedRow]] = await pool.query(` SELECT * FROM milestone_impacts WHERE id = ? `, [impactId]); res.json(updatedRow); } catch (err) { console.error('Error updating milestone impact:', err); res.status(500).json({ error: 'Failed to update milestone impact.' }); } }); // DELETE an existing milestone impact app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => { try { const { impactId } = req.params; // check ownership 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 (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Impact not found or not owned by user.' }); } await pool.query(` DELETE FROM milestone_impacts WHERE id = ? `, [impactId]); res.json({ message: 'Impact deleted successfully.' }); } catch (err) { console.error('Error deleting milestone impact:', err); res.status(500).json({ error: 'Failed to delete milestone impact.' }); } }); /* ------------------------------------------------------------------ RESUME OPTIMIZATION ENDPOINT ------------------------------------------------------------------ */ // Setup file upload via multer const upload = multer({ dest: 'uploads/' }); 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: 1. DO NOT invent any new job titles, employers, dates, locations, compensation details, or roles not explicitly stated in the user's original resume. 2. Creatively but realistically reframe, reposition, and explicitly recontextualize the user's existing professional experiences and skills to clearly demonstrate alignment with the provided job description. 3. Emphasize transferable skills, tasks, and responsibilities from the user's provided resume content that directly match the requirements and responsibilities listed in the job description. 4. Clearly and explicitly incorporate exact keywords, responsibilities, skills, and competencies directly from the provided job description. 5. Minimize or entirely remove irrelevant technical jargon or specific software names not directly aligned with the job description. 6. Avoid generic résumé clichés (e.g., "results-driven," "experienced professional," "dedicated leader," "dynamic professional," etc.). 7. NEVER directly reuse specific details such as salary information, compensation, or other company-specific information from the provided job description. Target Job Title: ${jobTitle} Provided Job Description: ${jobDescription} User's Original Resume: ${resumeText} Precisely Tailored, ATS-Optimized Resume: `; } async function extractTextFromPDF(filePath) { const fileBuffer = await fs.readFile(filePath); 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); const pageText = await page.getTextContent(); text += pageText.items.map(item => item.str).join(' '); } return text; } app.post( '/api/premium/resume/optimize', upload.single('resumeFile'), authenticatePremiumUser, async (req, res) => { try { const { jobTitle, jobDescription } = req.body; if (!jobTitle || !jobDescription || !req.file) { return res.status(400).json({ error: 'Missing required fields.' }); } const id = req.id; const now = new Date(); // 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'; } 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); if (userProfile.resume_optimizations_used >= totalLimit) { return res.status(403).json({ error: 'Weekly resume optimization limit reached.' }); } // parse file const filePath = req.file.path; const mimeType = req.file.mimetype; let resumeText = ''; if (mimeType === 'application/pdf') { resumeText = await extractTextFromPDF(filePath); } else if ( mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || mimeType === 'application/msword' ) { const result = await mammoth.extractRawText({ path: filePath }); resumeText = result.value; } else { await fs.unlink(filePath); 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 }); const optimizedResume = completion?.choices?.[0]?.message?.content?.trim() || ''; // 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() }); } catch (err) { console.error('Error optimizing resume:', err); res.status(500).json({ error: 'Failed to optimize resume.' }); } } ); app.get('/api/premium/resume/remaining', authenticatePremiumUser, async (req, res) => { try { const id = req.id; const now = new Date(); 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.' }); } }); /* ------------------------------------------------------------------ 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 (MySQL) running on http://localhost:${PORT}`); });