dev1/backend/server1.js

556 lines
17 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 pool from './config/mysqlPool.js'; // adjust path if needed
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 stage = env === 'staging' ? 'development' : env;
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
}
// Test a quick query (optional)
try {
const [rows] = await pool.query('SELECT 1');
console.log('Connected to MySQL user_profile_db');
} catch (err) {
console.error('Error connecting to MySQL user_profile_db:', err.message);
}
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);
}
/* ─── 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 (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);
const profileQuery = `
INSERT INTO user_profile
(username, firstname, lastname, email, zipcode, state, area, career_situation, phone_e164, sms_opt_in)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const [resultProfile] = await pool.query(profileQuery, [
username, firstname, lastname, email, zipcode, state, area,
career_situation, phone_e164 || null, sms_opt_in ? 1 : 0
]);
const newProfileId = resultProfile.insertId;
const authQuery = `
INSERT INTO user_auth (user_id, username, hashed_password)
VALUES (?, ?, ?)
`;
await pool.query(authQuery, [newProfileId, username, hashedPassword]);
const token = jwt.sign({ id: newProfileId }, JWT_SECRET, { expiresIn: '2h' });
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,
token,
user: userPayload
});
} 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 = ?
`;
try {
const [results] = await pool.query(query, [username]);
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,
},
});
} catch (err) {
console.error('Error querying user_auth:', err.message);
return res
.status(500)
.json({ error: 'Failed to query user authentication data' });
}
});
/* ------------------------------------------------------------------
CHECK USERNAME (MySQL)
------------------------------------------------------------------ */
app.get('/api/check-username/:username', async (req, res) => {
const { username } = req.params;
try {
const [results] = await pool.query(`SELECT username FROM user_auth WHERE username = ?`, [username]);
res.status(200).json({ exists: results.length > 0 });
} catch (err) {
console.error('Error checking username:', err.message);
res.status(500).json({ error: 'Database error' });
}
});
/* ------------------------------------------------------------------
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', async (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) {
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,
career_priorities,
career_list,
} = req.body;
try {
const [results] = await pool.query(
`SELECT * FROM user_profile WHERE id = ?`,
[profileId]
);
const existingRow = results.length > 0 ? results[0] : null;
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;
const finalUserName =
userName !== undefined ? userName : existingRow?.username || null;
const finalRiasec = riasec_scores
? JSON.stringify(riasec_scores)
: existingRow?.riasec_scores || null;
if (existingRow) {
const updateQuery = `
UPDATE user_profile
SET
username = ?,
firstname = ?,
lastname = ?,
email = ?,
zipcode = ?,
state = ?,
area = ?,
career_situation = ?,
interest_inventory_answers = ?,
riasec_scores = ?,
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,
];
await pool.query(updateQuery, params);
return res
.status(200)
.json({ message: 'User profile updated successfully' });
} else {
const insertQuery = `
INSERT INTO user_profile
(id, username, firstname, lastname, email, zipcode, state, area,
career_situation, interest_inventory_answers, riasec_scores,
career_priorities, career_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
profileId,
finalUserName,
firstName,
lastName,
email,
zipCode,
state,
area,
careerSituation || null,
finalAnswers,
finalRiasec,
finalCareerPriorities,
finalCareerList,
];
await pool.query(insertQuery, params);
return res
.status(201)
.json({ message: 'User profile created successfully', id: profileId });
}
} catch (err) {
console.error('Error upserting user profile:', err.message);
return res.status(500).json({ error: 'Internal server error' });
}
});
/* ------------------------------------------------------------------
FETCH USER PROFILE (MySQL)
------------------------------------------------------------------ */
app.get('/api/user-profile', async (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' });
}
try {
const [results] = await pool.query('SELECT * FROM user_profile WHERE id = ?', [profileId]);
if (!results || results.length === 0) {
return res.status(404).json({ error: 'User profile not found' });
}
res.status(200).json(results[0]);
} catch (err) {
console.error('Error fetching user profile:', err.message);
res.status(500).json({ error: 'Internal server error' });
}
});
/* ------------------------------------------------------------------
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', async (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' });
}
try {
await pool.query(`
UPDATE user_profile
SET is_premium = 1,
is_pro_premium = 1
WHERE id = ?
`, [profileId]);
res.status(200).json({ message: 'Premium activated successfully' });
} catch (err) {
console.error('Error updating premium status:', err.message);
res.status(500).json({ error: 'Failed to activate premium' });
}
});
/* ------------------------------------------------------------------
START SERVER
------------------------------------------------------------------ */
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});