Added Premium Route Guard
This commit is contained in:
parent
9618bc0ea9
commit
d207dce5d4
@ -219,10 +219,20 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
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.zipcode
|
// JOIN user_profile to fetch is_premium, email, or whatever columns you need
|
||||||
FROM user_auth
|
const query = `
|
||||||
LEFT JOIN user_profile ON user_auth.user_id = user_profile.user_id
|
SELECT
|
||||||
WHERE user_auth.username = ?`;
|
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 = ?
|
||||||
|
`;
|
||||||
|
|
||||||
db.get(query, [username], async (err, row) => {
|
db.get(query, [username], async (err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -230,33 +240,43 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
return res.status(500).json({ error: 'Failed to query user authentication data' });
|
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) {
|
if (!row) {
|
||||||
return res.status(401).json({ error: 'Invalid username or password' });
|
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);
|
const isMatch = await bcrypt.compare(password, row.hashed_password);
|
||||||
if (!isMatch) {
|
if (!isMatch) {
|
||||||
return res.status(401).json({ error: 'Invalid username or password' });
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that you're using the correct user_id
|
// user_id from the row
|
||||||
const { user_id, zipcode } = row;
|
const { user_id } = row;
|
||||||
|
|
||||||
console.log('UserID:', user_id);
|
// Generate JWT
|
||||||
console.log('ZIP Code:', zipcode); // Log the ZIP code to ensure it's correct
|
|
||||||
|
|
||||||
// Send correct token with user_id
|
|
||||||
const token = jwt.sign({ userId: user_id }, SECRET_KEY, { expiresIn: '2h' });
|
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
|
// Return user object including is_premium and other columns
|
||||||
res.status(200).json({ message: 'Login successful', token, userId: user_id, zipcode });
|
// 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
|
// Route to fetch user profile
|
||||||
app.get('/api/user-profile', (req, res) => {
|
app.get('/api/user-profile', (req, res) => {
|
||||||
const token = req.headers.authorization?.split(' ')[1];
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
|
66
src/App.js
66
src/App.js
@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
|
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import PremiumRoute from './components/PremiumRoute.js';
|
||||||
import SessionExpiredHandler from './components/SessionExpiredHandler.js';
|
import SessionExpiredHandler from './components/SessionExpiredHandler.js';
|
||||||
import GettingStarted from './components/GettingStarted.js';
|
import GettingStarted from './components/GettingStarted.js';
|
||||||
import SignIn from './components/SignIn.js';
|
import SignIn from './components/SignIn.js';
|
||||||
@ -8,8 +9,8 @@ import InterestInventory from './components/InterestInventory.js';
|
|||||||
import Dashboard from './components/Dashboard.js';
|
import Dashboard from './components/Dashboard.js';
|
||||||
import UserProfile from './components/UserProfile.js';
|
import UserProfile from './components/UserProfile.js';
|
||||||
import FinancialProfileForm from './components/FinancialProfileForm.js';
|
import FinancialProfileForm from './components/FinancialProfileForm.js';
|
||||||
import MilestoneTracker from "./components/MilestoneTracker.js";
|
import MilestoneTracker from './components/MilestoneTracker.js';
|
||||||
import Paywall from "./components/Paywall.js";
|
import Paywall from './components/Paywall.js';
|
||||||
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
|
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
|
||||||
import MultiScenarioView from './components/MultiScenarioView.js';
|
import MultiScenarioView from './components/MultiScenarioView.js';
|
||||||
|
|
||||||
@ -17,18 +18,22 @@ function App() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Track whether user is authenticated
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
||||||
return !!localStorage.getItem('token');
|
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 = [
|
const premiumPaths = [
|
||||||
'/milestone-tracker',
|
'/milestone-tracker',
|
||||||
'/paywall',
|
'/paywall',
|
||||||
'/financial-profile',
|
'/financial-profile',
|
||||||
'/multi-scenario',
|
'/multi-scenario',
|
||||||
|
'/premium-onboarding'
|
||||||
];
|
];
|
||||||
|
|
||||||
const showPremiumCTA = !premiumPaths.includes(location.pathname);
|
const showPremiumCTA = !premiumPaths.includes(location.pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -51,28 +56,69 @@ function App() {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 p-6">
|
<main className="flex-1 p-6">
|
||||||
<Routes>
|
<Routes>
|
||||||
|
{/* Default to /signin if no path */}
|
||||||
<Route path="/" element={<Navigate to="/signin" />} />
|
<Route path="/" element={<Navigate to="/signin" />} />
|
||||||
|
|
||||||
|
{/* Public routes */}
|
||||||
<Route
|
<Route
|
||||||
path="/signin"
|
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 />} />
|
<Route path="/signup" element={<SignUp />} />
|
||||||
|
|
||||||
|
{/* Paywall - open to everyone for subscription */}
|
||||||
<Route path="/paywall" element={<Paywall />} />
|
<Route path="/paywall" element={<Paywall />} />
|
||||||
|
|
||||||
|
{/* Authenticated routes */}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<>
|
<>
|
||||||
<Route path="/getting-started" element={<GettingStarted />} />
|
<Route path="/getting-started" element={<GettingStarted />} />
|
||||||
<Route path="/interest-inventory" element={<InterestInventory />} />
|
<Route path="/interest-inventory" element={<InterestInventory />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/profile" element={<UserProfile />} />
|
<Route path="/profile" element={<UserProfile />} />
|
||||||
<Route path="/milestone-tracker" element={<MilestoneTracker />} />
|
|
||||||
<Route path="/financial-profile" element={<FinancialProfileForm />} />
|
{/* Premium-only routes use <PremiumRoute> */}
|
||||||
<Route path="/premium-onboarding" element={<OnboardingContainer />} />
|
<Route
|
||||||
<Route path="/multi-scenario" element={<MultiScenarioView />} />
|
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" />} />
|
<Route path="*" element={<Navigate to="/signin" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
@ -84,7 +84,7 @@ const CareerSearch = ({ onCareerSelected }) => {
|
|||||||
</datalist>
|
</datalist>
|
||||||
|
|
||||||
<button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
|
<button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
|
||||||
Confirm New Career
|
Confirm
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
// Paywall.js
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const Paywall = () => {
|
const Paywall = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { state } = useLocation();
|
||||||
|
// Extract the selectedCareer from location state
|
||||||
|
const { selectedCareer } = state || {};
|
||||||
|
|
||||||
const handleSubscribe = () => {
|
const handleSubscribe = () => {
|
||||||
navigate('/milestone-tracker');
|
// Once the user subscribes, navigate to MilestoneTracker
|
||||||
|
navigate('/PremiumOnboarding', {
|
||||||
|
state: {
|
||||||
|
selectedCareer,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -111,15 +111,16 @@ function PopoutPanel({
|
|||||||
closePanel();
|
closePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original PlanMyPath logic
|
|
||||||
async function handlePlanMyPath() {
|
async function handlePlanMyPath() {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
alert("You need to be logged in to create a career path.");
|
alert("You need to be logged in to create a career path.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1) Fetch existing career profiles (a.k.a. "careerPaths")
|
||||||
const allPathsResponse = await fetch(
|
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",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@ -128,47 +129,67 @@ function PopoutPanel({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!allPathsResponse.ok) throw new Error(`HTTP error ${allPathsResponse.status}`);
|
|
||||||
|
if (!allPathsResponse.ok) {
|
||||||
const { careerPath } = await allPathsResponse.json();
|
throw new Error(`HTTP error ${allPathsResponse.status}`);
|
||||||
const match = careerPath.find((path) => path.career_name === data.title);
|
}
|
||||||
|
|
||||||
|
// 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 (match) {
|
||||||
|
// If a path already exists for this career, confirm with the user
|
||||||
const decision = window.confirm(
|
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.`
|
`Click OK to RELOAD the existing path.\nClick Cancel to CREATE a new one.`
|
||||||
);
|
);
|
||||||
if (decision) {
|
if (decision) {
|
||||||
|
// Reload existing path → go to Paywall
|
||||||
navigate("/paywall", {
|
navigate("/paywall", {
|
||||||
state: {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCareerPath = {
|
// 3) Otherwise, create a new career profile using POST /premium/career-profile
|
||||||
career_path_id: uuidv4(),
|
|
||||||
career_name: data.title,
|
|
||||||
};
|
|
||||||
const newResponse = await fetch(
|
const newResponse = await fetch(
|
||||||
`${process.env.REACT_APP_API_URL}/premium/planned-path`,
|
`${process.env.REACT_APP_API_URL}/premium/career-profile`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
"Content-Type": "application/json",
|
"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.");
|
|
||||||
|
if (!newResponse.ok) {
|
||||||
navigate("/milestone-tracker", {
|
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: {
|
state: {
|
||||||
selectedCareer: {
|
selectedCareer: {
|
||||||
career_path_id: newCareerPath.career_path_id,
|
career_path_id: newlyCreatedId,
|
||||||
career_name: data.title,
|
career_name: data.title,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -177,6 +198,7 @@ function PopoutPanel({
|
|||||||
console.error("Error in Plan My Path:", error);
|
console.error("Error in Plan My Path:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Filter & sort schools
|
// Filter & sort schools
|
||||||
const filteredAndSortedSchools = [...schools]
|
const filteredAndSortedSchools = [...schools]
|
||||||
|
19
src/components/PremiumRoute.js
Normal file
19
src/components/PremiumRoute.js
Normal 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;
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
function SignIn({ setIsAuthenticated }) {
|
function SignIn({ setIsAuthenticated, setUser }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const usernameRef = useRef('');
|
const usernameRef = useRef('');
|
||||||
const passwordRef = useRef('');
|
const passwordRef = useRef('');
|
||||||
@ -32,12 +32,22 @@ function SignIn({ setIsAuthenticated }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
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('token', token);
|
||||||
localStorage.setItem('userId', userId);
|
localStorage.setItem('userId', userId);
|
||||||
|
|
||||||
|
// Mark user as authenticated
|
||||||
setIsAuthenticated(true);
|
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');
|
navigate('/getting-started');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error.message);
|
setError(error.message);
|
||||||
@ -48,13 +58,13 @@ function SignIn({ setIsAuthenticated }) {
|
|||||||
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-100 p-4">
|
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-100 p-4">
|
||||||
<div className="w-full max-w-sm rounded-md bg-white p-6 shadow-md">
|
<div className="w-full max-w-sm rounded-md bg-white p-6 shadow-md">
|
||||||
<h1 className="mb-6 text-center text-2xl font-semibold">Sign In</h1>
|
<h1 className="mb-6 text-center text-2xl font-semibold">Sign In</h1>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p className="mb-4 rounded bg-red-50 p-2 text-sm text-red-600">
|
<p className="mb-4 rounded bg-red-50 p-2 text-sm text-red-600">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSignIn} className="flex flex-col space-y-4">
|
<form onSubmit={handleSignIn} className="flex flex-col space-y-4">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -70,13 +80,12 @@ function SignIn({ setIsAuthenticated }) {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="mt-4 text-center text-sm text-gray-600">
|
<p className="mt-4 text-center text-sm text-gray-600">
|
||||||
Don’t have an account?{' '}
|
Don’t have an account?{' '}
|
||||||
<Link
|
<Link
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user