dev1/backend/server.js

644 lines
19 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = 5000;
// 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());
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', '*');
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 <token> }
* 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 || '/app/data/salary_info.db';
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}`);
});