Removed UserID since ChatGPT can't code two similar variables
This commit is contained in:
parent
491fadbefd
commit
d4919c31a5
@ -5,17 +5,13 @@ 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';
|
||||
|
||||
import sqlite3 from 'sqlite3';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@ -33,7 +29,6 @@ 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,
|
||||
@ -41,7 +36,7 @@ const pool = mysql.createPool({
|
||||
user: DB_USER,
|
||||
password: DB_PASSWORD,
|
||||
database: DB_NAME,
|
||||
connectionLimit: 10, // optional
|
||||
connectionLimit: 10,
|
||||
});
|
||||
|
||||
// Test a quick query (optional)
|
||||
@ -55,7 +50,13 @@ pool.query('SELECT 1', (err) => {
|
||||
|
||||
const app = express();
|
||||
const PORT = 5000;
|
||||
const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'https://dev1.aptivaai.com'];
|
||||
|
||||
// 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());
|
||||
@ -79,7 +80,13 @@ app.use(
|
||||
}
|
||||
},
|
||||
methods: ['GET', 'POST', 'OPTIONS'],
|
||||
allowedHeaders: ['Authorization', 'Content-Type', 'Accept', 'Origin', 'X-Requested-With'],
|
||||
allowedHeaders: [
|
||||
'Authorization',
|
||||
'Content-Type',
|
||||
'Accept',
|
||||
'Origin',
|
||||
'X-Requested-With',
|
||||
],
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
@ -88,15 +95,21 @@ app.use(
|
||||
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.setHeader(
|
||||
'Access-Control-Allow-Headers',
|
||||
'Authorization, Content-Type, Accept, Origin, X-Requested-With'
|
||||
);
|
||||
res.status(200).end();
|
||||
});
|
||||
|
||||
// Add HTTP headers for security and caching
|
||||
// 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(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=31536000; includeSubDomains'
|
||||
);
|
||||
res.setHeader('Content-Security-Policy', "default-src 'self';");
|
||||
res.removeHeader('X-Powered-By');
|
||||
next();
|
||||
@ -108,11 +121,16 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
// =============== USER REGISTRATION (MySQL) ===============
|
||||
// /api/register
|
||||
/* ------------------------------------------------------------------
|
||||
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 {
|
||||
userId, // random ID from the front end
|
||||
username,
|
||||
password,
|
||||
firstname,
|
||||
@ -121,48 +139,70 @@ app.post('/api/register', async (req, res) => {
|
||||
zipcode,
|
||||
state,
|
||||
area,
|
||||
career_situation
|
||||
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);
|
||||
|
||||
// Insert row in user_profile, storing both user_id (random) and letting id auto-increment
|
||||
// 1) Insert into user_profile
|
||||
const profileQuery = `
|
||||
INSERT INTO user_profile
|
||||
(user_id, firstname, lastname, email, zipcode, state, area, career_situation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(firstname, lastname, email, zipcode, state, area, career_situation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
pool.query(
|
||||
profileQuery,
|
||||
[userId, firstname, lastname, email, zipcode, state, area, career_situation],
|
||||
[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' });
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Failed to create user profile' });
|
||||
}
|
||||
|
||||
const newProfileAutoId = resultProfile.insertId; // auto-increment PK
|
||||
const newProfileId = resultProfile.insertId; // auto-increment PK
|
||||
|
||||
// Insert into user_auth, referencing the auto-increment PK
|
||||
// 2) Insert into user_auth, referencing user_profile.id
|
||||
const authQuery = `
|
||||
INSERT INTO user_auth (user_id, username, hashed_password)
|
||||
VALUES (?, ?, ?)
|
||||
`;
|
||||
pool.query(authQuery, [newProfileAutoId, username, hashedPassword], (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' });
|
||||
pool.query(
|
||||
authQuery,
|
||||
[newProfileId, username, hashedPassword],
|
||||
(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' });
|
||||
}
|
||||
return res.status(500).json({ error: 'Failed to register user' });
|
||||
|
||||
return res.status(201).json({
|
||||
message: 'User registered successfully',
|
||||
profileId: newProfileId, // the user_profile.id
|
||||
});
|
||||
}
|
||||
return res.status(201).json({
|
||||
message: 'User registered successfully',
|
||||
dbId: newProfileAutoId, // the auto-increment PK
|
||||
customId: userId, // the random ID
|
||||
});
|
||||
});
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
@ -171,36 +211,47 @@ app.post('/api/register', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// =============== SIGN-IN (MySQL) ===============
|
||||
app.post('/api/signin', async (req, res) => {
|
||||
/* ------------------------------------------------------------------
|
||||
SIGN-IN (MySQL)
|
||||
------------------------------------------------------------------ */
|
||||
/**
|
||||
* POST /api/signin
|
||||
* Body: { username, password }
|
||||
* Returns JWT signed with user_profile.id
|
||||
*/
|
||||
app.post('/api/signin', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Both username and password are required' });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Both username and password are required' });
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
user_auth.user_id,
|
||||
user_auth.hashed_password,
|
||||
user_profile.id,
|
||||
SELECT
|
||||
user_auth.user_id,
|
||||
user_auth.hashed_password,
|
||||
user_profile.firstname,
|
||||
user_profile.lastname,
|
||||
user_profile.email,
|
||||
user_profile.zipcode,
|
||||
user_profile.state,
|
||||
user_profile.area,
|
||||
user_profile.is_premium,
|
||||
user_profile.is_pro_premium,
|
||||
user_profile.career_situation,
|
||||
user_profile.email,
|
||||
user_profile.firstname,
|
||||
user_profile.lastname,
|
||||
user_profile.career_priorities,
|
||||
user_profile.career_list
|
||||
FROM user_auth
|
||||
LEFT JOIN user_profile ON user_auth.user_id = user_profile.id
|
||||
LEFT JOIN user_profile ON user_auth.user_id = user_profile.id
|
||||
WHERE user_auth.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' });
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Failed to query user authentication data' });
|
||||
}
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
@ -208,22 +259,29 @@ app.post('/api/signin', async (req, res) => {
|
||||
}
|
||||
|
||||
const row = results[0];
|
||||
// Compare password
|
||||
const isMatch = await bcrypt.compare(password, row.hashed_password);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ userId: row.id }, SECRET_KEY, { expiresIn: '2h' });
|
||||
// The user_profile id is stored in user_auth.user_id
|
||||
const token = jwt.sign({ id: row.user_id }, SECRET_KEY, {
|
||||
expiresIn: '2h',
|
||||
});
|
||||
|
||||
// Return the user info + token
|
||||
res.status(200).json({
|
||||
message: 'Login successful',
|
||||
token,
|
||||
userId: row.id,
|
||||
userId: row.user_id, // The user_profile.id
|
||||
user: {
|
||||
user_id: row.user_id,
|
||||
firstname: row.firstname,
|
||||
lastname: row.lastname,
|
||||
email: row.email,
|
||||
zipcode: row.zipcode,
|
||||
state: row.state,
|
||||
area: row.area,
|
||||
is_premium: row.is_premium,
|
||||
is_pro_premium: row.is_pro_premium,
|
||||
career_situation: row.career_situation,
|
||||
@ -234,7 +292,9 @@ app.post('/api/signin', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// =============== CHECK USERNAME (MySQL) ===============
|
||||
/* ------------------------------------------------------------------
|
||||
CHECK USERNAME (MySQL)
|
||||
------------------------------------------------------------------ */
|
||||
app.get('/api/check-username/:username', (req, res) => {
|
||||
const { username } = req.params;
|
||||
const query = `SELECT username FROM user_auth WHERE username = ?`;
|
||||
@ -251,17 +311,27 @@ app.get('/api/check-username/:username', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// =============== UPSERT USER PROFILE (MySQL) ===============
|
||||
/* ------------------------------------------------------------------
|
||||
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 userId;
|
||||
let profileId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, SECRET_KEY);
|
||||
userId = decoded.userId;
|
||||
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' });
|
||||
@ -280,110 +350,134 @@ app.post('/api/user-profile', (req, res) => {
|
||||
career_list,
|
||||
} = req.body;
|
||||
|
||||
// Check existing profile
|
||||
pool.query(`SELECT * FROM user_profile WHERE user_id = ?`, [userId], (err, results) => {
|
||||
if (err) {
|
||||
console.error('Error checking profile:', err.message);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
// 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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
const existingRow = results && 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;
|
||||
|
||||
if (existingRow) {
|
||||
// Update
|
||||
const updateQuery = `
|
||||
UPDATE user_profile
|
||||
SET firstname = ?, lastname = ?, email = ?, zipcode = ?, state = ?, area = ?, career_situation = ?,
|
||||
interest_inventory_answers = ?, career_priorities = ?, career_list = ?
|
||||
WHERE user_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,
|
||||
userId
|
||||
];
|
||||
pool.query(updateQuery, params, (err2, result2) => {
|
||||
if (err2) {
|
||||
console.error('Update error:', err2.message);
|
||||
return res.status(500).json({ error: 'Failed to update user profile' });
|
||||
}
|
||||
res.status(200).json({ message: 'User profile updated successfully' });
|
||||
});
|
||||
} else {
|
||||
// Insert new profile
|
||||
const insertQuery = `
|
||||
INSERT INTO user_profile
|
||||
(user_id, firstname, lastname, email, zipcode, state, area, career_situation,
|
||||
interest_inventory_answers, career_priorities, career_list)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const params = [
|
||||
userId,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
zipCode,
|
||||
state,
|
||||
area,
|
||||
careerSituation || null,
|
||||
finalAnswers,
|
||||
finalCareerPriorities,
|
||||
finalCareerList,
|
||||
];
|
||||
pool.query(insertQuery, params, (err3, result3) => {
|
||||
if (err3) {
|
||||
console.error('Insert error:', err3.message);
|
||||
return res.status(500).json({ error: 'Failed to create user profile' });
|
||||
}
|
||||
res.status(201).json({ message: 'User profile created successfully', id: result3.insertId });
|
||||
});
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
// =============== FETCH USER PROFILE (MySQL) ===============
|
||||
/* ------------------------------------------------------------------
|
||||
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 userId;
|
||||
|
||||
let profileId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, SECRET_KEY);
|
||||
userId = decoded.userId;
|
||||
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 user_id = ?';
|
||||
pool.query(query, [userId], (err, results) => {
|
||||
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' });
|
||||
@ -395,20 +489,28 @@ app.get('/api/user-profile', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// =============== SALARY_INFO REMAINS IN SQLITE ===============
|
||||
/* ------------------------------------------------------------------
|
||||
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 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) => {
|
||||
@ -428,30 +530,29 @@ app.get('/api/areas', (req, res) => {
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
PREMIUM UPGRADE ENDPOINT
|
||||
------------------------------------------------------------------ */
|
||||
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 userId; // Will hold the auto-increment "id" from user_profile
|
||||
let profileId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, SECRET_KEY);
|
||||
userId = decoded.userId; // => user_profile.id (auto-inc PK)
|
||||
profileId = decoded.id;
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
|
||||
// Use MySQL pool.query instead of db.run (which is for SQLite)
|
||||
const query = `
|
||||
UPDATE user_profile
|
||||
SET is_premium = 1,
|
||||
is_pro_premium = 1
|
||||
WHERE id = ?
|
||||
`;
|
||||
pool.query(query, [userId], (err, results) => {
|
||||
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' });
|
||||
@ -460,7 +561,9 @@ app.post('/api/activate-premium', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// =============== START SERVER ===============
|
||||
/* ------------------------------------------------------------------
|
||||
START SERVER
|
||||
------------------------------------------------------------------ */
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
@ -39,6 +39,13 @@ body, #root {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dialog-overlay,
|
||||
[data-state="open"].dialog-overlay {
|
||||
background-color: transparent !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
/* Animation for the logo */
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
|
@ -249,9 +249,14 @@ function CareerExplorer() {
|
||||
if (profileData.career_list) {
|
||||
setCareerList(JSON.parse(profileData.career_list));
|
||||
}
|
||||
} else {
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
if (!profileData.career_priorities) {
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
} else {
|
||||
setShowModal(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching user profile:', err);
|
||||
setShowModal(true);
|
||||
|
@ -406,7 +406,7 @@ function EducationalProgramsPage() {
|
||||
<span className="relative inline-block">
|
||||
<span>Why no courses?</span>
|
||||
<span
|
||||
className="ml-1 inline-flex h-2.5 w-2.5 items-center justify-center
|
||||
className="top-0 left-0 -translate-y-3/4 translate-x-1/8 ml-.5 inline-flex h-2.5 w-2.5 items-center justify-center
|
||||
rounded-full bg-blue-500 text-[10px] font-italics text-white cursor-help"
|
||||
title="Abilities are more innate in nature, and difficult to offer courses for them."
|
||||
>
|
||||
|
@ -48,8 +48,7 @@ function SignUp() {
|
||||
const [areas, setAreas] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
const [loadingAreas, setLoadingAreas] = useState(false);
|
||||
const [frontendUserId] = useState(() => Math.floor(Math.random() * 1000000000));
|
||||
|
||||
|
||||
// new states
|
||||
const [showCareerSituations, setShowCareerSituations] = useState(false);
|
||||
const [selectedSituation, setSelectedSituation] = useState(null);
|
||||
@ -172,7 +171,6 @@ function SignUp() {
|
||||
|
||||
try {
|
||||
console.log("Payload sent to backend:", {
|
||||
userId: frontendUserId,
|
||||
username, password, firstname, lastname, email, zipcode, state, area,
|
||||
career_situation: selectedSituation.id
|
||||
});
|
||||
@ -181,7 +179,6 @@ function SignUp() {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: frontendUserId,
|
||||
username,
|
||||
password,
|
||||
firstname,
|
||||
@ -212,83 +209,154 @@ function SignUp() {
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||
{!showCareerSituations ? (
|
||||
<div className="w-full max-w-md bg-white p-6 rounded-lg shadow-lg">
|
||||
{!showCareerSituations ? (
|
||||
<>
|
||||
<h2 className="mb-4 text-2xl font-semibold text-center">Sign Up</h2>
|
||||
{error && (
|
||||
<div className="mb-4 p-2 text-sm text-red-600 bg-red-100 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form className="space-y-3" onSubmit={handleNextClick}>
|
||||
<input className="w-full px-3 py-2 border rounded-md" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||
<input type="password" className="w-full px-3 py-2 border rounded-md" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
<input className="w-full px-3 py-2 border rounded-md" placeholder="Retype Password" type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)}/>
|
||||
<input className="w-full px-3 py-2 border rounded-md" placeholder="First Name" value={firstname} onChange={(e) => setFirstname(e.target.value)} />
|
||||
<input className="w-full px-3 py-2 border rounded-md" placeholder="Last Name" value={lastname} onChange={(e) => setLastname(e.target.value)} />
|
||||
<input className="w-full px-3 py-2 border rounded-md" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<input className="w-full px-3 py-2 border rounded-md" placeholder="Retype Email" type="email" value={confirmEmail} onChange={(e) => setConfirmEmail(e.target.value)}/>
|
||||
<input className="w-full px-3 py-2 border rounded-md" placeholder="Zip Code" value={zipcode} onChange={(e) => setZipcode(e.target.value)} />
|
||||
<select className="w-full px-3 py-2 border rounded-md" value={state} onChange={(e) => setState(e.target.value)}><option value="">Select State</option>
|
||||
{states.map((s) => (
|
||||
<option key={s.code} value={s.code}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="relative w-full"><select className="w-full px-3 py-2 border rounded-md" value={area} onChange={(e) => setArea(e.target.value)} disabled={loadingAreas} // Disable select while loading
|
||||
>
|
||||
<option value="">Select Area</option>
|
||||
{areas.map((a, i) => (
|
||||
<option key={i} value={a}>{a}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{loadingAreas && (
|
||||
<span className="absolute right-3 top-2.5 text-gray-400 text-sm animate-pulse">
|
||||
Loading...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
Next
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="mb-4 text-xl font-semibold text-center">Where are you in your career journey right now?</h2>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{careerSituations.map((situation) => (
|
||||
<SituationCard
|
||||
key={situation.id}
|
||||
title={situation.title}
|
||||
description={situation.description}
|
||||
selected={selectedSituation?.id === situation.id}
|
||||
onClick={() => {
|
||||
setSelectedSituation(situation);
|
||||
setShowPrompt(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{showPrompt && <div className="mt-4 p-4 bg-yellow-200">Modal should be open!</div>}
|
||||
<PromptModal
|
||||
open={showPrompt}
|
||||
title="Confirm Your Selection"
|
||||
message={`Are you sure you want to select "${selectedSituation?.title}"? You can change this later in your profile settings.`}
|
||||
confirmText="Confirm"
|
||||
cancelText="Cancel"
|
||||
onConfirm={handleSituationConfirm}
|
||||
onCancel={() => setShowPrompt(false)}
|
||||
/>
|
||||
</>
|
||||
<h2 className="mb-4 text-2xl font-semibold text-center">Sign Up</h2>
|
||||
{error && (
|
||||
<div className="mb-4 p-2 text-sm text-red-600 bg-red-100 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form className="space-y-3" onSubmit={handleNextClick}>
|
||||
<input
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
placeholder="Retype Password"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
placeholder="First Name"
|
||||
value={firstname}
|
||||
onChange={(e) => setFirstname(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
placeholder="Last Name"
|
||||
value={lastname}
|
||||
onChange={(e) => setLastname(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
placeholder="Retype Email"
|
||||
type="email"
|
||||
value={confirmEmail}
|
||||
onChange={(e) => setConfirmEmail(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
placeholder="Zip Code"
|
||||
value={zipcode}
|
||||
onChange={(e) => setZipcode(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
value={state}
|
||||
onChange={(e) => setState(e.target.value)}
|
||||
>
|
||||
<option value="">Select State</option>
|
||||
{states.map((s) => (
|
||||
<option key={s.code} value={s.code}>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="relative w-full">
|
||||
<label>
|
||||
<span
|
||||
title="Selecting an Area will allow us to provide regional salary data for your career choices."
|
||||
className="absolute top-2 right-2 translate-x-7 -translate-y-3
|
||||
inline-flex h-4 w-4 items-center justify-center
|
||||
rounded-full bg-blue-500 text-xs font-bold text-white cursor-help"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
id="area"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
value={area}
|
||||
onChange={(e) => setArea(e.target.value)}
|
||||
disabled={loadingAreas}
|
||||
>
|
||||
<option value="">Select Area</option>
|
||||
{areas.map((a, i) => (
|
||||
<option key={i} value={a}>
|
||||
{a}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{loadingAreas && (
|
||||
<span className="absolute right-3 top-2.5 text-gray-400 text-sm animate-pulse">
|
||||
Loading...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
Next
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
) : (
|
||||
<div className="w-full max-w-screen-lg mx-auto bg-white p-6 rounded-lg shadow-lg">
|
||||
<h2 className="mb-4 text-xl font-semibold text-center">
|
||||
Where are you in your career journey right now?
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{careerSituations.map((situation) => (
|
||||
<SituationCard
|
||||
key={situation.id}
|
||||
title={situation.title}
|
||||
description={situation.description}
|
||||
selected={selectedSituation?.id === situation.id}
|
||||
onClick={() => {
|
||||
setSelectedSituation(situation);
|
||||
setShowPrompt(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPrompt && (
|
||||
<PromptModal
|
||||
open={true}
|
||||
title="Confirm Your Selection"
|
||||
message={`Are you sure you want to select "${selectedSituation?.title}"? You can change this later in your profile settings.`}
|
||||
confirmText="Confirm"
|
||||
cancelText="Cancel"
|
||||
onConfirm={handleSituationConfirm}
|
||||
onCancel={() => setShowPrompt(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default SignUp;
|
21
src/components/ui/FadingPromptModal.js
Normal file
21
src/components/ui/FadingPromptModal.js
Normal file
@ -0,0 +1,21 @@
|
||||
// FadingPromptModal.jsx
|
||||
import React from 'react';
|
||||
import PromptModal from './PromptModal.js';
|
||||
|
||||
export default function FadingPromptModal({ open, ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
fixed inset-0 z-50
|
||||
flex items-center justify-center
|
||||
transition-opacity duration-300
|
||||
${open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}
|
||||
`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gray-800 bg-opacity-50 z-10" />
|
||||
<div className="relative z-20">
|
||||
<PromptModal open={open} {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user