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' });
|
||||
}
|
||||
|
||||
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,33 +240,43 @@ 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
|
||||
app.get('/api/user-profile', (req, res) => {
|
||||
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 { 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>
|
||||
|
@ -84,7 +84,7 @@ const CareerSearch = ({ onCareerSelected }) => {
|
||||
</datalist>
|
||||
|
||||
<button onClick={handleConfirmCareer} style={{ marginLeft: '8px' }}>
|
||||
Confirm New Career
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -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 (
|
||||
|
@ -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) => {
|
||||
|
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 { 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
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user