Copilot pipeline rewrite v4
This commit is contained in:
parent
77ac250646
commit
2f4c8fb2cf
@ -128,7 +128,8 @@ steps:
|
|||||||
docker compose pull; \
|
docker compose pull; \
|
||||||
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH \
|
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH \
|
||||||
docker compose up -d --force-recreate --remove-orphans; \
|
docker compose up -d --force-recreate --remove-orphans; \
|
||||||
echo \"✅ Staging stack refreshed with tag $IMG_TAG\"'
|
echo \"✅ Staging stack refreshed with tag \$IMG_TAG\"'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
secrets:
|
secrets:
|
||||||
|
@ -5,43 +5,40 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import helmet from 'helmet'; // For HTTP security headers
|
import helmet from 'helmet';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import xlsx from 'xlsx'; // Keep for CIP->SOC mapping only
|
import xlsx from 'xlsx';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { open } from 'sqlite';
|
import { open } from 'sqlite';
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
import pool from './config/mysqlPool.js'; // adjust path if needed
|
import pool from './config/mysqlPool.js'; // exports { query, execute, raw, ... }
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { readFile } from 'fs/promises'; // <-- add this
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
import chatFreeEndpoint from "./utils/chatFreeEndpoint.js";
|
import chatFreeEndpoint from "./utils/chatFreeEndpoint.js";
|
||||||
import { OpenAI } from 'openai';
|
import { OpenAI } from 'openai';
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import authenticateUser from './utils/authenticateUser.js';
|
import authenticateUser from './utils/authenticateUser.js';
|
||||||
import { vectorSearch } from "./utils/vectorSearch.js";
|
import { vectorSearch } from "./utils/vectorSearch.js";
|
||||||
import { initEncryption, verifyCanary } from './shared/crypto/encryption.js';
|
import { initEncryption, verifyCanary, SENTINEL } from './shared/crypto/encryption.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Basic file init ---
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const rootPath = path.resolve(__dirname, '..'); // Up one level
|
const rootPath = path.resolve(__dirname, '..');
|
||||||
const env = process.env.NODE_ENV?.trim() || 'development';
|
const env = process.env.NODE_ENV?.trim() || 'development';
|
||||||
const envPath = path.resolve(rootPath, `.env.${env}`);
|
const envPath = path.resolve(rootPath, `.env.${env}`);
|
||||||
dotenv.config({ path: envPath }); // Load .env
|
dotenv.config({ path: envPath, override: false }); // don't clobber compose-injected env
|
||||||
|
|
||||||
const ROOT_DIR = path.resolve(__dirname, '..'); // repo root
|
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||||
const PUBLIC_DIR = path.join(ROOT_DIR, 'public'); // static json files
|
const PUBLIC_DIR = path.join(ROOT_DIR, 'public');
|
||||||
const CIP_TO_SOC_PATH = path.join(PUBLIC_DIR, 'CIP_to_ONET_SOC.xlsx');
|
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 INSTITUTION_DATA_PATH= path.join(PUBLIC_DIR, 'Institution_data.json');
|
||||||
const SALARY_DB_PATH = path.join(ROOT_DIR, 'salary_info.db');
|
const SALARY_DB_PATH = path.join(ROOT_DIR, 'salary_info.db');
|
||||||
const USER_PROFILE_DB_PATH = path.join(ROOT_DIR, 'user_profile.db');
|
const USER_PROFILE_DB_PATH = path.join(ROOT_DIR, 'user_profile.db');
|
||||||
|
|
||||||
for (const p of [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH,
|
for (const p of [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH, SALARY_DB_PATH, USER_PROFILE_DB_PATH]) {
|
||||||
SALARY_DB_PATH, USER_PROFILE_DB_PATH]) {
|
|
||||||
if (!fs.existsSync(p)) {
|
if (!fs.existsSync(p)) {
|
||||||
console.error(`FATAL Required data file not found → ${p}`);
|
console.error(`FATAL Required data file not found → ${p}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -56,12 +53,20 @@ const chatLimiter = rateLimit({
|
|||||||
keyGenerator: req => req.user?.id || req.ip
|
keyGenerator: req => req.user?.id || req.ip
|
||||||
});
|
});
|
||||||
|
|
||||||
// Institution data
|
// Load institution data (kept for existing routes)
|
||||||
const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8'));
|
const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8'));
|
||||||
|
|
||||||
|
// ── DEK + canary bootstrap (use raw pool to avoid DAO interception) ──
|
||||||
|
const db = pool.raw || pool;
|
||||||
|
|
||||||
|
try {
|
||||||
await initEncryption();
|
await initEncryption();
|
||||||
await pool.query('SELECT 1');
|
await db.query('SELECT 1');
|
||||||
await verifyCanary(pool);
|
await verifyCanary(db);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FATAL during crypto/DB bootstrap:', e?.message || e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -72,14 +77,14 @@ function fprPathFromEnv() {
|
|||||||
return p ? path.join(path.dirname(p), 'dek.fpr') : null;
|
return p ? path.join(path.dirname(p), 'dek.fpr') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Liveness: process is up and event loop responsive
|
// 1) Liveness: process up
|
||||||
app.get('/livez', (_req, res) => res.type('text').send('OK'));
|
app.get('/livez', (_req, res) => res.type('text').send('OK'));
|
||||||
|
|
||||||
// 2) Readiness: crypto + canary are good
|
// 2) Readiness: DEK + canary OK
|
||||||
app.get('/readyz', async (_req, res) => {
|
app.get('/readyz', async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
await initEncryption(); // load/unlock DEK
|
await initEncryption();
|
||||||
await verifyCanary(pool); // DB + decrypt sentinel
|
await verifyCanary(db); // <-- use raw pool
|
||||||
return res.type('text').send('OK');
|
return res.type('text').send('OK');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[READYZ]', e.message);
|
console.error('[READYZ]', e.message);
|
||||||
@ -87,15 +92,15 @@ app.get('/readyz', async (_req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3) Health: detailed JSON (you can curl this to “see everything”)
|
// 3) Health: detailed JSON you can curl
|
||||||
app.get('/healthz', async (_req, res) => {
|
app.get('/healthz', async (_req, res) => {
|
||||||
const out = {
|
const out = {
|
||||||
service: process.env.npm_package_name || 'server',
|
service: process.env.npm_package_name || 'server2',
|
||||||
version: process.env.IMG_TAG || null,
|
version: process.env.IMG_TAG || null,
|
||||||
uptime_s: Math.floor(process.uptime()),
|
uptime_s: Math.floor(process.uptime()),
|
||||||
now: new Date().toISOString(),
|
now: new Date().toISOString(),
|
||||||
checks: {
|
checks: {
|
||||||
live: { ok: true }, // if we reached here, process is up
|
live: { ok: true },
|
||||||
crypto: { ok: false, fp: null },
|
crypto: { ok: false, fp: null },
|
||||||
db: { ok: false, ping_ms: null },
|
db: { ok: false, ping_ms: null },
|
||||||
canary: { ok: false }
|
canary: { ok: false }
|
||||||
@ -108,8 +113,7 @@ app.get('/healthz', async (_req, res) => {
|
|||||||
out.checks.crypto.ok = true;
|
out.checks.crypto.ok = true;
|
||||||
const p = fprPathFromEnv();
|
const p = fprPathFromEnv();
|
||||||
if (p) {
|
if (p) {
|
||||||
try { out.checks.crypto.fp = (await readFile(p, 'utf8')).trim(); }
|
try { out.checks.crypto.fp = (await readFile(p, 'utf8')).trim(); } catch {}
|
||||||
catch { /* fp optional */ }
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
out.checks.crypto.error = e.message;
|
out.checks.crypto.error = e.message;
|
||||||
@ -118,7 +122,7 @@ app.get('/healthz', async (_req, res) => {
|
|||||||
// DB ping
|
// DB ping
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
try {
|
try {
|
||||||
await pool.query('SELECT 1');
|
await db.query('SELECT 1'); // <-- use raw pool
|
||||||
out.checks.db.ok = true;
|
out.checks.db.ok = true;
|
||||||
out.checks.db.ping_ms = Date.now() - t0;
|
out.checks.db.ping_ms = Date.now() - t0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -127,7 +131,7 @@ app.get('/healthz', async (_req, res) => {
|
|||||||
|
|
||||||
// canary
|
// canary
|
||||||
try {
|
try {
|
||||||
await verifyCanary(pool);
|
await verifyCanary(db); // <-- use raw pool
|
||||||
out.checks.canary.ok = true;
|
out.checks.canary.ok = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
out.checks.canary.error = e.message;
|
out.checks.canary.error = e.message;
|
||||||
@ -138,15 +142,14 @@ app.get('/healthz', async (_req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
* DB connections
|
* DB connections (SQLite)
|
||||||
**************************************************/
|
**************************************************/
|
||||||
|
let dbSqlite;
|
||||||
let db;
|
|
||||||
let userProfileDb;
|
let userProfileDb;
|
||||||
|
|
||||||
async function initDatabases() {
|
async function initDatabases() {
|
||||||
try {
|
try {
|
||||||
db = await open({
|
dbSqlite = await open({
|
||||||
filename: SALARY_DB_PATH,
|
filename: SALARY_DB_PATH,
|
||||||
driver : sqlite3.Database,
|
driver : sqlite3.Database,
|
||||||
mode : sqlite3.OPEN_READONLY
|
mode : sqlite3.OPEN_READONLY
|
||||||
@ -160,12 +163,15 @@ async function initDatabases() {
|
|||||||
console.log('✅ Connected to user_profile.db');
|
console.log('✅ Connected to user_profile.db');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ DB init failed →', err);
|
console.error('❌ DB init failed →', err);
|
||||||
process.exit(1); // let Docker restart the service
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await initDatabases();
|
await initDatabases();
|
||||||
|
|
||||||
|
// …rest of your routes and app.listen(PORT)
|
||||||
|
|
||||||
|
|
||||||
/* ──────────────────────────────────────────────────────────────
|
/* ──────────────────────────────────────────────────────────────
|
||||||
* SECURITY, CORS, JSON Body
|
* SECURITY, CORS, JSON Body
|
||||||
* ────────────────────────────────────────────────────────────── */
|
* ────────────────────────────────────────────────────────────── */
|
||||||
|
Loading…
Reference in New Issue
Block a user