import express from 'express'; import axios from 'axios'; import cors from 'cors'; import helmet from 'helmet'; import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import fs from 'fs'; import path from 'path'; import bodyParser from 'body-parser'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; // For token-based authentication import mysql from 'mysql2'; import sqlite3 from 'sqlite3'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootPath = path.resolve(__dirname, '..'); // Up one level const env = process.env.NODE_ENV?.trim() || 'development'; const envPath = path.resolve(rootPath, `.env.${env}`); dotenv.config({ path: envPath }); // Load .env file // Grab secrets and config from ENV const JWT_SECRET = process.env.JWT_SECRET; const DB_HOST = process.env.DB_HOST || '127.0.0.1'; const DB_PORT = process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306; const DB_USER = process.env.DB_USER || 'sqluser'; const DB_PASSWORD = process.env.DB_PASSWORD || ''; const DB_NAME = process.env.DB_NAME || 'user_profile_db'; if (!JWT_SECRET) { console.error('FATAL: JWT_SECRET missing – aborting startup'); process.exit(1); // container exits, Docker marks it unhealthy } // Create a MySQL pool for user_profile data const pool = mysql.createPool({ host: DB_HOST, port: DB_PORT, user: DB_USER, password: DB_PASSWORD, database: DB_NAME, connectionLimit: 10, }); // Test a quick query (optional) pool.query('SELECT 1', (err) => { if (err) { console.error('Error connecting to MySQL user_profile_db:', err.message); } else { console.log('Connected to MySQL user_profile_db'); } }); const app = express(); 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); app.disable('x-powered-by'); app.use(bodyParser.json()); app.use(express.json()); app.use( helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, }) ); app.get('/healthz', (req, res) => res.sendStatus(204)); // 204 No Content // Enable CORS with dynamic origin checking app.use( cors({ origin: (origin, callback) => { if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { console.error('Blocked by CORS:', origin); callback(new Error('Not allowed by CORS')); } }, methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: [ 'Authorization', 'Content-Type', 'Accept', 'Origin', 'X-Requested-With', ], credentials: true, }) ); // Handle preflight requests explicitly app.options('*', (req, res) => { res.setHeader('Access-Control-Allow-Origin', req.headers.origin || ''); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader( 'Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With' ); res.status(200).end(); }); // Add HTTP headers for security app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains' ); res.setHeader('Content-Security-Policy', "default-src 'self';"); res.removeHeader('X-Powered-By'); next(); }); // Force Content-Type to application/json on all responses app.use((req, res, next) => { res.setHeader('Content-Type', 'application/json'); next(); }); /* ------------------------------------------------------------------ USER REGISTRATION (MySQL) ------------------------------------------------------------------ */ /** * POST /api/register * Body: * username, password, firstname, lastname, email, zipcode, state, area, career_situation */ app.post('/api/register', async (req, res) => { const { username, password, firstname, lastname, email, zipcode, state, area, career_situation, phone_e164, sms_opt_in } = req.body; if ( !username || !password || !firstname || !lastname || !email || !zipcode || !state || !area ) { return res.status(400).json({ error: 'Missing required fields.' }); } // If they opted-in, phone must be in +E.164 format if (sms_opt_in && !/^\+\d{8,15}$/.test(phone_e164 || '')) { return res.status(400).json({ error: 'Phone must be +E.164 format.' }); } try { const hashedPassword = await bcrypt.hash(password, 10); // 1) Insert into user_profile const profileQuery = ` INSERT INTO user_profile (username, firstname, lastname, email, zipcode, state, area, career_situation, phone_e164, sms_opt_in) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; pool.query( profileQuery, [username, firstname, lastname, email, zipcode, state, area, career_situation,phone_e164 || null, sms_opt_in ? 1 : 0], (errProfile, resultProfile) => { if (errProfile) { console.error('Error inserting user_profile:', errProfile.message); return res .status(500) .json({ error: 'Failed to create user profile' }); } const newProfileId = resultProfile.insertId; // auto-increment PK from user_profile // 2) Insert into user_auth const authQuery = ` INSERT INTO user_auth (user_id, username, hashed_password) VALUES (?, ?, ?) `; pool.query(authQuery, [newProfileId, username, hashedPassword], async (errAuth) => { if (errAuth) { console.error('Error inserting user_auth:', errAuth.message); if (errAuth.code === 'ER_DUP_ENTRY') { return res .status(400) .json({ error: 'Username already exists' }); } return res.status(500).json({ error: 'Failed to register user' }); } // NEW: Now that we have the new user_profile.id (newProfileId), // generate a JWT to auto-sign them in. // We'll mimic what /api/signin does: const token = jwt.sign({ id: newProfileId }, JWT_SECRET, { expiresIn: '2h', }); // Optionally fetch or build a user object. We already know: // firstname, lastname, email, etc. from the request body. const userPayload = { username, firstname, lastname, email, zipcode, state, area, career_situation, phone_e164, sms_opt_in: !!sms_opt_in, }; return res.status(201).json({ message: 'User registered successfully', profileId: newProfileId, // the user_profile.id token, // NEW: the signed JWT user: userPayload // optional: user info }); }); } ); } catch (err) { console.error('Error during registration:', err.message); return res.status(500).json({ error: 'Internal server error' }); } }); /* ------------------------------------------------------------------ SIGN-IN (MySQL) ------------------------------------------------------------------ */ /** * POST /api/signin * Body: { username, password } * Returns JWT signed with user_profile.id */ app.post('/api/signin', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res .status(400) .json({ error: 'Both username and password are required' }); } // SELECT only the columns you actually have: // 'ua.id' is user_auth's primary key, // 'ua.user_id' references user_profile.id, // and we alias user_profile.id as profileId for clarity. const query = ` SELECT ua.id AS authId, ua.user_id AS userProfileId, ua.hashed_password, up.firstname, up.lastname, up.email, up.zipcode, up.state, up.area, up.career_situation FROM user_auth ua LEFT JOIN user_profile up ON ua.user_id = up.id WHERE ua.username = ? `; pool.query(query, [username], async (err, results) => { if (err) { console.error('Error querying user_auth:', err.message); return res .status(500) .json({ error: 'Failed to query user authentication data' }); } if (!results || results.length === 0) { return res.status(401).json({ error: 'Invalid username or password' }); } const row = results[0]; // Compare password with bcrypt const isMatch = await bcrypt.compare(password, row.hashed_password); if (!isMatch) { return res.status(401).json({ error: 'Invalid username or password' }); } // IMPORTANT: Use 'row.userProfileId' (from user_profile.id) in the token // so your '/api/user-profile' can decode it and do SELECT * FROM user_profile WHERE id=? const token = jwt.sign({ id: row.userProfileId }, JWT_SECRET, { expiresIn: '2h', }); // Return user info + token // 'authId' is user_auth's PK, but typically you won't need it on the client // 'row.userProfileId' is the actual user_profile.id res.status(200).json({ message: 'Login successful', token, id: row.userProfileId, // This is user_profile.id (important if your frontend needs it) user: { firstname: row.firstname, lastname: row.lastname, email: row.email, zipcode: row.zipcode, state: row.state, area: row.area, career_situation: row.career_situation, }, }); }); }); /* ------------------------------------------------------------------ CHECK USERNAME (MySQL) ------------------------------------------------------------------ */ app.get('/api/check-username/:username', (req, res) => { const { username } = req.params; const query = `SELECT username FROM user_auth WHERE username = ?`; pool.query(query, [username], (err, results) => { if (err) { console.error('Error checking username:', err.message); return res.status(500).json({ error: 'Database error' }); } if (results && results.length > 0) { res.status(200).json({ exists: true }); } else { res.status(200).json({ exists: false }); } }); }); /* ------------------------------------------------------------------ UPSERT USER PROFILE (MySQL) ------------------------------------------------------------------ */ /** * POST /api/user-profile * Headers: { Authorization: Bearer } * Body: { userName, firstName, lastName, email, zipCode, state, area, ... } * * If user_profile row exists (id = token.id), update * else insert */ app.post('/api/user-profile', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Authorization token is required' }); } let profileId; try { const decoded = jwt.verify(token, JWT_SECRET); profileId = decoded.id; // user_profile.id from sign-in } catch (error) { console.error('JWT verification failed:', error); return res.status(401).json({ error: 'Invalid or expired token' }); } const { userName, firstName, lastName, email, zipCode, state, area, careerSituation, interest_inventory_answers, riasec: riasec_scores, // NEW career_priorities, career_list, } = req.body; // Check if profile row exists pool.query( `SELECT * FROM user_profile WHERE id = ?`, [profileId], (err, results) => { if (err) { console.error('Error checking profile:', err.message); return res.status(500).json({ error: 'Database error' }); } const existingRow = results && results.length > 0 ? results[0] : null; // If creating a brand-new profile, ensure required fields if ( !existingRow && (!firstName || !lastName || !email || !zipCode || !state || !area) ) { return res .status(400) .json({ error: 'All fields are required for initial profile creation.' }); } // Final handling of interest inventory answers const finalAnswers = interest_inventory_answers !== undefined ? interest_inventory_answers : existingRow?.interest_inventory_answers || null; // final career priorities const finalCareerPriorities = career_priorities !== undefined ? career_priorities : existingRow?.career_priorities || null; // final career list const finalCareerList = career_list !== undefined ? career_list : existingRow?.career_list || null; // final userName const finalUserName = userName !== undefined ? userName : existingRow?.username || null; // final RIASEC scores const finalRiasec = req.body.riasec_scores ? JSON.stringify(req.body.riasec_scores) : existingRow.riasec_scores || null; if (existingRow) { // Update existing row const updateQuery = ` UPDATE user_profile SET username = ?, -- NEW firstname = ?, lastname = ?, email = ?, zipcode = ?, state = ?, area = ?, career_situation = ?, interest_inventory_answers = ?, riasec_scores = ?, -- NEW career_priorities = ?, career_list = ? WHERE id = ? `; const params = [ finalUserName, firstName || existingRow.firstname, lastName || existingRow.lastname, email || existingRow.email, zipCode || existingRow.zipcode, state || existingRow.state, area || existingRow.area, careerSituation || existingRow.career_situation, finalAnswers, finalRiasec, finalCareerPriorities, finalCareerList, profileId, ]; pool.query(updateQuery, params, (err2) => { if (err2) { console.error('Update error:', err2.message); return res .status(500) .json({ error: 'Failed to update user profile' }); } return res .status(200) .json({ message: 'User profile updated successfully' }); }); } else { // Insert a new profile const insertQuery = ` INSERT INTO user_profile (id, username, firstname, lastname, email, zipcode, state, area, career_situation, interest_inventory_answers, riasec, career_priorities, career_list) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; const params = [ profileId, finalUserName, firstName, lastName, email, zipCode, state, area, careerSituation || null, finalAnswers, finalRiasec, finalCareerPriorities, finalCareerList, ]; pool.query(insertQuery, params, (err3) => { if (err3) { console.error('Insert error:', err3.message); return res .status(500) .json({ error: 'Failed to create user profile' }); } return res .status(201) .json({ message: 'User profile created successfully', id: profileId }); }); } } ); }); /* ------------------------------------------------------------------ FETCH USER PROFILE (MySQL) ------------------------------------------------------------------ */ app.get('/api/user-profile', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Authorization token is required' }); } let profileId; try { const decoded = jwt.verify(token, JWT_SECRET); profileId = decoded.id; // user_profile.id } catch (error) { console.error('Error verifying token:', error.message); return res.status(401).json({ error: 'Invalid or expired token' }); } const query = 'SELECT * FROM user_profile WHERE id = ?'; pool.query(query, [profileId], (err, results) => { if (err) { console.error('Error fetching user profile:', err.message); return res.status(500).json({ error: 'Internal server error' }); } if (!results || results.length === 0) { return res.status(404).json({ error: 'User profile not found' }); } res.status(200).json(results[0]); }); }); /* ------------------------------------------------------------------ SALARY_INFO REMAINS IN SQLITE ------------------------------------------------------------------ */ app.get('/api/areas', (req, res) => { const { state } = req.query; if (!state) { return res.status(400).json({ error: 'State parameter is required' }); } // Use env when present (Docker), fall back for local dev const salaryDbPath = process.env.SALARY_DB_PATH // ← preferred || process.env.SALARY_DB // ← legacy || '/app/salary_info.db'; // final fallback const salaryDb = new sqlite3.Database( salaryDbPath, sqlite3.OPEN_READONLY, (err) => { if (err) { console.error('DB connect error:', err.message); return res .status(500) .json({ error: 'Failed to connect to database' }); } } ); const query = `SELECT DISTINCT AREA_TITLE FROM salary_data WHERE PRIM_STATE = ?`; salaryDb.all(query, [state], (err, rows) => { if (err) { console.error('Query error:', err.message); return res .status(500) .json({ error: 'Failed to fetch areas' }); } res.json({ areas: rows.map(r => r.AREA_TITLE) }); }); salaryDb.close(); }); /* ------------------------------------------------------------------ PREMIUM UPGRADE ENDPOINT ------------------------------------------------------------------ */ app.post('/api/activate-premium', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Authorization token is required' }); } let profileId; try { const decoded = jwt.verify(token, JWT_SECRET); profileId = decoded.id; } catch (error) { return res.status(401).json({ error: 'Invalid or expired token' }); } const query = ` UPDATE user_profile SET is_premium = 1, is_pro_premium = 1 WHERE id = ? `; pool.query(query, [profileId], (err) => { if (err) { console.error('Error updating premium status:', err.message); return res.status(500).json({ error: 'Failed to activate premium' }); } return res.status(200).json({ message: 'Premium activated successfully' }); }); }); /* ------------------------------------------------------------------ START SERVER ------------------------------------------------------------------ */ app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });