From b5268a0fe8d955ca105aa492baf4110749c490d0 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 16 Jul 2025 14:32:50 +0000 Subject: [PATCH] env file and yml file consistency --- .env.development | 38 +++++++++++++++------ .env.staging | 42 +++++++++++++++++++++++ backend/server.js | 26 +++++++++----- backend/server2.js | 69 +++++++++++++++++++++++++------------- backend/server3.js | 54 +++++++++++++++++++++++++---- docker-compose.dev.yml | 5 +++ docker-compose.prod.yml | 45 +++---------------------- docker-compose.staging.yml | 42 +++-------------------- docker-compose.yml | 41 ++++++++++------------ src/components/SignIn.js | 5 +-- 10 files changed, 213 insertions(+), 154 deletions(-) create mode 100644 .env.staging create mode 100644 docker-compose.dev.yml diff --git a/.env.development b/.env.development index 9037b4e..b2ebdd7 100755 --- a/.env.development +++ b/.env.development @@ -1,26 +1,42 @@ +# ─── O*NET ─────────────────────────────── ONET_USERNAME=aptivaai ONET_PASSWORD=2296ahq -REACT_APP_BLS_API_KEY=80d7a65a809a43f3a306a41ec874d231 -GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20 +# ─── Public‐facing React build ─────────── +NODE_ENV=development +REACT_APP_ENV=production +APTIVA_API_BASE=https://dev1.aptivaai.com/api +REACT_APP_API_URL=${APTIVA_API_BASE} REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCTMgjiHUF2Vl3QriQu2kDEuZWz39ZAR20 -COLLEGE_SCORECARD_KEY = BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj +REACT_APP_BLS_API_KEY=80d7a65a809a43f3a306a41ec874d231 +REACT_APP_OPENAI_API_KEY=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 +# ─── Back-end services ─────────────────── +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 + +# ─── Database (premium server) ─────────── DB_HOST=34.67.180.54 DB_PORT=3306 DB_USER=sqluser -DB_NAME=user_profile_db DB_PASSWORD=ps { }); const app = express(); -const PORT = 5000; +const PORT = process.env.SERVER1_PORT || 5000; + +/* ─── Require critical env vars ───────────────────────────────── */ +if (!process.env.CORS_ALLOWED_ORIGINS) { + console.error('FATAL CORS_ALLOWED_ORIGINS is not set'); // eslint-disable-line + process.exit(1); +} +if (!process.env.APTIVA_API_BASE) { + console.error('FATAL APTIVA_API_BASE is not set'); // eslint-disable-line + process.exit(1); +} + +/* ─── Allowed origins for CORS (comma-separated in env) ──────── */ +const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS + .split(',') + .map(o => o.trim()) + .filter(Boolean); -// Allowed origins for CORS -const allowedOrigins = [ - 'http://localhost:3000', - 'http://34.16.120.118:3000', - 'https://dev1.aptivaai.com', -]; app.disable('x-powered-by'); app.use(bodyParser.json()); @@ -101,7 +111,7 @@ app.use( // Handle preflight requests explicitly app.options('*', (req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Origin', req.headers.origin || ''); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader( 'Access-Control-Allow-Headers', diff --git a/backend/server2.js b/backend/server2.js index d6374d0..a82e831 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -38,22 +38,20 @@ const chatLimiter = rateLimit({ keyGenerator: req => req.user?.id || req.ip }); -// Whitelist CORS -const allowedOrigins = [ - 'http://localhost:3000', - 'http://34.16.120.118:3000', - 'https://dev1.aptivaai.com', -]; +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 = path.resolve(rootPath, 'public', 'Institution_data.json'); +const institutionFilePath = 'home/jcoakley/aptiva-dev1-app/public/Institution_data.json'; // Create Express app const app = express(); -const PORT = process.env.PORT || 5001; +const PORT = process.env.SERVER2_PORT || 5001; // at top of backend/server.js (do once per server codebase) app.get('/healthz', (req, res) => res.sendStatus(204)); // 204 No Content @@ -86,9 +84,23 @@ async function initDatabases() { await initDatabases(); -/************************************************** - * Security, CORS, JSON Body - **************************************************/ +/* ────────────────────────────────────────────────────────────── + * SECURITY, CORS, JSON Body + * ────────────────────────────────────────────────────────────── */ + +/* 1 — Require critical env var up-front */ +if (!process.env.CORS_ALLOWED_ORIGINS) { + console.error('FATAL CORS_ALLOWED_ORIGINS is not set'); + process.exit(1); +} + +/* 2 — Build allow-list from env (comma-separated) */ +const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS + .split(',') + .map(o => o.trim()) + .filter(Boolean); + +/* 3 — Security headers */ app.use( helmet({ contentSecurityPolicy: false, @@ -96,8 +108,11 @@ app.use( }) ); +/* 4 — Dynamic CORS / pre-flight handling */ app.use((req, res, next) => { const origin = req.headers.origin; + + /* 4a — Whitelisted origins (credentials allowed) */ if (origin && allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Credentials', 'true'); @@ -105,17 +120,22 @@ app.use((req, res, next) => { 'Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With, Access-Control-Allow-Methods' ); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader( + 'Access-Control-Allow-Methods', + 'GET, POST, OPTIONS' + ); + + /* 4b — Public JSON exception */ } else if (req.path.includes('Institution_data')) { - // For that JSON res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); res.setHeader( 'Access-Control-Allow-Headers', 'Content-Type, Accept, Origin, X-Requested-With' ); + + /* 4c — Default permissive fallback (same as your original) */ } else { - // default res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader( @@ -123,21 +143,22 @@ app.use((req, res, next) => { 'Authorization, Content-Type, Accept, Origin, X-Requested-With' ); } + + /* 4d — Short-circuit pre-flight requests */ if (req.method === 'OPTIONS') { - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type'); - return res.status(204).end(); + res.status(204).end(); + return; } + next(); }); +/* 5 — JSON parsing & static assets */ app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); -// For completeness -app.use((req, res, next) => { - next(); -}); +/* 6 — No-op pass-through (kept for completeness) */ +app.use((req, res, next) => next()); /************************************************** * Load CIP->SOC mapping @@ -383,7 +404,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.MAIN_API_URL}/api/user-profile`, + await axios.post(`${process.env.APTIVA_API_BASE}/api/user-profile`, { interest_inventory_answers: answers, riasec: riasecCode @@ -454,7 +475,7 @@ app.get('/api/onet/career-details/:socCode', async (req, res) => { try { const response = await axios.get(`https://services.onetcenter.org/ws/mnm/careers/${socCode}`, { auth: { - username: process.env.ONet_USERNAME, + username: process.env.ONET_USERNAME, password: process.env.ONET_PASSWORD, }, headers: { Accept: 'application/json' }, @@ -1000,5 +1021,5 @@ chatFreeEndpoint(app, { * Start the Express server **************************************************/ app.listen(PORT, () => { - console.log(`Server running on https://34.16.120.118:${PORT}`); + console.log(`Server running on port ${PORT}`); }); diff --git a/backend/server3.js b/backend/server3.js index a2c5b19..4cce5f6 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -7,7 +7,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); import express from 'express'; -import cors from 'cors'; import helmet from 'helmet'; import fs from 'fs/promises'; import multer from 'multer'; @@ -30,7 +29,7 @@ dotenv.config({ path: envPath }); const apiBase = process.env.APTIVA_API_BASE || "http://localhost:5002/api"; const app = express(); -const PORT = process.env.PREMIUM_PORT || 5002; +const PORT = process.env.SERVER3_PORT || 5002; const { getDocument } = pkg; const bt = "`".repeat(3); @@ -45,14 +44,55 @@ function internalFetch(req, url, opts = {}) { }); } - - // 2) Basic middlewares -app.use(helmet()); +app.use(helmet({ contentSecurityPolicy:false, crossOriginEmbedderPolicy:false })); app.use(express.json({ limit: '5mb' })); -const allowedOrigins = ['https://dev1.aptivaai.com']; -app.use(cors({ origin: allowedOrigins, credentials: true })); +/* ─── Require critical env vars ─────────────────────────────── */ +if (!process.env.CORS_ALLOWED_ORIGINS) { + console.error('FATAL CORS_ALLOWED_ORIGINS is not set'); + process.exit(1); +} +if (!process.env.APTIVA_API_BASE) { + console.error('FATAL APTIVA_API_BASE is not set'); + process.exit(1); +} + +/* ─── Allowed origins for CORS (comma-separated in env) ─────── */ +const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS + .split(',') + .map(o => o.trim()) + .filter(Boolean); + + /* ─── Dynamic CORS middleware (matches server1 / server2) ────────────── */ +app.use((req, res, next) => { + const origin = req.headers.origin; + + // A) whitelisted origins (credentials allowed) + if (origin && allowedOrigins.includes(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Authorization, Content-Type, Accept, Origin, X-Requested-With, Access-Control-Allow-Methods' + ); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + + // B) default permissive fallback (same as server2’s behaviour) + } else { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Authorization, Content-Type, Accept, Origin, X-Requested-With' + ); + } + + if (req.method === 'OPTIONS') { + return res.status(204).end(); + } + next(); +}); // 3) Authentication middleware const authenticatePremiumUser = (req, res, next) => { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..d5fed1f --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,5 @@ +services: + server1: { env_file: .env.dev } + server2: { env_file: .env.dev } + server3: { env_file: .env.dev } + nginx : { env_file: .env.dev } \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 99b567e..d83a12e 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,42 +1,5 @@ services: - # ─── server1 ─── - server1: - extends: - file: docker-compose.yml - service: server1 - env_file: [ ./env/prod.env ] - environment: - - NODE_ENV=production - - SALARY_DB=/app/data/salary_info.db - volumes: - - /home/jcoakley/aptiva-dev1-app/salary_info.db:/app/data/salary_info.db:ro - - /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro - - # ─── server2 ─── - server2: - extends: - file: docker-compose.yml - service: server2 - env_file: [ ./env/prod.env ] - environment: - - NODE_ENV=production - - SALARY_DB=/home/jcoakley/aptiva-dev1-app/salary_info.db - volumes: - - /home/jcoakley/aptiva-dev1-app/salary_info.db:/home/jcoakley/aptiva-dev1-app/salary_info.db:ro - - /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db - - /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro - - # ─── server3 ─── - server3: - extends: - file: docker-compose.yml - service: server3 - env_file: [ ./env/prod.env ] - environment: - - NODE_ENV=production - - TWILIO_ACCOUNT_SID=ACd700c6fb9f691ccd9ccab73f2dd4173d - - TWILIO_AUTH_TOKEN=fb8979ccb172032a249014c9c30eba80 - - TWILIO_MESSAGING_SERVICE_SID=MGMGaa07992a9231c841b1bfb879649026d6 - volumes: - - /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro - - /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db \ No newline at end of file + server1: { env_file: .env.prod } + server2: { env_file: .env.prod } + server3: { env_file: .env.prod } + nginx : { env_file: .env.prod } \ No newline at end of file diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 6aeb041..053cc6f 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -1,39 +1,5 @@ services: -# ─── server1 ─── -server1: - extends: - file: docker-compose.yml - service: server1 - env_file: [ ./env/staging.env ] - environment: - - NODE_ENV=production - - SALARY_DB=/app/data/salary_info.db - volumes: - - /home/jcoakley/aptiva-dev1-app/salary_info.db:/app/data/salary_info.db:ro - - /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro - -# ─── server2 ─── -server2: - extends: - file: docker-compose.yml - service: server2 - env_file: [ ./env/staging.env ] - environment: - - NODE_ENV=production - - SALARY_DB=/home/jcoakley/aptiva-dev1-app/salary_info.db - volumes: - - /home/jcoakley/aptiva-dev1-app/salary_info.db:/home/jcoakley/aptiva-dev1-app/salary_info.db:ro - - /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro - - /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db - -# ─── server3 ─── -server3: - extends: - file: docker-compose.yml - service: server3 - env_file: [ ./env/staging.env ] - environment: - - NODE_ENV=production - volumes: - - /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro - - /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db \ No newline at end of file + server1: { env_file: .env.staging } + server2: { env_file: .env.staging } + server3: { env_file: .env.staging } + nginx : { env_file: .env.staging } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1628076..408dfa4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,50 +1,45 @@ +version: "3.9" + services: server1: - image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:prod-20250710 + image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG} + expose: ["${SERVER1_PORT}"] restart: unless-stopped - expose: ["5000"] healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:5000/healthz || exit 1"] + test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"] interval: 30s timeout: 5s retries: 3 server2: - image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:prod-20250710 + image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:${IMG_TAG} + expose: ["${SERVER2_PORT}"] restart: unless-stopped - expose: ["5001"] healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:5001/healthz || exit 1"] + test: ["CMD-SHELL", "curl -f http://localhost:${SERVER2_PORT}/healthz || exit 1"] interval: 30s timeout: 5s retries: 3 - server3: - build: - context: . - dockerfile: Dockerfile.server3 + server3: + image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:${IMG_TAG} + expose: ["${SERVER3_PORT}"] restart: unless-stopped - expose: ["5002"] - environment: - NODE_ENV: production - JWT_SECRET: gW4QsOu4AJA4MooIUC9ld2i71VbBovzV1INsaU6ftxYPrxLIeMq6/OY61j0X2RV7 - TWILIO_ACCOUNT_SID: ACd700c6fb9f691ccd9ccab73f2dd4173d - TWILIO_AUTH_TOKEN: fb8979ccb172032a249014c9c30eba80 - TWILIO_MESSAGING_SERVICE_SID: MGMGaa07992a9231c841b1bfb879649026d6 healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:5002/healthz || exit 1"] + test: ["CMD-SHELL", "curl -f http://localhost:${SERVER3_PORT}/healthz || exit 1"] interval: 30s timeout: 5s retries: 3 + nginx: image: nginx:1.25-alpine - command: ["nginx", "-g", "daemon off;"] + command: ["nginx","-g","daemon off;"] ports: - "80:80" - "443:443" volumes: - - ./build:/usr/share/nginx/html:ro # React build - - ./nginx.conf:/etc/nginx/nginx.conf:ro # overwrite default - - /etc/letsencrypt:/etc/letsencrypt:ro # certs - - ./empty:/etc/nginx/conf.d # hide default.conf + - ./build:/usr/share/nginx/html:ro + - ./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/components/SignIn.js b/src/components/SignIn.js index 49caf89..1f5d2b9 100644 --- a/src/components/SignIn.js +++ b/src/components/SignIn.js @@ -10,6 +10,7 @@ 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 @@ -42,10 +43,10 @@ function SignIn({ setIsAuthenticated, setUser }) { } try { - const resp = await fetch('https://dev1.aptivaai.com/api/signin', { + const resp = await fetch(`${apiUrl}/signin`, { method : 'POST', headers: { 'Content-Type': 'application/json' }, - body : JSON.stringify({ username, password }) + body : JSON.stringify(formData), }); const data = await resp.json(); // ← read ONCE