Compare commits
No commits in common. "a8c5ed828b91dbfa8d1d21421ea616a331550ac3" and "8c52e29e34d1da0831e1cccc8dae02f74bff983b" have entirely different histories.
a8c5ed828b
...
8c52e29e34
@ -1 +1 @@
|
||||
e0de79c21e9b87f23a4da67149cea4e0e979e9e0-803b2c2ecad09a0fbca070296808a53489de891a-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
2aa2de355546f6401f80a07e00066bd83f37fdc6-803b2c2ecad09a0fbca070296808a53489de891a-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
|
@ -39,6 +39,7 @@ 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');
|
||||
const API_BASE = (process.env.APTIVA_API_BASE || 'http://server1:5000').replace(/\/+$/,'');
|
||||
|
||||
for (const p of [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH, SALARY_DB_PATH, USER_PROFILE_DB_PATH]) {
|
||||
if (!fs.existsSync(p)) {
|
||||
@ -1252,13 +1253,11 @@ ${body}`;
|
||||
}
|
||||
);
|
||||
|
||||
/* ----------------- Support bot chat (server2) ----------------- */
|
||||
|
||||
/* CREATE thread */
|
||||
app.post('/api/chat/threads', authenticateUser, async (req, res) => {
|
||||
/* ----------------- Support chat threads ----------------- */
|
||||
app.post('/api/support/chat/threads', authenticateUser, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const id = uuid();
|
||||
const title = (req.body?.title || 'Support chat').slice(0, 200);
|
||||
const id = uuid();
|
||||
const title = (req.body?.title || 'Support chat').slice(0, 200);
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_threads (id,user_id,bot_type,title) VALUES (?,?, "support", ?)',
|
||||
[id, userId, title]
|
||||
@ -1266,8 +1265,7 @@ app.post('/api/chat/threads', authenticateUser, async (req, res) => {
|
||||
res.json({ id, title });
|
||||
});
|
||||
|
||||
/* LIST threads */
|
||||
app.get('/api/chat/threads', authenticateUser, async (req, res) => {
|
||||
app.get('/api/support/chat/threads', authenticateUser, async (req, res) => {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id,title,updated_at FROM ai_chat_threads WHERE user_id=? AND bot_type="support" ORDER BY updated_at DESC LIMIT 50',
|
||||
[req.user.id]
|
||||
@ -1275,15 +1273,13 @@ app.get('/api/chat/threads', authenticateUser, async (req, res) => {
|
||||
res.json({ threads: rows });
|
||||
});
|
||||
|
||||
/* GET thread + messages */
|
||||
app.get('/api/chat/threads/:id', authenticateUser, async (req, res) => {
|
||||
app.get('/api/support/chat/threads/:id', authenticateUser, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const [[t]] = await pool.query(
|
||||
'SELECT id FROM ai_chat_threads WHERE id=? AND user_id=? AND bot_type="support"',
|
||||
[id, req.user.id]
|
||||
);
|
||||
if (!t) return res.status(404).json({ error: 'not_found' });
|
||||
|
||||
const [msgs] = await pool.query(
|
||||
'SELECT role,content,created_at FROM ai_chat_messages WHERE thread_id=? ORDER BY id ASC LIMIT 200',
|
||||
[id]
|
||||
@ -1291,8 +1287,8 @@ app.get('/api/chat/threads/:id', authenticateUser, async (req, res) => {
|
||||
res.json({ messages: msgs });
|
||||
});
|
||||
|
||||
/* STREAM reply via local /api/chat/free */
|
||||
app.post('/api/chat/threads/:id/stream', authenticateUser, async (req, res) => {
|
||||
/* ---- STREAM proxy: saves user msg, calls your /api/chat/free, saves assistant ---- */
|
||||
app.post('/api/support/chat/threads/:id/stream', authenticateUser, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
const { prompt = '', pageContext = '', snapshot = null } = req.body || {};
|
||||
@ -1304,85 +1300,79 @@ app.post('/api/chat/threads/:id/stream', authenticateUser, async (req, res) => {
|
||||
);
|
||||
if (!t) return res.status(404).json({ error: 'not_found' });
|
||||
|
||||
// save user msg
|
||||
// 1) save user message
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "user", ?)',
|
||||
[id, userId, prompt]
|
||||
);
|
||||
|
||||
// small history for context
|
||||
// 2) load last 40 messages as chatHistory for context
|
||||
const [history] = await pool.query(
|
||||
'SELECT role,content FROM ai_chat_messages WHERE thread_id=? ORDER BY id ASC LIMIT 40',
|
||||
[id]
|
||||
);
|
||||
|
||||
// call local free-chat (server2 hosts /api/chat/free)
|
||||
const internal = await fetch('http://localhost:5001/api/chat/free', {
|
||||
// 3) call internal free endpoint (streaming)
|
||||
const internal = await fetch(`${API_BASE}/chat/free`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json',
|
||||
'Accept' : 'text/event-stream',
|
||||
'Authorization': req.headers.authorization || '',
|
||||
'Cookie' : req.headers.cookie || ''
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/event-stream',
|
||||
Authorization: req.headers.authorization || ''
|
||||
},
|
||||
body: JSON.stringify({ prompt, pageContext, snapshot, chatHistory: history })
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
pageContext,
|
||||
snapshot,
|
||||
chatHistory: history
|
||||
})
|
||||
});
|
||||
|
||||
if (!internal.ok || !internal.body) {
|
||||
return res.status(502).json({ error: 'upstream_failed' });
|
||||
}
|
||||
|
||||
// SSE-ish newline stream (matches your ChatDrawer reader)
|
||||
res.writeHead(200, {
|
||||
'Content-Type' : 'text/event-stream; charset=utf-8',
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Connection' : 'keep-alive',
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
res.flushHeaders?.();
|
||||
// 4) pipe stream to client while buffering assistant text to persist at the end
|
||||
res.status(200);
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
const reader = internal.body.getReader();
|
||||
const reader = internal.body.getReader();
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = '';
|
||||
|
||||
let assistant = '';
|
||||
|
||||
const push = (line) => {
|
||||
async function flush(line) {
|
||||
assistant += line + '\n';
|
||||
res.write(line + '\n'); // write strings, no await
|
||||
};
|
||||
await res.write(encoder.encode(line + '\n'));
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (!value) continue;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
let nl;
|
||||
while ((nl = buf.indexOf('\n')) !== -1) {
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (line) push(line);
|
||||
}
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (!value) continue;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
let nl;
|
||||
while ((nl = buf.indexOf('\n')) !== -1) {
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (line) await flush(line);
|
||||
}
|
||||
if (buf.trim()) push(buf.trim());
|
||||
} catch (err) {
|
||||
console.error('[support stream]', err);
|
||||
res.write('Sorry — error occurred\n');
|
||||
}
|
||||
if (buf.trim()) await flush(buf.trim());
|
||||
|
||||
// persist assistant
|
||||
if (assistant.trim()) {
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "assistant", ?)',
|
||||
[id, userId, assistant.trim()]
|
||||
);
|
||||
await pool.query('UPDATE ai_chat_threads SET updated_at=CURRENT_TIMESTAMP WHERE id=?', [id]);
|
||||
}
|
||||
// 5) persist assistant message & touch thread
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "assistant", ?)',
|
||||
[id, userId, assistant.trim()]
|
||||
);
|
||||
await pool.query('UPDATE ai_chat_threads SET updated_at=CURRENT_TIMESTAMP WHERE id=?', [id]);
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
|
||||
/**************************************************
|
||||
* Start the Express server
|
||||
**************************************************/
|
||||
|
@ -156,7 +156,6 @@ function internalFetch(req, urlPath, opts = {}) {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: req.headers?.authorization || "", // tolerate undefined
|
||||
Cookie: req.headers?.cookie || "",
|
||||
...(opts.headers || {})
|
||||
}
|
||||
});
|
||||
@ -1724,19 +1723,16 @@ Always end with: “AptivaAI is an educational tool – not advice.”
|
||||
);
|
||||
|
||||
/* ------------- Retirement chat threads ------------- */
|
||||
|
||||
/* CREATE a Retirement thread */
|
||||
app.post('/api/premium/retire/chat/threads', authenticatePremiumUser, async (req, res) => {
|
||||
const id = uuid();
|
||||
const title = (req.body?.title || 'Retirement chat').slice(0, 200);
|
||||
const id = uuid();
|
||||
const title = (req.body?.title || 'Retirement chat').slice(0,200);
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_threads (id,user_id,bot_type,title) VALUES (?,?, "retire", ?)',
|
||||
[id, req.id, title]
|
||||
[req.id, title]
|
||||
);
|
||||
res.json({ id, title });
|
||||
});
|
||||
|
||||
/* LIST Retirement threads */
|
||||
app.get('/api/premium/retire/chat/threads', authenticatePremiumUser, async (req, res) => {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id,title,updated_at FROM ai_chat_threads WHERE user_id=? AND bot_type="retire" ORDER BY updated_at DESC LIMIT 50',
|
||||
@ -1745,7 +1741,6 @@ app.get('/api/premium/retire/chat/threads', authenticatePremiumUser, async (req,
|
||||
res.json({ threads: rows });
|
||||
});
|
||||
|
||||
/* GET one Retirement thread + messages */
|
||||
app.get('/api/premium/retire/chat/threads/:id', authenticatePremiumUser, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const [[t]] = await pool.query(
|
||||
@ -1753,7 +1748,6 @@ app.get('/api/premium/retire/chat/threads/:id', authenticatePremiumUser, async (
|
||||
[id, req.id]
|
||||
);
|
||||
if (!t) return res.status(404).json({ error: 'not_found' });
|
||||
|
||||
const [msgs] = await pool.query(
|
||||
'SELECT role,content,created_at FROM ai_chat_messages WHERE thread_id=? ORDER BY id ASC LIMIT 200',
|
||||
[id]
|
||||
@ -1761,76 +1755,60 @@ app.get('/api/premium/retire/chat/threads/:id', authenticatePremiumUser, async (
|
||||
res.json({ messages: msgs });
|
||||
});
|
||||
|
||||
/* POST a message (auto-create thread if missing) */
|
||||
app.post('/api/premium/retire/chat/threads/:id/messages', authenticatePremiumUser, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { content = '', context = {} } = req.body || {};
|
||||
if (!content.trim()) return res.status(400).json({ error: 'empty' });
|
||||
|
||||
// ensure thread exists (auto-create if missing)
|
||||
const [[t]] = await pool.query(
|
||||
'SELECT id FROM ai_chat_threads WHERE id=? AND user_id=? AND bot_type="retire"',
|
||||
[id, req.id]
|
||||
);
|
||||
if (!t) {
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_threads (id,user_id,bot_type,title) VALUES (?,?, "retire", ?)',
|
||||
[id, req.id, 'Retirement chat']
|
||||
);
|
||||
}
|
||||
if (!t) return res.status(404).json({ error: 'not_found' });
|
||||
|
||||
// save user msg
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "user", ?)',
|
||||
[id, req.id, content]
|
||||
);
|
||||
|
||||
// history (≤40)
|
||||
const [history] = await pool.query(
|
||||
'SELECT role,content FROM ai_chat_messages WHERE thread_id=? ORDER BY id ASC LIMIT 40',
|
||||
[id]
|
||||
);
|
||||
|
||||
// call AI
|
||||
// Call your existing retirement logic (keeps all safety/patch behavior)
|
||||
const resp = await internalFetch(req, '/premium/retirement/aichat', {
|
||||
method : 'POST',
|
||||
headers: { 'Content-Type':'application/json' },
|
||||
body : JSON.stringify({ prompt: content, scenario_id: context?.scenario_id, chatHistory: history })
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: content,
|
||||
scenario_id: context?.scenario_id,
|
||||
chatHistory: history
|
||||
})
|
||||
});
|
||||
const json = await resp.json();
|
||||
const reply = (json?.reply || '').trim() || 'Sorry, please try again.';
|
||||
|
||||
let reply = 'Sorry, please try again.';
|
||||
if (resp.ok) {
|
||||
const json = await resp.json();
|
||||
reply = (json?.reply || '').trim() || reply;
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "assistant", ?)',
|
||||
[id, req.id, reply]
|
||||
);
|
||||
await pool.query('UPDATE ai_chat_threads SET updated_at=CURRENT_TIMESTAMP WHERE id=?', [id]);
|
||||
|
||||
// save AI reply
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "assistant", ?)',
|
||||
[id, req.id, reply]
|
||||
);
|
||||
await pool.query('UPDATE ai_chat_threads SET updated_at=CURRENT_TIMESTAMP WHERE id=?', [id]);
|
||||
|
||||
return res.json(json); // keep scenarioPatch passthrough
|
||||
} else {
|
||||
return res.status(502).json({ error: 'upstream_failed' });
|
||||
}
|
||||
res.json(json); // keep scenarioPatch passthrough
|
||||
});
|
||||
|
||||
|
||||
/* ------------------ Coach chat threads ------------------ */
|
||||
|
||||
/* CREATE a Coach thread */
|
||||
app.post('/api/premium/coach/chat/threads', authenticatePremiumUser, async (req, res) => {
|
||||
const id = uuid();
|
||||
const title = (req.body?.title || 'CareerCoach chat').slice(0, 200);
|
||||
const id = uuid();
|
||||
const title = (req.body?.title || 'CareerCoach chat').slice(0,200);
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_threads (id,user_id,bot_type,title) VALUES (?,?, "coach", ?)',
|
||||
[id, req.id, title]
|
||||
[req.id, title]
|
||||
);
|
||||
res.json({ id, title });
|
||||
});
|
||||
|
||||
/* LIST Coach threads */
|
||||
app.get('/api/premium/coach/chat/threads', authenticatePremiumUser, async (req, res) => {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id,title,updated_at FROM ai_chat_threads WHERE user_id=? AND bot_type="coach" ORDER BY updated_at DESC LIMIT 50',
|
||||
@ -1839,7 +1817,6 @@ app.get('/api/premium/coach/chat/threads', authenticatePremiumUser, async (req,
|
||||
res.json({ threads: rows });
|
||||
});
|
||||
|
||||
/* GET one Coach thread + messages */
|
||||
app.get('/api/premium/coach/chat/threads/:id', authenticatePremiumUser, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const [[t]] = await pool.query(
|
||||
@ -1847,7 +1824,6 @@ app.get('/api/premium/coach/chat/threads/:id', authenticatePremiumUser, async (r
|
||||
[id, req.id]
|
||||
);
|
||||
if (!t) return res.status(404).json({ error: 'not_found' });
|
||||
|
||||
const [msgs] = await pool.query(
|
||||
'SELECT role,content,created_at FROM ai_chat_messages WHERE thread_id=? ORDER BY id ASC LIMIT 200',
|
||||
[id]
|
||||
@ -1855,59 +1831,46 @@ app.get('/api/premium/coach/chat/threads/:id', authenticatePremiumUser, async (r
|
||||
res.json({ messages: msgs });
|
||||
});
|
||||
|
||||
/* POST a message (auto-create thread if missing) */
|
||||
/* Post a user message → call your existing /api/premium/ai/chat → save both */
|
||||
app.post('/api/premium/coach/chat/threads/:id/messages', authenticatePremiumUser, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { content = '', context = {} } = req.body || {};
|
||||
if (!content.trim()) return res.status(400).json({ error: 'empty' });
|
||||
|
||||
// ensure thread exists (auto-create if missing)
|
||||
const [[t]] = await pool.query(
|
||||
'SELECT id FROM ai_chat_threads WHERE id=? AND user_id=? AND bot_type="coach"',
|
||||
[id, req.id]
|
||||
);
|
||||
if (!t) {
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_threads (id,user_id,bot_type,title) VALUES (?,?, "coach", ?)',
|
||||
[id, req.id, 'CareerCoach chat']
|
||||
);
|
||||
}
|
||||
if (!t) return res.status(404).json({ error: 'not_found' });
|
||||
|
||||
// save user msg
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "user", ?)',
|
||||
[id, req.id, content]
|
||||
);
|
||||
|
||||
// history (≤40)
|
||||
const [history] = await pool.query(
|
||||
'SELECT role,content FROM ai_chat_messages WHERE thread_id=? ORDER BY id ASC LIMIT 40',
|
||||
[id]
|
||||
);
|
||||
|
||||
// call AI
|
||||
const resp = await internalFetch(req, '/premium/ai/chat', {
|
||||
method : 'POST',
|
||||
headers: { 'Content-Type':'application/json' },
|
||||
body : JSON.stringify({ ...context, chatHistory: history })
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...context, // userProfile, scenarioRow, etc.
|
||||
chatHistory: history // reuse your existing prompt builder
|
||||
})
|
||||
});
|
||||
const json = await resp.json();
|
||||
const reply = (json?.reply || '').trim() || 'Sorry, please try again.';
|
||||
|
||||
let reply = 'Sorry, please try again.';
|
||||
if (resp.ok) {
|
||||
const json = await resp.json();
|
||||
reply = (json?.reply || '').trim() || reply;
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "assistant", ?)',
|
||||
[id, req.id, reply]
|
||||
);
|
||||
await pool.query('UPDATE ai_chat_threads SET updated_at=CURRENT_TIMESTAMP WHERE id=?', [id]);
|
||||
|
||||
// save AI reply
|
||||
await pool.query(
|
||||
'INSERT INTO ai_chat_messages (thread_id,user_id,role,content) VALUES (?,?, "assistant", ?)',
|
||||
[id, req.id, reply]
|
||||
);
|
||||
await pool.query('UPDATE ai_chat_threads SET updated_at=CURRENT_TIMESTAMP WHERE id=?', [id]);
|
||||
|
||||
return res.json({ reply });
|
||||
} else {
|
||||
return res.status(502).json({ error: 'upstream_failed' });
|
||||
}
|
||||
res.json({ reply });
|
||||
});
|
||||
|
||||
app.post('/api/premium/career-profile/clone', authenticatePremiumUser, async (req,res) => {
|
||||
|
@ -39,7 +39,6 @@ export FROM_SECRETS_MANAGER=true
|
||||
|
||||
# React needs the prefixed var at BUILD time
|
||||
export REACT_APP_GOOGLE_MAPS_API_KEY="$GOOGLE_MAPS_API_KEY"
|
||||
export REACT_APP_ENV_NAME="$ENV_NAME"
|
||||
|
||||
|
||||
# ───────────────────────── node + npm ci cache ─────────────────────────
|
||||
|
@ -217,20 +217,3 @@ CREATE TABLE IF NOT EXISTS ai_chat_messages (
|
||||
FOREIGN KEY (thread_id) REFERENCES ai_chat_threads(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Orphan message thread_ids (no matching thread row)
|
||||
SELECT DISTINCT m.thread_id
|
||||
FROM ai_chat_messages m
|
||||
LEFT JOIN ai_chat_threads t ON t.id = m.thread_id
|
||||
WHERE t.id IS NULL;
|
||||
|
||||
INSERT INTO ai_chat_threads (id, user_id, bot_type, title)
|
||||
SELECT m.thread_id, 58, 'coach', 'CareerCoach chat'
|
||||
FROM ai_chat_messages m
|
||||
LEFT JOIN ai_chat_threads t ON t.id = m.thread_id
|
||||
WHERE t.id IS NULL;
|
||||
|
||||
ALTER TABLE ai_chat_messages
|
||||
ADD CONSTRAINT fk_messages_thread
|
||||
FOREIGN KEY (thread_id) REFERENCES ai_chat_threads(id)
|
||||
ON DELETE CASCADE;
|
||||
|
@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT=aptivaai-dev
|
||||
ENV=dev
|
||||
|
||||
add_ver() {
|
||||
local name="$1" ; shift
|
||||
gcloud secrets versions add "${name}_${ENV}" --data-file=- --project="$PROJECT" >/dev/null
|
||||
echo "✅ ${name}_${ENV} rotated"
|
||||
}
|
||||
|
||||
echo "🔐 Rotating DEV secrets in ${PROJECT}"
|
||||
|
||||
# ── Generate fresh randoms
|
||||
openssl rand -hex 32 | add_ver JWT_SECRET
|
||||
|
||||
# ── Paste new third-party keys (press Enter to skip any you don't want to rotate)
|
||||
read -s -p "OPENAI_API_KEY_${ENV}: " OPENAI && echo
|
||||
[[ -n "${OPENAI}" ]] && printf "%s" "$OPENAI" | add_ver OPENAI_API_KEY
|
||||
|
||||
read -p "ONET_USERNAME_${ENV}: " ONETU && echo
|
||||
[[ -n "${ONETU}" ]] && printf "%s" "$ONETU" | add_ver ONET_USERNAME
|
||||
|
||||
read -s -p "ONET_PASSWORD_${ENV}: " ONETP && echo
|
||||
[[ -n "${ONETP}" ]] && printf "%s" "$ONETP" | add_ver ONET_PASSWORD
|
||||
|
||||
read -s -p "STRIPE_SECRET_KEY_${ENV}: " SSK && echo
|
||||
[[ -n "${SSK}" ]] && printf "%s" "$SSK" | add_ver STRIPE_SECRET_KEY
|
||||
|
||||
read -p "STRIPE_PUBLISHABLE_KEY_${ENV}: " SPK && echo
|
||||
[[ -n "${SPK}" ]] && printf "%s" "$SPK" | add_ver STRIPE_PUBLISHABLE_KEY
|
||||
|
||||
read -s -p "STRIPE_WH_SECRET_${ENV}: " SWH && echo
|
||||
[[ -n "${SWH}" ]] && printf "%s" "$SWH" | add_ver STRIPE_WH_SECRET
|
||||
|
||||
read -s -p "SUPPORT_SENDGRID_API_KEY_${ENV}: " SG && echo
|
||||
[[ -n "${SG}" ]] && printf "%s" "$SG" | add_ver SUPPORT_SENDGRID_API_KEY
|
||||
|
||||
read -s -p "EMAIL_INDEX_SECRET_${ENV}: " EIDX && echo
|
||||
[[ -n "${EIDX}" ]] && printf "%s" "$EIDX" | add_ver EMAIL_INDEX_SECRET
|
||||
|
||||
read -p "TWILIO_ACCOUNT_SID_${ENV}: " TSID && echo
|
||||
[[ -n "${TSID}" ]] && printf "%s" "$TSID" | add_ver TWILIO_ACCOUNT_SID
|
||||
|
||||
read -s -p "TWILIO_AUTH_TOKEN_${ENV}: " TAUT && echo
|
||||
[[ -n "${TAUT}" ]] && printf "%s" "$TAUT" | add_ver TWILIO_AUTH_TOKEN
|
||||
|
||||
read -p "TWILIO_MESSAGING_SERVICE_SID_${ENV}: " TMSS && echo
|
||||
[[ -n "${TMSS}" ]] && printf "%s" "$TMSS" | add_ver TWILIO_MESSAGING_SERVICE_SID
|
||||
|
||||
# Optional: rotate Maps if it was in the leaked image
|
||||
read -s -p "GOOGLE_MAPS_API_KEY_${ENV} (optional): " GMAPS && echo
|
||||
[[ -n "${GMAPS}" ]] && printf "%s" "$GMAPS" | add_ver GOOGLE_MAPS_API_KEY
|
||||
|
||||
echo "🔁 Rebuilding DEV with fresh secrets…"
|
||||
ENV=dev ./deploy_all.sh
|
||||
|
||||
echo "🧪 Verifying runtime env inside containers:"
|
||||
docker compose exec -T server1 sh -lc 'printenv | egrep "JWT_SECRET|OPENAI|ONET|STRIPE_(SECRET|PUBLISH|WH)|SENDGRID|EMAIL_INDEX|TWILIO|TOKEN_MAX_AGE_MS|ACCESS_COOKIE_NAME|COOKIE_(SECURE|SAMESITE)"'
|
||||
echo "✅ Done."
|
33
src/App.js
33
src/App.js
@ -69,7 +69,6 @@ function App() {
|
||||
const [retireProps, setRetireProps] = useState(null);
|
||||
const [supportOpen, setSupportOpen] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState('');
|
||||
const [loggingOut, setLoggingOut] = useState(false);
|
||||
|
||||
|
||||
const AUTH_HOME = '/signin-landing';
|
||||
@ -168,15 +167,9 @@ const showPremiumCTA = !premiumPaths.some(p =>
|
||||
// ==============================
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (loggingOut) return;
|
||||
|
||||
// Skip auth probe on all public auth routes
|
||||
if (
|
||||
location.pathname.startsWith('/reset-password') ||
|
||||
location.pathname === '/signin' ||
|
||||
location.pathname === '/signup' ||
|
||||
location.pathname === '/forgot-password'
|
||||
) {
|
||||
// Don’t do auth probe on reset-password
|
||||
if (location.pathname.startsWith('/reset-password')) {
|
||||
try { localStorage.removeItem('id'); } catch {}
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
@ -184,7 +177,6 @@ if (loggingOut) return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@ -216,7 +208,7 @@ if (loggingOut) return;
|
||||
|
||||
// include isAuthScreen if you prefer, but this local check avoids a dep loop
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname, navigate, loggingOut]);
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
/* =====================
|
||||
Support Modal Email
|
||||
@ -242,7 +234,6 @@ if (loggingOut) return;
|
||||
|
||||
|
||||
const confirmLogout = async () => {
|
||||
setLoggingOut(true);
|
||||
// 1) Ask the server to clear the session cookie
|
||||
try {
|
||||
// If you created /logout (no /api prefix):
|
||||
@ -277,7 +268,6 @@ const confirmLogout = async () => {
|
||||
|
||||
// 4) Back to sign-in
|
||||
navigate('/signin', { replace: true });
|
||||
setLoggingOut(false);
|
||||
};
|
||||
|
||||
const cancelLogout = () => {
|
||||
@ -307,13 +297,9 @@ const cancelLogout = () => {
|
||||
scenario, setScenario,
|
||||
user, setUser}}
|
||||
>
|
||||
<ChatCtx.Provider value={{ setChatSnapshot,
|
||||
openSupport: () => {
|
||||
if (!isAuthenticated) return;
|
||||
setDrawerPane('support'); setDrawerOpen(true);
|
||||
},
|
||||
openRetire : (props) => {
|
||||
if (!isAuthenticated || !canShowRetireBot) return;
|
||||
<ChatCtx.Provider value={{ setChatSnapshot,
|
||||
openSupport: () => { setDrawerPane('support'); setDrawerOpen(true); },
|
||||
openRetire : (props) => {
|
||||
if (!canShowRetireBot) {
|
||||
console.warn('Retirement bot disabled on this page');
|
||||
return;
|
||||
@ -563,11 +549,10 @@ const cancelLogout = () => {
|
||||
|
||||
{/* LOGOUT BUTTON */}
|
||||
<button
|
||||
className="text-red-600 hover:text-red-800 bg-transparent border-none"
|
||||
onClick={handleLogoutClick}
|
||||
disabled={loggingOut}
|
||||
className="text-red-600 hover:text-red-800 bg-transparent border-none disabled:opacity-60"
|
||||
>
|
||||
{loggingOut ? 'Signing out…' : 'Logout'}
|
||||
Logout
|
||||
</button>
|
||||
|
||||
{/* SHOW WARNING MODAL IF needed */}
|
||||
@ -714,7 +699,6 @@ const cancelLogout = () => {
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
{isAuthenticated && (
|
||||
<ChatDrawer
|
||||
open={drawerOpen}
|
||||
onOpenChange={setDrawerOpen}
|
||||
@ -729,7 +713,6 @@ const cancelLogout = () => {
|
||||
uiToolHandlers={uiToolHandlers}
|
||||
canShowRetireBot={canShowRetireBot}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Session Handler (Optional) */}
|
||||
<SessionExpiredHandler />
|
||||
|
@ -3,24 +3,6 @@ import authFetch from "../utils/authFetch.js";
|
||||
|
||||
const isoToday = new Date().toISOString().slice(0,10); // top-level helper
|
||||
|
||||
async function ensureCoachThread() {
|
||||
// try to list an existing thread
|
||||
const r = await authFetch('/api/premium/coach/chat/threads');
|
||||
if (r.ok && (r.headers.get('content-type')||'').includes('application/json')) {
|
||||
const { threads = [] } = await r.json();
|
||||
if (threads.length) return threads[0].id;
|
||||
}
|
||||
// none → create one
|
||||
const r2 = await authFetch('/api/premium/coach/chat/threads', {
|
||||
method : 'POST',
|
||||
headers: { 'Content-Type':'application/json' },
|
||||
body : JSON.stringify({ title: 'CareerCoach chat' })
|
||||
});
|
||||
if (!r2.ok) throw new Error('failed to create coach thread');
|
||||
const { id } = await r2.json();
|
||||
return id;
|
||||
}
|
||||
|
||||
function buildInterviewPrompt(careerName, jobDescription = "") {
|
||||
return `
|
||||
You are an expert interviewer for the role **${careerName}**.
|
||||
@ -144,51 +126,42 @@ export default function CareerCoach({
|
||||
if (chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight;
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const id = await ensureCoachThread();
|
||||
setThreadId(id);
|
||||
const r3 = await authFetch(`/api/premium/coach/chat/threads/${id}`);
|
||||
if (r3.ok && (r3.headers.get('content-type') || '').includes('application/json')) {
|
||||
const { messages: msgs = [] } = await r3.json();
|
||||
setMessages(msgs);
|
||||
}
|
||||
} catch {
|
||||
// keep UI usable; callAi will create on first send
|
||||
}
|
||||
})();
|
||||
}, [careerProfileId]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
if (!careerProfileId) return;
|
||||
|
||||
try {
|
||||
// ensure or create a thread
|
||||
const newId = await ensureCoachThread();
|
||||
if (cancelled) return;
|
||||
// list threads for this profile
|
||||
const r = await authFetch(
|
||||
`/api/premium/coach/chat/threads?careerProfileId=${encodeURIComponent(careerProfileId)}`
|
||||
);
|
||||
|
||||
setThreadId(newId);
|
||||
if (!(r.ok && (r.headers.get('content-type') || '').includes('application/json'))) {
|
||||
setThreadId(null); // coach offline; no network errors on mount
|
||||
return;
|
||||
}
|
||||
|
||||
// preload history (best-effort)
|
||||
const r3 = await authFetch(`/api/premium/coach/chat/threads/${newId}`);
|
||||
if (cancelled) return;
|
||||
const { threads = [] } = await r.json();
|
||||
const existing = threads.find(Boolean);
|
||||
if (!existing?.id) {
|
||||
setThreadId(null); // no thread yet; lazy-create on first send
|
||||
return;
|
||||
}
|
||||
|
||||
if (r3.ok && (r3.headers.get('content-type') || '').includes('application/json')) {
|
||||
const { messages: msgs = [] } = await r3.json();
|
||||
if (!cancelled) setMessages(msgs);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) setThreadId(null); // UI stays usable; callAi will create on first send
|
||||
const id = existing.id;
|
||||
setThreadId(id);
|
||||
|
||||
// preload history
|
||||
const r3 = await authFetch(
|
||||
`/api/premium/coach/chat/threads/${id}?careerProfileId=${encodeURIComponent(careerProfileId)}`
|
||||
);
|
||||
if (r3.ok && (r3.headers.get('content-type') || '').includes('application/json')) {
|
||||
const { messages: msgs = [] } = await r3.json();
|
||||
setMessages(msgs);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [careerProfileId]);
|
||||
|
||||
|
||||
/* -------------- intro ---------------- */
|
||||
useEffect(() => {
|
||||
if (!scenarioRow) return;
|
||||
@ -262,37 +235,28 @@ I'm here to support you with personalized coaching. What would you like to focus
|
||||
async function callAi(updatedHistory, opts = {}) {
|
||||
setLoading(true);
|
||||
try {
|
||||
let id = threadId; // <-- declare it
|
||||
if (!id) { // first send or race
|
||||
id = await ensureCoachThread(); // create/reuse
|
||||
setThreadId(id);
|
||||
}
|
||||
|
||||
if (!threadId) throw new Error('thread not ready');
|
||||
const context = { userProfile, financialProfile, scenarioRow, collegeProfile };
|
||||
|
||||
const r = await authFetch(`/api/premium/coach/chat/threads/${id}/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: updatedHistory.at(-1)?.content || '',
|
||||
context
|
||||
})
|
||||
const r = await authFetch(`/api/premium/coach/chat/threads/${threadId}/messages`, {
|
||||
method:'POST',
|
||||
headers:{ 'Content-Type':'application/json' },
|
||||
body: JSON.stringify({ content: updatedHistory.at(-1)?.content || '', context })
|
||||
});
|
||||
|
||||
let reply = 'Sorry, something went wrong.';
|
||||
if (r.ok && (r.headers.get('content-type') || '').includes('application/json')) {
|
||||
const data = await r.json();
|
||||
reply = (data?.reply || '').trim() || reply;
|
||||
}
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: reply }]);
|
||||
if (r.ok && (r.headers.get('content-type')||'').includes('application/json')) {
|
||||
const data = await r.json();
|
||||
reply = (data?.reply || '').trim() || reply;
|
||||
}
|
||||
setMessages(prev => [...prev, { role:'assistant', content: reply }]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: 'Sorry, something went wrong.' }]);
|
||||
setMessages(prev => [...prev, { role:'assistant', content:'Sorry, something went wrong.' }]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ------------ normal send ------------- */
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
@ -8,20 +8,15 @@ import { MessageCircle } from 'lucide-react';
|
||||
import RetirementChatBar from './RetirementChatBar.js';
|
||||
|
||||
async function ensureSupportThread() {
|
||||
// list existing
|
||||
const r = await fetch('/api/chat/threads', { credentials:'include' });
|
||||
if (!r.ok) throw new Error(`threads list failed: ${r.status}`);
|
||||
const r = await fetch('/api/support/chat/threads', { credentials:'include' });
|
||||
const { threads } = await r.json();
|
||||
if (threads?.length) return threads[0].id;
|
||||
|
||||
// create new
|
||||
const r2 = await fetch('/api/chat/threads', {
|
||||
const r2 = await fetch('/api/support/chat/threads', {
|
||||
method: 'POST',
|
||||
credentials:'include',
|
||||
headers:{ 'Content-Type':'application/json' },
|
||||
body: JSON.stringify({ title: 'Support chat' })
|
||||
});
|
||||
if (!r2.ok) throw new Error(`thread create failed: ${r2.status}`);
|
||||
const { id } = await r2.json();
|
||||
return id;
|
||||
}
|
||||
@ -67,24 +62,14 @@ export default function ChatDrawer({
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const id = await ensureSupportThread();
|
||||
setSupportThreadId(id);
|
||||
// preload messages
|
||||
const r = await fetch(`/api/chat/threads/${id}`, { credentials:'include' });
|
||||
if (r.ok) {
|
||||
const { messages: msgs } = await r.json();
|
||||
setMessages(msgs || []);
|
||||
} else {
|
||||
// don’t crash UI on preload failure
|
||||
setMessages([]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Support preload]', e);
|
||||
setMessages([]);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
const id = await ensureSupportThread();
|
||||
setSupportThreadId(id);
|
||||
// preload messages if you want:
|
||||
const r = await fetch(`/api/support/chat/threads/${id}`, { credentials:'include' });
|
||||
const { messages: msgs } = await r.json();
|
||||
setMessages(msgs || []);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
/* helper: merge chunks while streaming */
|
||||
const pushAssistant = (chunk) =>
|
||||
@ -117,7 +102,7 @@ export default function ChatDrawer({
|
||||
setPrompt('');
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/chat/threads/${supportThreadId}/stream`, {
|
||||
const resp = await fetch(`/api/support/chat/threads/${supportThreadId}/stream`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type':'application/json', Accept:'text/event-stream' },
|
||||
|
@ -23,7 +23,6 @@ const InterestInventory = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [userProfile, setUserProfile] = useState(null);
|
||||
const isProd = (process.env.REACT_APP_ENV_NAME || '').toLowerCase() === 'prod';
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -291,16 +290,14 @@ const InterestInventory = () => {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!isProd && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={randomizeAnswers}
|
||||
disabled={isSubmitting}
|
||||
className="rounded bg-orange-500 px-4 py-2 text-white hover:bg-orange-600 disabled:bg-orange-300"
|
||||
>
|
||||
Randomize Answers
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={randomizeAnswers}
|
||||
disabled={isSubmitting}
|
||||
className="rounded bg-orange-500 px-4 py-2 text-white hover:bg-orange-600 disabled:bg-orange-300"
|
||||
>
|
||||
Randomize Answers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user