From c2cc63e2f7c29216d896375fc746db554c1ff0f3 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 17 Jul 2025 19:26:54 +0000 Subject: [PATCH] fixed server2 database and public file access --- .env | 5 +- .env.development | 10 ++-- .env.staging | 8 +-- Dockerfile.server | 2 +- Dockerfile.server2 | 20 +++++--- Dockerfile.server3 | 2 +- backend/server2.js | 51 ++++++++++--------- docker-compose.dev.yml | 5 -- docker-compose.prod.yml | 5 -- docker-compose.staging.yml | 5 -- docker-compose.yml | 20 ++++++-- src/App.js | 3 +- src/components/CareerExplorer.js | 35 +++++++------ src/components/CareerModal.js | 1 - src/components/CareerRoadmap.js | 44 +++++++--------- src/components/Dashboard.js | 23 ++++----- src/components/MilestoneAddModal.js | 11 ++-- .../PremiumOnboarding/CareerOnboarding.js | 2 - src/components/SignIn.js | 3 +- src/utils/apiUtils.js | 3 +- src/utils/fetchCareerEnrichment.js | 10 ++-- src/utils/handleCareerClick.js | 12 ++--- 22 files changed, 135 insertions(+), 145 deletions(-) delete mode 100644 docker-compose.dev.yml delete mode 100644 docker-compose.prod.yml delete mode 100644 docker-compose.staging.yml diff --git a/.env b/.env index 559565b..ea67c9e 100644 --- a/.env +++ b/.env @@ -1,5 +1,6 @@ -IMG_TAG=20250716 +IMG_TAG=20250716 SERVER1_PORT=5000 SERVER2_PORT=5001 SERVER3_PORT=5002 -SALARY_DB=/salary_info.db \ No newline at end of file +SALARY_DB=/salary_info.db +NODE_ENV=production \ No newline at end of file diff --git a/.env.development b/.env.development index b2ebdd7..78c234e 100755 --- a/.env.development +++ b/.env.development @@ -4,8 +4,8 @@ ONET_PASSWORD=2296ahq # ─── Public‐facing React build ─────────── NODE_ENV=development -REACT_APP_ENV=production -APTIVA_API_BASE=https://dev1.aptivaai.com/api +REACT_APP_ENV=development +APTIVA_API_BASE=https://dev1.aptivaai.com REACT_APP_API_URL=${APTIVA_API_BASE} REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20 REACT_APP_BLS_API_KEY=80d7a65a809a43f3a306a41ec874d231 @@ -15,7 +15,7 @@ REACT_APP_OPENAI_API_KEY=sk-proj-IyBOKc2T9RyViN_WBZwnjNCwUiRDBekmrghpHTKyf6OsqWx OPENAI_API_KEY=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 GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20 COLLEGE_SCORECARD_KEY=BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj -SALARY_DB=/home/jcoakley/aptiva-dev1-app/salary_info.db +SALARY_DB=/salary_info.db # ─── Database (premium server) ─────────── DB_HOST=34.67.180.54 @@ -31,10 +31,10 @@ TWILIO_AUTH_TOKEN=fb8979ccb172032a249014c9c30eba80 TWILIO_MESSAGING_SERVICE_SID=MGMGaa07992a9231c841b1bfb879649026d6 # ─── Anything new goes here ────────────── -JWT_SECRET=a35F0iFAkkdWvSjnaLzepAl/JIxPRUh4NpcGptJgry2Z3KVLX4ZcYY5KaTf7kJY0 +JWT_SECRET=gW4QsOu4AJA4MooIUC9ld2i71VbBovzV1INsaU6ftxYPrxLIeMq6/OY61j0X2RV7 # ------------ CORS ------------ -CORS_ALLOWED_ORIGINS=http://localhost:3000,http://34.16.120.118:3000,https://dev1.aptivaai.com +CORS_ALLOWED_ORIGINS=https://dev1.aptivaai.com,http://34.16.120.118:3000,http://localhost:3000 SERVER1_PORT=5000 SERVER2_PORT=5001 SERVER3_PORT=5002 diff --git a/.env.staging b/.env.staging index fd766ad..c2aa6e7 100644 --- a/.env.staging +++ b/.env.staging @@ -3,9 +3,9 @@ ONET_USERNAME=aptivaai ONET_PASSWORD=2296ahq # ─── Public‐facing React build ─────────── -NODE_ENV=staging -REACT_APP_ENV=production -APTIVA_API_BASE=https://staging.aptivaai.com/api +NODE_ENV=production +REACT_APP_ENV=staging +APTIVA_API_BASE=https://staging.aptivaai.com REACT_APP_API_URL=${APTIVA_API_BASE} REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20 REACT_APP_BLS_API_KEY=80d7a65a809a43f3a306a41ec874d231 @@ -15,7 +15,7 @@ REACT_APP_OPENAI_API_KEY=sk-proj-IyBOKc2T9RyViN_WBZwnjNCwUiRDBekmrghpHTKyf6OsqWx OPENAI_API_KEY=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 GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20 COLLEGE_SCORECARD_KEY=BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj -SALARY_DB=/home/jcoakley/aptiva-dev1-app/salary_info.db +SALARY_DB=/salary_info.db # ─── Database (premium server) ─────────── DB_HOST=34.67.180.54 diff --git a/Dockerfile.server b/Dockerfile.server index 78d286f..14429c4 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -1,5 +1,5 @@ ARG APPPORT=5000 -FROM --platform=$TARGETPLATFORM node:20-slim +FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN apt-get update -y && apt-get install -y --no-install-recommends build-essential python3 make g++ && rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile.server2 b/Dockerfile.server2 index 7ea6540..fcff8f3 100644 --- a/Dockerfile.server2 +++ b/Dockerfile.server2 @@ -1,10 +1,16 @@ -ARG APPPORT=5001 -FROM --platform=$TARGETPLATFORM node:20-slim +FROM node:20-bullseye AS base WORKDIR /app + +# ---- native build deps ---- +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + build-essential python3 pkg-config && \ + rm -rf /var/lib/apt/lists/* +# --------------------------- + COPY package*.json ./ -RUN npm ci --omit=dev --ignore-scripts -RUN apt-get update && apt-get install -y build-essential python3 make g++ sqlite3 && rm -rf /var/lib/apt/lists/* +RUN npm ci --unsafe-perm +COPY public/ /app/public/ COPY . . -ENV PORT=5001 -EXPOSE 5001 -CMD ["node","backend/server2.js"] + +CMD ["node", "backend/server2.js"] \ No newline at end of file diff --git a/Dockerfile.server3 b/Dockerfile.server3 index a563640..ca4ff10 100644 --- a/Dockerfile.server3 +++ b/Dockerfile.server3 @@ -1,5 +1,5 @@ ARG APPPORT=5002 -FROM --platform=$TARGETPLATFORM node:20-slim +FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN apt-get update -y \ diff --git a/backend/server2.js b/backend/server2.js index a82e831..915d63b 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -30,6 +30,21 @@ const env = process.env.NODE_ENV?.trim() || 'development'; const envPath = path.resolve(rootPath, `.env.${env}`); dotenv.config({ path: envPath }); // Load .env +const ROOT_DIR = path.resolve(__dirname, '..'); // repo root +const PUBLIC_DIR = path.join(ROOT_DIR, 'public'); // static json files +const CIP_TO_SOC_PATH = path.join(PUBLIC_DIR, 'CIP_to_ONET_SOC.xlsx'); +const INSTITUTION_DATA_PATH = path.join(PUBLIC_DIR, 'Institution_data.json'); +const SALARY_DB_PATH = path.join(ROOT_DIR, 'salary_info.db'); +const USER_PROFILE_DB_PATH = path.join(ROOT_DIR, 'user_profile.db'); + +for (const p of [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH, + SALARY_DB_PATH, USER_PROFILE_DB_PATH]) { + if (!fs.existsSync(p)) { + console.error(`FATAL Required data file not found → ${p}`); + process.exit(1); + } +} + const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const chatLimiter = rateLimit({ @@ -38,16 +53,8 @@ const chatLimiter = rateLimit({ keyGenerator: req => req.user?.id || req.ip }); -if (!process.env.APTIVA_API_BASE) { - console.error('FATAL APTIVA_API_BASE is not set'); - process.exit(1); -} - -// CIP->SOC mapping file -const mappingFilePath = '/home/jcoakley/aptiva-dev1-app/public/CIP_to_ONET_SOC.xlsx'; - // Institution data -const institutionFilePath = 'home/jcoakley/aptiva-dev1-app/public/Institution_data.json'; +const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8')); // Create Express app const app = express(); @@ -66,13 +73,14 @@ let userProfileDb; async function initDatabases() { try { db = await open({ - filename: '/home/jcoakley/aptiva-dev1-app/salary_info.db', - driver : sqlite3.Database + filename: SALARY_DB_PATH, + driver : sqlite3.Database, + mode : sqlite3.OPEN_READONLY }); console.log('✅ Connected to salary_info.db'); userProfileDb = await open({ - filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db', + filename: USER_PROFILE_DB_PATH, driver : sqlite3.Database }); console.log('✅ Connected to user_profile.db'); @@ -164,14 +172,9 @@ app.use((req, res, next) => next()); * Load CIP->SOC mapping **************************************************/ function loadMapping() { - try { - const workbook = xlsx.readFile(mappingFilePath); - const sheet = workbook.Sheets[workbook.SheetNames[0]]; - return xlsx.utils.sheet_to_json(sheet); - } catch (error) { - console.error('Error reading CIP_to_ONET_SOC:', error); - return []; - } + const wb = xlsx.readFile(CIP_TO_SOC_PATH); + const sheet= wb.Sheets[wb.SheetNames[0]]; + return xlsx.utils.sheet_to_json(sheet); // socToCipMapping array } const socToCipMapping = loadMapping(); if (socToCipMapping.length === 0) { @@ -404,7 +407,7 @@ app.post('/api/onet/submit_answers', async (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (token) { try { - await axios.post(`${process.env.APTIVA_API_BASE}/api/user-profile`, + await axios.post('/api/user-profile', { interest_inventory_answers: answers, riasec: riasecCode @@ -579,8 +582,7 @@ app.get('/api/schools', (req, res) => { // 3) Load your raw schools data let schoolsData = []; try { - const rawData = fs.readFileSync(institutionFilePath, 'utf8'); - schoolsData = JSON.parse(rawData); + schoolsData = institutionData; } catch (err) { console.error('Error parsing institution data:', err.message); return res.status(500).json({ error: 'Failed to load schools data.' }); @@ -633,8 +635,7 @@ app.get('/api/tuition', (req, res) => { } try { - const raw = fs.readFileSync(institutionFilePath, 'utf8'); - const schoolsData = JSON.parse(raw); + schoolsData = institutionData; const cipArray = cipCodes .split(',') diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index da1a950..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - server1: { env_file: ./env/dev.env } - server2: { env_file: ./env/dev.env } - server3: { env_file: ./env/dev.env } - nginx : { env_file: ./env/dev.env } \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 7712cd4..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - server1: { env_file: ./env/prod.env } - server2: { env_file: ./env/prod.env } - server3: { env_file: ./env/prod.env } - nginx : { env_file: ./env/prod.env } \ No newline at end of file diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml deleted file mode 100644 index c324693..0000000 --- a/docker-compose.staging.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - server1: { env_file: ./env/staging.env } - server2: { env_file: ./env/staging.env } - server3: { env_file: ./env/staging.env } - nginx : { env_file: ./env/staging.env } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 005f9bb..3180b07 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,13 @@ +x-env: &with-env + env_file: + - ${RUNTIME_ENV_FILE:-.env.production} # default for local runs + restart: unless-stopped + services: server1: + <<: *with-env image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG} expose: ["${SERVER1_PORT}"] - restart: unless-stopped healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"] interval: 30s @@ -13,6 +18,12 @@ services: image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:${IMG_TAG} expose: ["${SERVER2_PORT}"] restart: unless-stopped + env_file: + - ${RUNTIME_ENV_FILE} + volumes: + - ./public:/app/public:ro + - ./salary_info.db:/app/salary_info.db:ro + - ./user_profile.db:/app/user_profile.db healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:${SERVER2_PORT}/healthz || exit 1"] interval: 30s @@ -20,9 +31,9 @@ services: retries: 3 server3: + <<: *with-env image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:${IMG_TAG} expose: ["${SERVER3_PORT}"] - restart: unless-stopped healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:${SERVER3_PORT}/healthz || exit 1"] interval: 30s @@ -30,8 +41,10 @@ services: retries: 3 nginx: + <<: *with-env image: nginx:1.25-alpine - command: ["nginx","-g","daemon off;"] + command: ["nginx", "-g", "daemon off;"] + depends_on: [server1, server2, server3] ports: - "80:80" - "443:443" @@ -40,4 +53,3 @@ services: - ./nginx.conf:/etc/nginx/nginx.conf:ro - /etc/letsencrypt:/etc/letsencrypt:ro - ./empty:/etc/nginx/conf.d - depends_on: [server1, server2, server3] diff --git a/src/App.js b/src/App.js index cb7bafe..671103f 100644 --- a/src/App.js +++ b/src/App.js @@ -38,7 +38,6 @@ import ChatCtx from './contexts/ChatCtx.js'; export const ProfileCtx = React.createContext(); -const apiUrl = process.env.REACT_APP_API_URL || ''; function App() { @@ -131,7 +130,7 @@ const uiToolHandlers = useMemo(() => { } // If we have a token, validate it by fetching user - fetch(`${apiUrl}/user-profile`, { + fetch('/api/user-profile', { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => { diff --git a/src/components/CareerExplorer.js b/src/components/CareerExplorer.js index 0489f8a..a952235 100644 --- a/src/components/CareerExplorer.js +++ b/src/components/CareerExplorer.js @@ -58,7 +58,6 @@ function getFullStateName(code) { function CareerExplorer() { const navigate = useNavigate(); const location = useLocation(); - const apiUrl = process.env.REACT_APP_API_URL || ''; // ---------- Component States ---------- const [userProfile, setUserProfile] = useState(null); @@ -170,7 +169,7 @@ function CareerExplorer() { setProgress(0); // 1) O*NET answers -> initial career list - const submitRes = await axios.post(`${apiUrl}/onet/submit_answers`, { + const submitRes = await axios.post('/api/onet/submit_answers', { answers, state: profileData.state, area: profileData.area, @@ -204,7 +203,7 @@ function CareerExplorer() { // 2) job zones (one call for all SOC codes) const socCodes = flattened.map((c) => c.code); - const zonesRes = await axios.post(`${apiUrl}/job-zones`, { socCodes }).catch(() => null); + const zonesRes = await axios.post('/api/job-zones', { socCodes }).catch(() => null); // increment progress for this single request increment(); @@ -215,9 +214,9 @@ function CareerExplorer() { const strippedSoc = career.code.split('.')[0]; // build URLs - const cipUrl = `${apiUrl}/cip/${career.code}`; - const jobDetailsUrl = `${apiUrl}/onet/career-description/${career.code}`; - const economicUrl = `${apiUrl}/projections/${strippedSoc}`; + const cipUrl = `/api/cip/${career.code}`; + const jobDetailsUrl = `/api/onet/career-description/${career.code}`; + const economicUrl = `/api/projections/${strippedSoc}`; const salaryParams = { socCode: strippedSoc, area: profileData.area }; // We'll fetch them in parallel with our custom fetchWithProgress: @@ -225,7 +224,7 @@ function CareerExplorer() { fetchWithProgress(cipUrl), fetchWithProgress(jobDetailsUrl), fetchWithProgress(economicUrl), - fetchWithProgress(`${apiUrl}/salary`, salaryParams), + fetchWithProgress('/api/salary', salaryParams), ]); // parse data @@ -292,7 +291,7 @@ function CareerExplorer() { const fetchUserProfile = async () => { try { const token = localStorage.getItem('token'); - const res = await axios.get(`${apiUrl}/user-profile`, { + const res = await axios.get('/api/user-profile', { headers: { Authorization: `Bearer ${token}` }, }); @@ -323,7 +322,7 @@ function CareerExplorer() { }; fetchUserProfile(); - }, [apiUrl]); + }, []); // ------------------------------------------------------ // If user came from Interest Inventory => auto-fetch @@ -363,7 +362,7 @@ function CareerExplorer() { try { // 1) CIP fetch - const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); + const cipResponse = await fetch(`/api/cip/${socCode}`); if (!cipResponse.ok) { setError( `We're sorry, but specific details for "${career.title}" are not available at this time.` @@ -379,7 +378,7 @@ function CareerExplorer() { // 2) Job details (description + tasks) const jobDetailsResponse = await fetch( - `${apiUrl}/onet/career-description/${socCode}` + `/api/onet/career-description/${socCode}` ); if (!jobDetailsResponse.ok) { setCareerDetails({ @@ -393,7 +392,7 @@ function CareerExplorer() { // 3) Salary data let salaryResponse; try { - salaryResponse = await axios.get(`${apiUrl}/salary`, { + salaryResponse = await axios.get('/api/salary', { params: { socCode: socCode.split('.')[0], area: areaTitle }, }); } catch (error) { @@ -437,7 +436,7 @@ function CareerExplorer() { let economicResponse = { data: {} }; try { economicResponse = await axios.get( - `${apiUrl}/projections/${socCode.split('.')[0]}`, + `/api/projections/${socCode.split('.')[0]}`, { params: { state: fullStateName }, } @@ -457,13 +456,13 @@ function CareerExplorer() { try { // Check local DB first (SQLite -> server2) - const localRiskRes = await axios.get(`${apiUrl}/ai-risk/${socCode}`); + const localRiskRes = await axios.get(`/api/ai-risk/${socCode}`); aiRisk = localRiskRes.data; } catch (err) { // If 404, we call server3's ChatGPT route at the SAME base url if (err.response && err.response.status === 404) { try { - const aiRes = await axios.post(`${apiUrl}/public/ai-risk-analysis`, { + const aiRes = await axios.post('/api/public/ai-risk-analysis', { socCode, careerName: career.title, jobDescription: description, @@ -473,7 +472,7 @@ function CareerExplorer() { const { riskLevel, reasoning } = aiRes.data; // store it back in server2 to avoid repeated GPT calls - await axios.post(`${apiUrl}/ai-risk`, { + await axios.post('/api/ai-risk', { socCode, careerName: aiRes.data.careerName, jobDescription: aiRes.data.jobDescription, @@ -521,7 +520,7 @@ function CareerExplorer() { setLoading(false); } }, - [userState, apiUrl, areaTitle] + [userState, areaTitle] ); // ------------------------------------------------------ @@ -644,7 +643,7 @@ useEffect(() => { try { const token = localStorage.getItem('token'); await axios.post( - `${apiUrl}/user-profile`, + '/api/user-profile', { firstName: userProfile?.firstname, lastName: userProfile?.lastname, diff --git a/src/components/CareerModal.js b/src/components/CareerModal.js index 76cbc38..edf0504 100644 --- a/src/components/CareerModal.js +++ b/src/components/CareerModal.js @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; -const apiUrl = process.env.REACT_APP_API_URL; function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js index 3ba6879..686b41c 100644 --- a/src/components/CareerRoadmap.js +++ b/src/components/CareerRoadmap.js @@ -38,10 +38,6 @@ import InfoTooltip from "./ui/infoTooltip.js"; import differenceInMonths from 'date-fns/differenceInMonths'; import "../styles/legacy/MilestoneTimeline.legacy.css"; -const apiUrl = process.env.REACT_APP_API_URL || ''; - - - // -------------- // Register ChartJS Plugins @@ -339,7 +335,6 @@ function getYearsInCareer(startDateString) { export default function CareerRoadmap({ selectedCareer: initialCareer }) { const { careerId } = useParams(); const location = useLocation(); - const apiURL = process.env.REACT_APP_API_URL; const [interestStrategy, setInterestStrategy] = useState('FLAT'); // 'NONE' | 'FLAT' | 'MONTE_CARLO' const [flatAnnualRate, setFlatAnnualRate] = useState(0.06); @@ -398,7 +393,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) { const reloadScenarioAndCollege = useCallback(async () => { if (!careerProfileId) return; const s = await authFetch( - `${apiURL}/premium/career-profile/${careerProfileId}` + `api/premium/career-profile/${careerProfileId}` ); if (s.ok) { const row = await s.json(); @@ -408,10 +403,10 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) { } const c = await authFetch( - `${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}` + `api/premium/college-profile?careerProfileId=${careerProfileId}` ); if (c.ok) setCollegeProfile(await c.json()); - }, [careerProfileId, apiURL]); + }, [careerProfileId]); const milestoneGroups = useMemo(() => { if (!scenarioMilestones.length) return []; @@ -514,10 +509,10 @@ useEffect(() => { const up = await authFetch('/api/user-profile'); if (up.ok) setUserProfile(await up.json()); - const fp = await authFetch(`${apiURL}/premium/financial-profile`); + const fp = await authFetch('api/premium/financial-profile'); if (fp.ok) setFinancialProfile(await fp.json()); })(); -}, [apiURL]); +}, []); /* quick derived helpers */ const userSalary = parseFloatOrZero(financialProfile?.current_salary); @@ -687,7 +682,7 @@ useEffect(() => { (async function init () { /* 1 ▸ get every row the user owns */ - const r = await authFetch(`${apiURL}/premium/career-profile/all`); + const r = await authFetch('api/premium/career-profile/all'); if (!r?.ok || cancelled) return; const { careerProfiles=[] } = await r.json(); setExistingCareerProfiles(careerProfiles); @@ -771,9 +766,9 @@ useEffect(() => { const refetchScenario = useCallback(async () => { if (!careerProfileId) return; - const r = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`); + const r = await authFetch('api/premium/career-profile/${careerProfileId}'); if (r.ok) setScenarioRow(await r.json()); -}, [careerProfileId, apiURL]); +}, [careerProfileId]); // 5) from scenarioRow => find the full SOC => strip useEffect(() => { @@ -822,14 +817,14 @@ async function fetchAiRisk(socCode, careerName, description, tasks) { try { // 1) Check server2 for existing entry - const localRiskRes = await axios.get(`${apiUrl}/ai-risk/${socCode}`); + const localRiskRes = await axios.get('api/ai-risk/${socCode}'); aiRisk = localRiskRes.data; // { socCode, riskLevel, ... } } catch (err) { // 2) If 404 => call server3 if (err.response && err.response.status === 404) { try { // Call GPT via server3 - const aiRes = await axios.post(`${apiUrl}/public/ai-risk-analysis`, { + const aiRes = await axios.post('api/public/ai-risk-analysis', { socCode, careerName, jobDescription: description, @@ -863,7 +858,7 @@ try { } // 3) Store in server2 - await axios.post(`${apiUrl}/ai-risk`, storePayload); + await axios.post('api/ai-risk', storePayload); // Construct final object for usage here aiRisk = { @@ -900,7 +895,7 @@ useEffect(() => { (async () => { try { const qs = new URLSearchParams({ socCode: strippedSocCode, area: userArea }); - const res = await fetch(`${apiURL}/salary?${qs}`, { signal: ctrl.signal }); + const res = await fetch('api/salary?${qs}', { signal: ctrl.signal }); if (res.ok) { setSalaryData(await res.json()); @@ -916,7 +911,7 @@ useEffect(() => { // cancel if strippedSocCode / userArea changes before the fetch ends return () => ctrl.abort(); -}, [strippedSocCode, userArea, apiURL]); +}, [strippedSocCode, userArea]); /* 7) Economic Projections ---------------------------------------- */ useEffect(() => { @@ -932,7 +927,7 @@ useEffect(() => { try { const qs = new URLSearchParams({ state: userState }); const res = await authFetch( - `${apiURL}/projections/${strippedSocCode}?${qs}`, + `api/projections/${strippedSocCode}?${qs}`, { signal: ctrl.signal } ); @@ -949,7 +944,7 @@ useEffect(() => { })(); return () => ctrl.abort(); -}, [strippedSocCode, userState, apiURL]); +}, [strippedSocCode, userState]); // 8) Build financial projection async function buildProjection(milestones) { @@ -960,7 +955,7 @@ useEffect(() => { // fetch impacts const imPromises = allMilestones.map((m) => - authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`) + authFetch(`api/premium/milestone-impacts?milestone_id=${m.id}`) .then((r) => (r.ok ? r.json() : null)) .then((dd) => dd?.impacts || []) .catch((e) => { @@ -1280,8 +1275,8 @@ const fetchMilestones = useCallback(async () => { if (!careerProfileId) return; const [profRes, uniRes] = await Promise.all([ - authFetch(`${apiURL}/premium/milestones?careerProfileId=${careerProfileId}`), - authFetch(`${apiURL}/premium/milestones?careerProfileId=universal`) + authFetch(`api/premium/milestones?careerProfileId=${careerProfileId}`), + authFetch(`api/premium/milestones?careerProfileId=universal`) ]); if (!profRes.ok || !uniRes.ok) return; @@ -1293,7 +1288,7 @@ const fetchMilestones = useCallback(async () => { if (financialProfile && scenarioRow && collegeProfile) { buildProjection(merged); } // single rebuild -}, [financialProfile, scenarioRow, careerProfileId, apiURL]); // ← NOTICE: no buildProjection here +}, [financialProfile, scenarioRow, careerProfileId]); // ← NOTICE: no buildProjection here return ( @@ -1543,7 +1538,6 @@ const fetchMilestones = useCallback(async () => { setFinancialProfile={setFinancialProfile} collegeProfile={collegeProfile} setCollegeProfile={setCollegeProfile} - apiURL={apiURL} authFetch={authFetch} /> diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index 5877ceb..4e1aded 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -159,7 +159,6 @@ function Dashboard() { const popoutVisible = !!selectedCareer; // ============= Auth & URL Setup ============= - const apiUrl = process.env.REACT_APP_API_URL || ''; // AUTH fetch const authFetch = async (url, options = {}, onUnauthorized) => { @@ -194,7 +193,7 @@ function Dashboard() { // ============= Fetch user profile ============= const fetchUserProfile = async () => { - const res = await authFetch(`${apiUrl}/user-profile`); + const res = await authFetch('api/user-profile'); if (!res) return; if (res.ok) { @@ -215,7 +214,7 @@ function Dashboard() { // ============= Lifecycle: Load Profile ============= useEffect(() => { fetchUserProfile(); - }, [apiUrl]); // load once + }, []); // load once // ============= jobZone & Fit Setup ============= const jobZoneLabels = { @@ -237,7 +236,7 @@ function Dashboard() { try { setLoading(true); setProgress(0); - const response = await authFetch(`${apiUrl}/onet/submit_answers`, { + const response = await authFetch('api/onet/submit_answers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ answers }), @@ -294,7 +293,7 @@ function Dashboard() { if (careerSuggestions.length === 0) return; const socCodes = careerSuggestions.map((career) => career.code); try { - const response = await axios.post(`${apiUrl}/job-zones`, { socCodes }); + const response = await axios.post('api/job-zones', { socCodes }); const jobZoneData = response.data; const updatedCareers = careerSuggestions.map((career) => ({ ...career, @@ -306,7 +305,7 @@ function Dashboard() { } }; fetchJobZones(); - }, [careerSuggestions, apiUrl]); + }, [careerSuggestions]); // ============= Filter by job zone, fit ============= const filteredCareers = useMemo(() => { @@ -382,20 +381,20 @@ function Dashboard() { try { // CIP fetch - const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); + const cipResponse = await fetch(`api/cip/${socCode}`); if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code'); const { cipCode } = await cipResponse.json(); const cleanedCipCode = cipCode.replace('.', '').slice(0, 4); // Job details - const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`); + const jobDetailsResponse = await fetch(`api/onet/career-description/${socCode}`); if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description'); const { description, tasks } = await jobDetailsResponse.json(); // Salary let salaryResponse; try { - salaryResponse = await axios.get(`${apiUrl}/salary`, { + salaryResponse = await axios.get('api/salary', { params: { socCode: socCode.split('.')[0], area: areaTitle }, }); } catch (error) { @@ -407,7 +406,7 @@ function Dashboard() { // Economic let economicResponse; try { - economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, { + economicResponse = await axios.get(`api/projections/${socCode.split('.')[0]}`, { params: { state: fullName }, // e.g. "Kentucky" }); } catch (error) { @@ -417,7 +416,7 @@ function Dashboard() { // Tuition let tuitionResponse; try { - tuitionResponse = await axios.get(`${apiUrl}/tuition`, { + tuitionResponse = await axios.get('api/tuition', { params: { cipCode: cleanedCipCode, state: userState }, }); } catch (error) { @@ -515,7 +514,7 @@ function Dashboard() { setLoading(false); } }, - [userState, apiUrl, areaTitle, userZipcode, updateChatbotContext] + [userState, areaTitle, userZipcode, updateChatbotContext] ); // ============= Let typed careers open PopoutPanel ============= diff --git a/src/components/MilestoneAddModal.js b/src/components/MilestoneAddModal.js index 6a99095..4180b39 100644 --- a/src/components/MilestoneAddModal.js +++ b/src/components/MilestoneAddModal.js @@ -8,7 +8,6 @@ const MilestoneAddModal = ({ defaultScenarioId, scenarioId, // which scenario this milestone applies to editMilestone, // if editing an existing milestone, pass its data - apiURL }) => { // Basic milestone fields const [title, setTitle] = useState(''); @@ -74,7 +73,7 @@ const MilestoneAddModal = ({ if (editMilestone) { // 1) Update existing milestone milestoneId = editMilestone.id; - await authFetch(`${apiURL}/premium/milestones/${milestoneId}`, { + await authFetch(`api/premium/milestones/${milestoneId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -87,7 +86,7 @@ const MilestoneAddModal = ({ // Then handle impacts below... } else { // 1) Create new milestone - const res = await authFetch(`${apiURL}/premium/milestones`, { + const res = await authFetch('api/premium/milestones', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -105,7 +104,7 @@ const MilestoneAddModal = ({ // For simplicity, let's do multiple POST calls for (const impact of impacts) { // If editing, you might do a PUT if the impact already has an id - await authFetch(`${apiURL}/premium/milestone-impacts`, { + await authFetch('api/premium/milestone-impacts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -117,8 +116,8 @@ const MilestoneAddModal = ({ end_month: impact.end_month !== null ? parseInt(impact.end_month, 10) : null, - created_at: new Date()..toISOString().slice(0, 10), - updated_at: new Date()..toISOString().slice(0, 10) + created_at: new Date().toISOString().slice(0, 10), + updated_at: new Date().toISOString().slice(0, 10) }) }); } diff --git a/src/components/PremiumOnboarding/CareerOnboarding.js b/src/components/PremiumOnboarding/CareerOnboarding.js index 504d8f8..4944a3a 100644 --- a/src/components/PremiumOnboarding/CareerOnboarding.js +++ b/src/components/PremiumOnboarding/CareerOnboarding.js @@ -8,8 +8,6 @@ import authFetch from '../../utils/authFetch.js'; // 1) Import your CareerSearch component import CareerSearch from '../CareerSearch.js'; // adjust path as necessary -const apiURL = process.env.REACT_APP_API_URL; - const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => { // We store local state for “are you working,” “selectedCareer,” etc. const [currentlyWorking, setCurrentlyWorking] = useState(''); diff --git a/src/components/SignIn.js b/src/components/SignIn.js index 51b5494..97b6ea2 100644 --- a/src/components/SignIn.js +++ b/src/components/SignIn.js @@ -10,7 +10,6 @@ function SignIn({ setIsAuthenticated, setUser }) { const [error, setError] = useState(''); const [showSessionExpiredMsg, setShowSessionExpiredMsg] = useState(false); const location = useLocation(); - const apiUrl = process.env.REACT_APP_API_URL; useEffect(() => { // Check if the URL query param has ?session=expired @@ -43,7 +42,7 @@ function SignIn({ setIsAuthenticated, setUser }) { } try { - const resp = await fetch(`${apiUrl}/signin`, { + const resp = await fetch('api/signin', { method : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify({username, password}), diff --git a/src/utils/apiUtils.js b/src/utils/apiUtils.js index 6ffa7fe..6e1b072 100644 --- a/src/utils/apiUtils.js +++ b/src/utils/apiUtils.js @@ -49,7 +49,6 @@ export function haversineDistance(lat1, lon1, lat2, lon2) { export async function fetchSchools(cipCodes) { try { - const apiUrl = process.env.REACT_APP_API_URL || ''; // 1) If `cipCodes` is a single string => wrap in array let codesArray = Array.isArray(cipCodes) ? cipCodes : [cipCodes]; @@ -59,7 +58,7 @@ export async function fetchSchools(cipCodes) { const cipParam = codesArray.join(','); // 3) Call your endpoint with `?cipCodes=1101,1409&state=NY` - const response = await axios.get(`${apiUrl}/schools`, { + const response = await axios.get('api/schools', { params: { cipCodes: cipParam, }, diff --git a/src/utils/fetchCareerEnrichment.js b/src/utils/fetchCareerEnrichment.js index 5b29cf2..17a07eb 100644 --- a/src/utils/fetchCareerEnrichment.js +++ b/src/utils/fetchCareerEnrichment.js @@ -2,15 +2,15 @@ import axios from 'axios'; -export async function fetchCareerEnrichment(apiUrl, socCode, area) { +export async function fetchCareerEnrichment(socCode, area) { // strippedSoc = remove decimals from e.g. "15-1132.00" => "15-1132" const strippedSoc = socCode.includes('.') ? socCode.split('.')[0] : socCode; const [cipData, jobDetailsData, economicData, salaryData] = await Promise.all([ - axios.get(`${apiUrl}/cip/${socCode}`).catch(() => null), - axios.get(`${apiUrl}/onet/career-description/${socCode}`).catch(() => null), - axios.get(`${apiUrl}/projections/${strippedSoc}`, { params: { area } }).catch(() => null), - axios.get(`${apiUrl}/salary`, { params: { socCode: strippedSoc, area } }).catch(() => null), + axios.get(`api/cip/${socCode}`).catch(() => null), + axios.get(`api/onet/career-description/${socCode}`).catch(() => null), + axios.get(`api/projections/${strippedSoc}`, { params: { area } }).catch(() => null), + axios.get('api/salary', { params: { socCode: strippedSoc, area } }).catch(() => null), ]); return { diff --git a/src/utils/handleCareerClick.js b/src/utils/handleCareerClick.js index eebec6c..8ea6af8 100644 --- a/src/utils/handleCareerClick.js +++ b/src/utils/handleCareerClick.js @@ -21,20 +21,20 @@ try { // CIP fetch - const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); + const cipResponse = await fetch(`api/cip/${socCode}`); if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code'); const { cipCode } = await cipResponse.json(); const cleanedCipCode = cipCode.replace('.', '').slice(0, 4); // Job details - const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`); + const jobDetailsResponse = await fetch(`api/onet/career-description/${socCode}`); if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description'); const { description, tasks } = await jobDetailsResponse.json(); // Salary let salaryResponse; try { - salaryResponse = await axios.get(`${apiUrl}/salary`, { + salaryResponse = await axios.get('api/salary', { params: { socCode: socCode.split('.')[0], area: areaTitle }, }); } catch (error) { @@ -46,7 +46,7 @@ // Economic let economicResponse; try { - economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, { + economicResponse = await axios.get(`api/projections/${socCode.split('.')[0]}`, { params: { state: fullName }, // e.g. "Kentucky" }); } catch (error) { @@ -56,7 +56,7 @@ // Tuition let tuitionResponse; try { - tuitionResponse = await axios.get(`${apiUrl}/tuition`, { + tuitionResponse = await axios.get('api/tuition', { params: { cipCode: cleanedCipCode, state: userState }, }); } catch (error) { @@ -154,5 +154,5 @@ setLoading(false); } }, - [userState, apiUrl, areaTitle, userZipcode, updateChatbotContext] + [userState, areaTitle, userZipcode, updateChatbotContext] ); \ No newline at end of file