590 lines
18 KiB
JavaScript
Executable File
590 lines
18 KiB
JavaScript
Executable File
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 SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey';
|
|
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';
|
|
|
|
// 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,
|
|
})
|
|
);
|
|
|
|
// 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,
|
|
} = req.body;
|
|
|
|
if (
|
|
!username ||
|
|
!password ||
|
|
!firstname ||
|
|
!lastname ||
|
|
!email ||
|
|
!zipcode ||
|
|
!state ||
|
|
!area
|
|
) {
|
|
return res.status(400).json({ error: 'Missing required fields.' });
|
|
}
|
|
|
|
try {
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
|
|
// 1) Insert into user_profile
|
|
const profileQuery = `
|
|
INSERT INTO user_profile
|
|
(firstname, lastname, email, zipcode, state, area, career_situation)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`;
|
|
pool.query(
|
|
profileQuery,
|
|
[firstname, lastname, email, zipcode, state, area, career_situation],
|
|
(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 }, SECRET_KEY, {
|
|
expiresIn: '2h',
|
|
});
|
|
|
|
// Optionally fetch or build a user object. We already know:
|
|
// firstname, lastname, email, etc. from the request body.
|
|
const userPayload = {
|
|
firstname,
|
|
lastname,
|
|
email,
|
|
zipcode,
|
|
state,
|
|
area,
|
|
career_situation,
|
|
// any other fields you want
|
|
};
|
|
|
|
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 }, SECRET_KEY, {
|
|
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: { 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, SECRET_KEY);
|
|
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 {
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
zipCode,
|
|
state,
|
|
area,
|
|
careerSituation,
|
|
interest_inventory_answers,
|
|
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.' });
|
|
}
|
|
|
|
const finalAnswers =
|
|
interest_inventory_answers !== undefined
|
|
? interest_inventory_answers
|
|
: existingRow?.interest_inventory_answers || null;
|
|
|
|
const finalCareerPriorities =
|
|
career_priorities !== undefined
|
|
? career_priorities
|
|
: existingRow?.career_priorities || null;
|
|
|
|
const finalCareerList =
|
|
career_list !== undefined
|
|
? career_list
|
|
: existingRow?.career_list || null;
|
|
|
|
if (existingRow) {
|
|
// Update the existing user_profile
|
|
const updateQuery = `
|
|
UPDATE user_profile
|
|
SET firstname = ?, lastname = ?, email = ?, zipcode = ?, state = ?, area = ?,
|
|
career_situation = ?, interest_inventory_answers = ?, career_priorities = ?,
|
|
career_list = ?
|
|
WHERE id = ?
|
|
`;
|
|
const params = [
|
|
firstName || existingRow.firstname,
|
|
lastName || existingRow.lastname,
|
|
email || existingRow.email,
|
|
zipCode || existingRow.zipcode,
|
|
state || existingRow.state,
|
|
area || existingRow.area,
|
|
careerSituation || existingRow.career_situation,
|
|
finalAnswers,
|
|
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 (the user_auth record exists, but the user_profile row is missing)
|
|
const insertQuery = `
|
|
INSERT INTO user_profile
|
|
(id, firstname, lastname, email, zipcode, state, area, career_situation,
|
|
interest_inventory_answers, career_priorities, career_list)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`;
|
|
const params = [
|
|
profileId, // Force the row's primary key to match the existing user ID
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
zipCode,
|
|
state,
|
|
area,
|
|
careerSituation || null,
|
|
finalAnswers,
|
|
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, SECRET_KEY);
|
|
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' });
|
|
}
|
|
|
|
const salaryDbPath = path.resolve(
|
|
'/home/jcoakley/aptiva-dev1-app/salary_info.db'
|
|
);
|
|
const salaryDb = new sqlite3.Database(
|
|
salaryDbPath,
|
|
sqlite3.OPEN_READONLY,
|
|
(err) => {
|
|
if (err) {
|
|
console.error('Error connecting to database:', 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('Error executing query:', err.message);
|
|
return res.status(500).json({ error: 'Failed to fetch areas' });
|
|
}
|
|
const areas = rows.map((row) => row.AREA_TITLE);
|
|
res.json({ areas });
|
|
});
|
|
|
|
salaryDb.close((err) => {
|
|
if (err) {
|
|
console.error('Error closing the salary_info.db:', err.message);
|
|
}
|
|
});
|
|
});
|
|
|
|
/* ------------------------------------------------------------------
|
|
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, SECRET_KEY);
|
|
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}`);
|
|
});
|