dev1/backend/server3.js

2120 lines
61 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 sql = `
SELECT
*,
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles
WHERE user_id = ?
ORDER BY start_date DESC
LIMIT 1
`;
const [rows] = await pool.query(sql, [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 sql = `
SELECT
*,
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles
WHERE user_id = ?
ORDER BY start_date ASC
`;
const [rows] = await pool.query(sql, [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 sql = `
SELECT
*,
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
FROM career_profiles
WHERE id = ?
AND user_id = ?
LIMIT 1
`;
const [rows] = await pool.query(sql, [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,
// The new field:
career_goals,
// planned fields
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();
// 1) Insert includes career_goals
const sql = `
INSERT INTO career_profiles (
id,
user_id,
scenario_title,
career_name,
status,
start_date,
projected_end_date,
college_enrollment_status,
currently_working,
career_goals, -- ADD THIS
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),
career_goals = VALUES(career_goals), -- ADD THIS
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,
career_goals || null, // pass career_goals here
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 scenarios 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.' });
}
});
/***************************************************
AI - NEXT STEPS ENDPOINT (with date constraints,
ignoring scenarioRow.start_date)
****************************************************/
app.post('/api/premium/ai/next-steps', authenticatePremiumUser, async (req, res) => {
try {
// 1) Gather user data from request
const {
userProfile = {},
scenarioRow = {},
financialProfile = {},
collegeProfile = {}
} = req.body;
// 2) Build a summary for ChatGPT
// (We'll ignore scenarioRow.start_date in the prompt)
const summaryText = buildUserSummary({
userProfile,
scenarioRow,
financialProfile,
collegeProfile
});
// 3) Dynamically compute "today's" date and future cutoffs
const now = new Date();
const isoToday = now.toISOString().slice(0, 10); // e.g. "2025-06-01"
// short-term = within 6 months
const shortTermLimit = new Date(now);
shortTermLimit.setMonth(shortTermLimit.getMonth() + 6);
const isoShortTermLimit = shortTermLimit.toISOString().slice(0, 10);
// long-term = 1-3 years
const oneYearFromNow = new Date(now);
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);
const isoOneYearFromNow = oneYearFromNow.toISOString().slice(0, 10);
const threeYearsFromNow = new Date(now);
threeYearsFromNow.setFullYear(threeYearsFromNow.getFullYear() + 3);
const isoThreeYearsFromNow = threeYearsFromNow.toISOString().slice(0, 10);
// 4) Construct ChatGPT messages
const messages = [
{
role: 'system',
content: `
You are an expert career & financial coach.
Today's date: ${isoToday}.
Short-term means any date up to ${isoShortTermLimit} (within 6 months).
Long-term means a date between ${isoOneYearFromNow} and ${isoThreeYearsFromNow} (1-3 years).
All milestone dates must be strictly >= ${isoToday}. Titles must be <= 5 words.
Respond ONLY in the requested JSON format.`
},
{
role: 'user',
content: `
Here is the user's current situation:
${summaryText}
Please provide exactly 3 short-term (within 6 months) and 2 long-term (13 years) milestones.
Each milestone must have:
- "title" (up to 5 words)
- "date" in YYYY-MM-DD format (>= ${isoToday})
- "description" (1-2 sentences)
Return ONLY a JSON array, no extra text:
[
{
"title": "string",
"date": "YYYY-MM-DD",
"description": "string"
},
...
]`
}
];
// 5) Call OpenAI (ignoring scenarioRow.start_date for date logic)
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const completion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo', // or 'gpt-4'
messages,
temperature: 0.7,
max_tokens: 600
});
// 6) Extract raw text
const aiAdvice = completion?.choices?.[0]?.message?.content?.trim() || 'No response';
res.json({ recommendations: aiAdvice });
} catch (err) {
console.error('Error in /api/premium/ai/next-steps =>', err);
res.status(500).json({ error: 'Failed to get AI next steps.' });
}
});
/**
* Helper that converts user data into a concise text summary.
* This can still mention scenarioRow, but we do NOT feed
* scenarioRow.start_date to ChatGPT for future date calculations.
*/
function buildUserSummary({
userProfile = {},
scenarioRow = {},
financialProfile = {},
collegeProfile = {}
}) {
// Provide a short multiline string about the user's finances, goals, etc.
// but avoid referencing scenarioRow.start_date
// e.g.:
const location = `${userProfile.state || 'Unknown State'}, ${userProfile.area || 'N/A'}`;
const careerName = scenarioRow.career_name || 'Unknown';
const careerGoals = scenarioRow.career_goals || 'No goals specified';
const status = scenarioRow.status || 'planned';
const currentlyWorking = scenarioRow.currently_working || 'no';
const currentSalary = financialProfile.current_salary || 0;
const monthlyExpenses = financialProfile.monthly_expenses || 0;
const monthlyDebt = financialProfile.monthly_debt_payments || 0;
const retirementSavings = financialProfile.retirement_savings || 0;
const emergencyFund = financialProfile.emergency_fund || 0;
// And similarly for collegeProfile if needed, ignoring start_date
return `
User Location: ${location}
Career Name: ${careerName}
Career Goals: ${careerGoals}
Career Status: ${status}
Currently Working: ${currentlyWorking}
Financial:
- Salary: \$${currentSalary}
- Monthly Expenses: \$${monthlyExpenses}
- Monthly Debt: \$${monthlyDebt}
- Retirement Savings: \$${retirementSavings}
- Emergency Fund: \$${emergencyFund}
`.trim();
}
/***************************************************
AI MILESTONE CONVERSION ENDPOINT
****************************************************/
app.post('/api/premium/milestone/convert-ai', authenticatePremiumUser, async (req, res) => {
try {
// The client passes us an array of milestones, e.g.:
// { milestones: [ { title, date, description, tasks, impacts }, ... ] }
const { milestones } = req.body;
const { careerProfileId } = req.query;
// or from body, if you prefer:
// const { careerProfileId } = req.body;
if (!careerProfileId) {
return res.status(400).json({ error: 'careerProfileId is required.' });
}
if (!Array.isArray(milestones)) {
return res.status(400).json({ error: 'Expected milestones array in body.' });
}
const newMilestones = [];
for (const m of milestones) {
// Required fields for your DB:
// title, date, career_profile_id
if (!m.title || !m.date) {
return res.status(400).json({
error: 'Missing required milestone fields (title/date).',
details: m
});
}
// create the milestone row
const id = uuidv4();
await pool.query(`
INSERT INTO milestones (
id,
user_id,
career_profile_id,
title,
description,
date,
progress,
status,
is_universal
) VALUES (?, ?, ?, ?, ?, ?, 0, 'planned', 0)
`, [
id,
req.id,
careerProfileId,
m.title,
m.description || '',
m.date
]);
// If the user also sent tasks in m.tasks:
if (Array.isArray(m.tasks)) {
for (const t of m.tasks) {
const taskId = uuidv4();
await pool.query(`
INSERT INTO tasks (
id,
milestone_id,
user_id,
title,
description,
due_date,
status
) VALUES (?, ?, ?, ?, ?, ?, 'not_started')
`, [
taskId,
id,
req.id,
t.title || 'Task',
t.description || '',
t.due_date || null
]);
}
}
// If the user also sent impacts in m.impacts:
if (Array.isArray(m.impacts)) {
for (const imp of m.impacts) {
const impactId = uuidv4();
await pool.query(`
INSERT INTO milestone_impacts (
id,
milestone_id,
impact_type,
direction,
amount,
start_date,
end_date
) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
impactId,
id,
imp.impact_type || 'none',
imp.direction || 'add',
imp.amount || 0,
imp.start_date || null,
imp.end_date || null
]);
}
}
newMilestones.push({
id,
title: m.title,
description: m.description || '',
date: m.date,
tasks: m.tasks || [],
impacts: m.impacts || []
});
}
return res.json({ createdMilestones: newMilestones });
} catch (err) {
console.error('Error converting AI milestones:', err);
return res.status(500).json({ error: 'Failed to convert AI milestones.' });
}
});
/* ------------------------------------------------------------------
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 {
title,
description,
date,
career_profile_id,
progress,
status,
is_universal
} = m;
if (!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,
title,
description,
date,
progress,
status,
is_universal
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
req.id,
career_profile_id,
title,
description || '',
date,
progress || 0,
status || 'planned',
is_universal ? 1 : 0
]);
createdMilestones.push({
id,
user_id: req.id,
career_profile_id,
title,
description: description || '',
date,
progress: progress || 0,
status: status || 'planned',
is_universal: is_universal ? 1 : 0,
tasks: []
});
}
return res.status(201).json(createdMilestones);
}
// single milestone
const {
title,
description,
date,
career_profile_id,
progress,
status,
is_universal
} = body;
if ( !title || !date || !career_profile_id) {
return res.status(400).json({
error: 'Missing required fields',
details: { title, date, career_profile_id }
});
}
const id = uuidv4();
await pool.query(`
INSERT INTO milestones (
id,
user_id,
career_profile_id,
title,
description,
date,
progress,
status,
is_universal
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
req.id,
career_profile_id,
title,
description || '',
date,
progress || 0,
status || 'planned',
is_universal ? 1 : 0
]);
const newMilestone = {
id,
user_id: req.id,
career_profile_id,
title,
description: description || '',
date,
progress: progress || 0,
status: status || 'planned',
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 {
title,
description,
date,
career_profile_id,
progress,
status,
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 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 finalIsUniversal = is_universal != null ? (is_universal ? 1 : 0) : row.is_universal;
await pool.query(`
UPDATE milestones
SET
title = ?,
description = ?,
date = ?,
career_profile_id = ?,
progress = ?,
status = ?,
is_universal = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
AND user_id = ?
`, [
finalTitle,
finalDesc,
finalDate,
finalCareerProfileId,
finalProgress,
finalStatus,
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,
title,
description,
date,
progress,
status,
is_universal,
origin_milestone_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
newMilestoneId,
req.id,
scenarioId,
original.title,
original.description,
original.date,
original.progress,
original.status,
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}`);
});