Added Premium Route Guard

This commit is contained in:
Josh 2025-05-01 15:21:56 +00:00
parent 9618bc0ea9
commit d207dce5d4
8 changed files with 178 additions and 55 deletions

View File

@ -219,10 +219,20 @@ app.post('/api/signin', async (req, res) => {
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.zipcode
// JOIN user_profile to fetch is_premium, email, or whatever columns you need
const query = `
SELECT
user_auth.user_id,
user_auth.hashed_password,
user_profile.zipcode,
user_profile.is_premium,
user_profile.email,
user_profile.firstname,
user_profile.lastname
FROM user_auth
LEFT JOIN user_profile ON user_auth.user_id = user_profile.user_id
WHERE user_auth.username = ?`;
WHERE user_auth.username = ?
`;
db.get(query, [username], async (err, row) => {
if (err) {
@ -230,31 +240,41 @@ app.post('/api/signin', async (req, res) => {
return res.status(500).json({ error: 'Failed to query user authentication data' });
}
console.log('Row data:', row); // Log the result of the query
// If no matching username
if (!row) {
return res.status(401).json({ error: 'Invalid username or password' });
}
// Verify password
// Verify the password using bcrypt
const isMatch = await bcrypt.compare(password, row.hashed_password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid username or password' });
}
// Ensure that you're using the correct user_id
const { user_id, zipcode } = row;
// user_id from the row
const { user_id } = row;
console.log('UserID:', user_id);
console.log('ZIP Code:', zipcode); // Log the ZIP code to ensure it's correct
// Send correct token with user_id
// Generate JWT
const token = jwt.sign({ userId: user_id }, SECRET_KEY, { expiresIn: '2h' });
// You can optionally return the ZIP code or any other data as part of the response
res.status(200).json({ message: 'Login successful', token, userId: user_id, zipcode });
// Return user object including is_premium and other columns
// The front end can store this in state (e.g. setUser).
res.status(200).json({
message: 'Login successful',
token,
userId: user_id,
user: {
user_id,
firstname: row.firstname,
lastname: row.lastname,
email: row.email,
zipcode: row.zipcode,
is_premium: row.is_premium,
}
});
});
});
// Route to fetch user profile

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
import PremiumRoute from './components/PremiumRoute.js';
import SessionExpiredHandler from './components/SessionExpiredHandler.js';
import GettingStarted from './components/GettingStarted.js';
import SignIn from './components/SignIn.js';
@ -8,8 +9,8 @@ import InterestInventory from './components/InterestInventory.js';
import Dashboard from './components/Dashboard.js';
import UserProfile from './components/UserProfile.js';
import FinancialProfileForm from './components/FinancialProfileForm.js';
import MilestoneTracker from "./components/MilestoneTracker.js";
import Paywall from "./components/Paywall.js";
import MilestoneTracker from './components/MilestoneTracker.js';
import Paywall from './components/Paywall.js';
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
import MultiScenarioView from './components/MultiScenarioView.js';
@ -17,18 +18,22 @@ function App() {
const navigate = useNavigate();
const location = useLocation();
// Track whether user is authenticated
const [isAuthenticated, setIsAuthenticated] = useState(() => {
return !!localStorage.getItem('token');
});
// Hide the Upgrade CTA on these paths:
// Track the user object, including is_premium
const [user, setUser] = useState(null);
// We hide the "Upgrade to Premium" CTA if we're already on certain premium routes
const premiumPaths = [
'/milestone-tracker',
'/paywall',
'/financial-profile',
'/multi-scenario',
'/premium-onboarding'
];
const showPremiumCTA = !premiumPaths.includes(location.pathname);
return (
@ -51,28 +56,69 @@ function App() {
{/* Main Content */}
<main className="flex-1 p-6">
<Routes>
{/* Default to /signin if no path */}
<Route path="/" element={<Navigate to="/signin" />} />
{/* Public routes */}
<Route
path="/signin"
element={<SignIn setIsAuthenticated={setIsAuthenticated} />}
element={
<SignIn
setIsAuthenticated={setIsAuthenticated}
setUser={setUser} // We pass setUser so SignIn can store user details
/>
}
/>
<Route path="/signup" element={<SignUp />} />
{/* Paywall - open to everyone for subscription */}
<Route path="/paywall" element={<Paywall />} />
{/* Authenticated routes */}
{isAuthenticated && (
<>
<Route path="/getting-started" element={<GettingStarted />} />
<Route path="/interest-inventory" element={<InterestInventory />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<UserProfile />} />
<Route path="/milestone-tracker" element={<MilestoneTracker />} />
<Route path="/financial-profile" element={<FinancialProfileForm />} />
<Route path="/premium-onboarding" element={<OnboardingContainer />} />
<Route path="/multi-scenario" element={<MultiScenarioView />} />
{/* Premium-only routes use <PremiumRoute> */}
<Route
path="/milestone-tracker"
element={
<PremiumRoute user={user}>
<MilestoneTracker />
</PremiumRoute>
}
/>
<Route
path="/financial-profile"
element={
<PremiumRoute user={user}>
<FinancialProfileForm />
</PremiumRoute>
}
/>
<Route
path="/premium-onboarding"
element={
<PremiumRoute user={user}>
<OnboardingContainer />
</PremiumRoute>
}
/>
<Route
path="/multi-scenario"
element={
<PremiumRoute user={user}>
<MultiScenarioView />
</PremiumRoute>
}
/>
</>
)}
{/* Fallback */}
{/* Fallback if route not found */}
<Route path="*" element={<Navigate to="/signin" />} />
</Routes>
</main>

View File

@ -84,7 +84,7 @@ const CareerSearch = ({ onCareerSelected }) => {
</datalist>
<button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
Confirm New Career
Confirm
</button>
</div>
);

View File

@ -1,12 +1,19 @@
// Paywall.js
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
const Paywall = () => {
const navigate = useNavigate();
const { state } = useLocation();
// Extract the selectedCareer from location state
const { selectedCareer } = state || {};
const handleSubscribe = () => {
navigate('/milestone-tracker');
// Once the user subscribes, navigate to MilestoneTracker
navigate('/PremiumOnboarding', {
state: {
selectedCareer,
},
});
};
return (

View File

@ -111,15 +111,16 @@ function PopoutPanel({
closePanel();
}
// Original PlanMyPath logic
async function handlePlanMyPath() {
if (!token) {
alert("You need to be logged in to create a career path.");
return;
}
try {
// 1) Fetch existing career profiles (a.k.a. "careerPaths")
const allPathsResponse = await fetch(
`${process.env.REACT_APP_API_URL}/premium/planned-path/all`,
`${process.env.REACT_APP_API_URL}/premium/career-profile/all`,
{
method: "GET",
headers: {
@ -128,47 +129,67 @@ function PopoutPanel({
},
}
);
if (!allPathsResponse.ok) throw new Error(`HTTP error ${allPathsResponse.status}`);
const { careerPath } = await allPathsResponse.json();
const match = careerPath.find((path) => path.career_name === data.title);
if (!allPathsResponse.ok) {
throw new Error(`HTTP error ${allPathsResponse.status}`);
}
// The server returns { careerPaths: [...] }
const { careerPaths } = await allPathsResponse.json();
// 2) Check if there's already a career path with the same name
const match = careerPaths.find((cp) => cp.career_name === data.title);
if (match) {
// If a path already exists for this career, confirm with the user
const decision = window.confirm(
`A career path for "${data.title}" already exists.\n\n` +
`A career path (scenario) for "${data.title}" already exists.\n\n` +
`Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
);
if (decision) {
// Reload existing path → go to Paywall
navigate("/paywall", {
state: {
selectedCareer: { career_path_id: match.id, career_name: data.title },
selectedCareer: {
career_path_id: match.id, // 'id' is the primary key from the DB
career_name: data.title,
},
},
});
return;
}
}
const newCareerPath = {
career_path_id: uuidv4(),
career_name: data.title,
};
// 3) Otherwise, create a new career profile using POST /premium/career-profile
const newResponse = await fetch(
`${process.env.REACT_APP_API_URL}/premium/planned-path`,
`${process.env.REACT_APP_API_URL}/premium/career-profile`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(newCareerPath),
body: JSON.stringify({
// The server expects at least career_name
career_name: data.title,
// Optionally pass scenario_title, start_date, etc.
}),
}
);
if (!newResponse.ok) throw new Error("Failed to create new career path.");
navigate("/milestone-tracker", {
if (!newResponse.ok) {
throw new Error("Failed to create new career path.");
}
// The server returns something like { message: 'Career profile upserted.', career_path_id: 'xxx-xxx' }
const result = await newResponse.json();
const newlyCreatedId = result?.career_path_id;
// 4) Navigate to /paywall, passing the newly created career_path_id
navigate("/paywall", {
state: {
selectedCareer: {
career_path_id: newCareerPath.career_path_id,
career_path_id: newlyCreatedId,
career_name: data.title,
},
},
@ -178,6 +199,7 @@ function PopoutPanel({
}
}
// Filter & sort schools
const filteredAndSortedSchools = [...schools]
.filter((school) => {

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
function PremiumRoute({ user, children }) {
if (!user) {
// Not even logged in; go to sign in
return <Navigate to="/signin" replace />;
}
if (!user.is_premium) {
// Logged in but not premium; go to paywall
return <Navigate to="/paywall" replace />;
}
// User is logged in and has premium
return children;
}
export default PremiumRoute;

View File

@ -1,7 +1,7 @@
import React, { useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
function SignIn({ setIsAuthenticated }) {
function SignIn({ setIsAuthenticated, setUser }) {
const navigate = useNavigate();
const usernameRef = useRef('');
const passwordRef = useRef('');
@ -32,12 +32,22 @@ function SignIn({ setIsAuthenticated }) {
}
const data = await response.json();
const { token, userId } = data;
// Destructure user, which includes is_premium, etc.
const { token, userId, user } = data;
// Store token & userId in localStorage
localStorage.setItem('token', token);
localStorage.setItem('userId', userId);
// Mark user as authenticated
setIsAuthenticated(true);
// Store the full user object in state, so we can check user.is_premium, etc.
if (setUser && user) {
setUser(user);
}
// Navigate to next screen
navigate('/getting-started');
} catch (error) {
setError(error.message);
@ -70,7 +80,6 @@ function SignIn({ setIsAuthenticated }) {
/>
<button
type="submit"
// Make the button auto-sized, centered, and ensure text is centered
className="mx-auto rounded bg-blue-600 px-6 py-2 text-center text-white transition-colors hover:bg-blue-700 focus:outline-none"
>
Sign In

Binary file not shown.