dev1/backend/server3.js

739 lines
20 KiB
JavaScript

// server3.js
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import { fileURLToPath } from 'url';
// If you still need the projection logic somewhere else
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
const app = express();
const PORT = process.env.PREMIUM_PORT || 5002;
let db;
const initDB = async () => {
try {
db = await open({
filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db',
driver: sqlite3.Database
});
console.log('Connected to user_profile.db for Premium Services.');
} catch (error) {
console.error('Error connecting to premium database:', error);
}
};
initDB();
app.use(helmet());
app.use(express.json());
const allowedOrigins = ['https://dev1.aptivaai.com'];
app.use(cors({ origin: allowedOrigins, credentials: true }));
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 { userId } = jwt.verify(token, SECRET_KEY);
req.userId = userId;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
};
/* ------------------------------------------------------------------
CAREER PROFILE ENDPOINTS
(Renamed from planned-path to career-profile)
------------------------------------------------------------------ */
// GET the latest selected career profile
app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => {
try {
const row = await db.get(`
SELECT *
FROM career_paths
WHERE user_id = ?
ORDER BY start_date DESC
LIMIT 1
`, [req.userId]);
res.json(row || {});
} 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 db.all(`
SELECT *
FROM career_paths
WHERE user_id = ?
ORDER BY start_date ASC
`, [req.userId]);
res.json({ careerPaths: rows });
} catch (error) {
console.error('Error fetching career profiles:', error);
res.status(500).json({ error: 'Failed to fetch career profiles' });
}
});
// POST a new career profile
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
const {
career_name,
status,
start_date,
projected_end_date,
college_enrollment_status,
currently_working
} = req.body;
// If you need to ensure the user gave us a career_name:
if (!career_name) {
return res.status(400).json({ error: 'career_name is required.' });
}
try {
const newCareerPathId = uuidv4();
const now = new Date().toISOString();
await db.run(`
INSERT INTO career_paths (
id,
user_id,
career_name,
status,
start_date,
projected_end_date,
college_enrollment_status,
currently_working,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(user_id, career_name)
DO UPDATE SET
status = excluded.status,
start_date = excluded.start_date,
projected_end_date = excluded.projected_end_date,
college_enrollment_status = excluded.college_enrollment_status,
currently_working = excluded.currently_working,
updated_at = ?
`, [
newCareerPathId, // id
req.userId, // user_id
career_name, // career_name
status || 'planned', // status (if null, default to 'planned')
start_date || now,
projected_end_date || null,
college_enrollment_status || null,
currently_working || null,
now, // created_at
now, // updated_at on initial insert
now // updated_at on conflict
]);
// Optionally fetch the row's ID after upsert
const result = await db.get(`
SELECT id
FROM career_paths
WHERE user_id = ?
AND career_name = ?
`, [req.userId, career_name]);
res.status(200).json({
message: 'Career profile upserted.',
career_path_id: result?.id
});
} catch (error) {
console.error('Error upserting career profile:', error);
res.status(500).json({ error: 'Failed to upsert career profile.' });
}
});
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
try {
const {
milestone_type,
title,
description,
date,
career_path_id,
progress,
status,
new_salary
} = req.body;
if (!milestone_type || !title || !date || !career_path_id) {
return res.status(400).json({
error: 'Missing required fields',
details: { milestone_type, title, date, career_path_id }
});
}
const id = uuidv4();
const now = new Date().toISOString();
await db.run(`
INSERT INTO milestones (
id,
user_id,
career_path_id,
milestone_type,
title,
description,
date,
progress,
status,
new_salary, -- store the full new salary if provided
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
req.userId,
career_path_id,
milestone_type,
title,
description || '',
date,
progress || 0,
status || 'planned',
new_salary || null,
now,
now
]);
// Return the newly created milestone object
// (No tasks initially, so tasks = [])
const newMilestone = {
id,
user_id: req.userId,
career_path_id,
milestone_type,
title,
description: description || '',
date,
progress: progress || 0,
status: status || 'planned',
new_salary: new_salary || null,
tasks: []
};
res.status(201).json(newMilestone);
} catch (err) {
console.error('Error creating milestone:', err);
res.status(500).json({ error: 'Failed to create milestone.' });
}
});
app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => {
try {
const { milestoneId } = req.params;
const {
milestone_type,
title,
description,
date,
career_path_id,
progress,
status,
new_salary
} = req.body;
// Check if milestone exists and belongs to user
const existing = await db.get(`
SELECT *
FROM milestones
WHERE id = ?
AND user_id = ?
`, [milestoneId, req.userId]);
if (!existing) {
return res.status(404).json({ error: 'Milestone not found or not yours.' });
}
// Update
const now = new Date().toISOString();
await db.run(`
UPDATE milestones
SET
milestone_type = ?,
title = ?,
description = ?,
date = ?,
career_path_id = ?,
progress = ?,
status = ?,
new_salary = ?,
updated_at = ?
WHERE id = ?
`, [
milestone_type || existing.milestone_type,
title || existing.title,
description || existing.description,
date || existing.date,
career_path_id || existing.career_path_id,
progress != null ? progress : existing.progress,
status || existing.status,
new_salary != null ? new_salary : existing.new_salary,
now,
milestoneId
]);
// Return the updated record with tasks
const updatedMilestoneRow = await db.get(`
SELECT *
FROM milestones
WHERE id = ?
`, [milestoneId]);
// Fetch tasks for this milestone
const tasks = await db.all(`
SELECT *
FROM tasks
WHERE milestone_id = ?
`, [milestoneId]);
const updatedMilestone = {
...updatedMilestoneRow,
tasks: tasks || []
};
res.json(updatedMilestone);
} catch (err) {
console.error('Error updating milestone:', err);
res.status(500).json({ error: 'Failed to update milestone.' });
}
});
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.query;
try {
// 1. Fetch the milestones for this user + path
const milestones = await db.all(`
SELECT *
FROM milestones
WHERE user_id = ?
AND career_path_id = ?
`, [req.userId, careerPathId]);
// 2. For each milestone, fetch tasks
const milestoneIds = milestones.map(m => m.id);
let tasksByMilestone = {};
if (milestoneIds.length > 0) {
const tasks = await db.all(`
SELECT *
FROM tasks
WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')})
`, milestoneIds);
tasksByMilestone = tasks.reduce((acc, t) => {
if (!acc[t.milestone_id]) acc[t.milestone_id] = [];
acc[t.milestone_id].push(t);
return acc;
}, {});
}
// 3. Attach tasks to each milestone object
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.' });
}
});
/* ------------------------------------------------------------------
FINANCIAL PROFILES (Renamed emergency_contribution)
------------------------------------------------------------------ */
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
try {
const row = await db.get(`
SELECT *
FROM financial_profiles
WHERE user_id = ?
`, [req.userId]);
res.json(row || {});
} 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 {
// Check if row exists
const existing = await db.get(`
SELECT user_id
FROM financial_profiles
WHERE user_id = ?
`, [req.userId]);
if (!existing) {
// Insert new row
await db.run(`
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,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [
req.userId,
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, // store new field
extra_cash_emergency_pct || 0,
extra_cash_retirement_pct || 0
]);
} else {
// Update existing
await db.run(`
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, // updated field
extra_cash_emergency_pct || 0,
extra_cash_retirement_pct || 0,
req.userId
]);
}
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_path_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 id = uuidv4();
const user_id = req.userId;
await db.run(`
INSERT INTO college_profiles (
id,
user_id,
career_path_id,
selected_school,
selected_program,
program_type,
is_in_state,
is_in_district,
college_enrollment_status,
annual_financial_aid,
is_online,
credit_hours_per_year,
hours_completed,
program_length,
credit_hours_required,
expected_graduation,
existing_college_debt,
interest_rate,
loan_term,
loan_deferral_until_graduation,
extra_payment,
expected_salary,
academic_calendar,
tuition,
tuition_paid,
created_at,
updated_at
) VALUES (
?, -- id
?, -- user_id
?, -- career_path_id
?, -- selected_school
?, -- selected_program
?, -- program_type
?, -- is_in_state
?, -- is_in_district
?, -- college_enrollment_status
?, -- annual_financial_aid
?, -- is_online
?, -- credit_hours_per_year
?, -- hours_completed
?, -- program_length
?, -- credit_hours_required
?, -- expected_graduation
?, -- existing_college_debt
?, -- interest_rate
?, -- loan_term
?, -- loan_deferral_until_graduation
?, -- extra_payment
?, -- expected_salary
?, -- academic_calendar
?, -- tuition
?, -- tuition_paid
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
`, [
id,
user_id,
career_path_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 saved.',
collegeProfileId: id
});
} catch (error) {
console.error('Error saving college profile:', error);
res.status(500).json({ error: 'Failed to save college profile.' });
}
});
/* ------------------------------------------------------------------
FINANCIAL PROJECTIONS
------------------------------------------------------------------ */
app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.params;
const { projectionData, loanPaidOffMonth, finalEmergencySavings, finalRetirementSavings, finalLoanBalance } = req.body;
try {
const projectionId = uuidv4();
await db.run(`
INSERT INTO financial_projections (
id, user_id, career_path_id, projection_data,
loan_paid_off_month, final_emergency_savings,
final_retirement_savings, final_loan_balance,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [
projectionId,
req.userId,
careerPathId,
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/:careerPathId', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.params;
try {
const row = await db.get(`
SELECT projection_data, loan_paid_off_month,
final_emergency_savings, final_retirement_savings, final_loan_balance
FROM financial_projections
WHERE user_id = ?
AND career_path_id = ?
ORDER BY created_at DESC
LIMIT 1
`, [req.userId, careerPathId]);
if (!row) {
return res.status(404).json({ error: 'Projection not found.' });
}
const parsedProjectionData = JSON.parse(row.projection_data);
res.status(200).json({
projectionData: parsedProjectionData,
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.' });
}
});
// POST create a new task
app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
const {
milestone_id, // which milestone this belongs to
user_id, // might come from token or from body
title,
description,
due_date
} = req.body;
// Insert into tasks table
// Return the new task in JSON
});
// GET tasks for a milestone
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.query;
try {
// 1. Fetch the milestones for this user + path
const milestones = await db.all(`
SELECT *
FROM milestones
WHERE user_id = ?
AND career_path_id = ?
`, [req.userId, careerPathId]);
// 2. For each milestone, fetch tasks (or do a single join—see note below)
// We'll do it in Node code for clarity:
const milestoneIds = milestones.map(m => m.id);
let tasksByMilestone = {};
if (milestoneIds.length > 0) {
const tasks = await db.all(`
SELECT *
FROM tasks
WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')})
`, milestoneIds);
// Group tasks by milestone_id
tasksByMilestone = tasks.reduce((acc, t) => {
if (!acc[t.milestone_id]) acc[t.milestone_id] = [];
acc[t.milestone_id].push(t);
return acc;
}, {});
}
// 3. Attach tasks to each milestone object
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.' });
}
});
app.listen(PORT, () => {
console.log(`Premium server running on http://localhost:${PORT}`);
});