Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ service cloud.firestore {
// Locks referralPoints to the actual totalEarned value in the referrals index.
allow update: if isAuthenticated()
&& request.auth.uid != uid
// 🛡️ NEW GUARD (Issue #432): Only users actively onboarding can trigger a referral point grant
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.onboardingStatus == "incomplete"
// Prevent modifying any root-level fields other than points
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['points'])
// FIX: Sync referralPoints directly with the referrals index document's totalEarned counter
Expand All @@ -120,7 +122,6 @@ service cloud.firestore {
&& request.resource.data.points.auditorPoints == resource.data.points.auditorPoints
&& exists(/databases/$(database)/documents/referrals/$(uid))
&& request.auth.uid in get(/databases/$(database)/documents/referrals/$(uid)).data.usedBy;
}

match /referrals/{uid} {
allow read: if isAuthenticated();
Expand All @@ -131,6 +132,8 @@ service cloud.firestore {
// Allow either the owner to write, OR a referred user to atomically append their UID
allow update: if isOwner(uid) || (
isAuthenticated()
// 🛡️ NEW GUARD (Issue #432): Prevent existing users from arbitrarily claiming codes
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.onboardingStatus == "incomplete"
&& !(request.auth.uid in resource.data.usedBy)
&& request.resource.data.usedBy.size() == resource.data.usedBy.size() + 1
&& request.resource.data.usedBy[resource.data.usedBy.size()] == request.auth.uid
Expand Down
48 changes: 39 additions & 9 deletions src/components/ui/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { Component } from "react";
import { motion } from "framer-motion";

export class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
this.state = { hasError: false, error: null, errorInfo: null };
}

static getDerivedStateFromError(error) {
Expand All @@ -12,13 +13,30 @@

componentDidCatch(error, errorInfo) {
console.error("ErrorBoundary caught an error:", error, errorInfo);
this.setState({ errorInfo });
}

// ADDED: Graceful recovery method (No full page reload)
handleRetry = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
};

render() {
if (this.state.hasError) {
// ADDED: Dynamic context-specific message
const customMessage = this.props.fallbackMessage || "The application encountered an unexpected error. Don't worry, your data is safe.";

return (
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-950 p-6 text-slate-800 dark:text-slate-100 transition-colors duration-300">
<div className="max-w-md w-full backdrop-blur-xl bg-white/70 dark:bg-slate-900/70 border border-slate-200/50 dark:border-slate-800/50 shadow-2xl rounded-2xl p-8 text-center flex flex-col items-center">
// Changed min-h-screen to min-h-[60vh] so it fits perfectly inside dashboard layouts too
<div className="w-full h-full min-h-[60vh] flex flex-col items-center justify-center p-6 text-slate-800 dark:text-slate-100 transition-colors duration-300">

{/* ADDED: Framer motion wrapper for entry animation */}
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="max-w-md w-full backdrop-blur-xl bg-white/70 dark:bg-slate-900/70 border border-slate-200/50 dark:border-slate-800/50 shadow-2xl rounded-2xl p-8 text-center flex flex-col items-center"
>
{/* Premium CSS-Animated SVG warning graphic */}
<div className="relative w-40 h-40 mb-6 flex items-center justify-center">
{/* Glowing backdrop rings */}
Expand All @@ -33,19 +51,31 @@
</svg>
</div>
</div>

<h1 className="text-2xl font-bold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-violet-500 to-blue-500">
Something went wrong.
</h1>

<p className="text-sm text-slate-500 dark:text-slate-400 mb-6 font-medium">
The application encountered an unexpected error. Don't worry, your data is safe.
{customMessage}
</p>

{/* ADDED: Hidden technical details for developers (only shows in localhost/dev mode) */}
{process.env.NODE_ENV === 'development' && this.state.error && (

Check failure on line 64 in src/components/ui/ErrorBoundary.jsx

View workflow job for this annotation

GitHub Actions / Lint Check

'process' is not defined
<div className="w-full mb-6 p-3 bg-slate-50 dark:bg-slate-950 rounded-lg text-left overflow-hidden">
<p className="text-[10px] font-mono text-red-500 break-words">
{this.state.error.toString()}
</p>
</div>
)}

<button
onClick={() => window.location.reload()}
className="px-6 py-2.5 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-700 hover:to-indigo-700 text-white font-semibold shadow-lg hover:shadow-indigo-500/20 active:scale-95 transition-all duration-200 cursor-pointer"
onClick={this.handleRetry}
className="px-6 py-2.5 w-full rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-700 hover:to-indigo-700 text-white font-semibold shadow-lg hover:shadow-indigo-500/20 active:scale-95 transition-all duration-200 cursor-pointer"
>
Refresh Page
Try Again
</button>
</div>
</motion.div>
</div>
);
}
Expand All @@ -54,4 +84,4 @@
}
}

export default ErrorBoundary;
export default ErrorBoundary;
152 changes: 80 additions & 72 deletions src/routes/AppRoutes.jsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
import React from "react";
import React, { Suspense } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
import PublicLayout from "../layouts/PublicLayout";
import DashboardLayout from "../layouts/DashboardLayout";
import Home from "../pages/Home";
import Dashboard from "../pages/Dashboard";
import GitRank from "../pages/GitRank";
import RankHer from "../pages/RankHer";
import CodingVerse from "../pages/CodingVerse";
import CodingOwl from "../pages/CodingOwl";
import Matchmaker from "../pages/Matchmaker";
import Profile from "../pages/Profile";
import Friends from "../pages/Friends";
import Login from "../pages/Login";
import Onboarding from "../pages/Onboarding";
import NotFound from "../pages/NotFound";
import Achievements from "../pages/Achievements";
import About from "../pages/About";
import Terms from "../pages/Terms";
import Privacy from "../pages/Privacy";
import ComingSoonCard from "../components/ui/ComingSoonCard";
import GlobalModals from "../components/ui/GlobalModals";
import { Settings as SettingsIcon } from "lucide-react";
import Auditor from "../pages/Auditor";
import ErrorBoundary from "../components/ui/ErrorBoundary";

// Lazy Loaded Pages to reduce initial JS bundle
const Home = React.lazy(() => import("../pages/Home"));
const Dashboard = React.lazy(() => import("../pages/Dashboard"));
const GitRank = React.lazy(() => import("../pages/GitRank"));
const RankHer = React.lazy(() => import("../pages/RankHer"));
const CodingVerse = React.lazy(() => import("../pages/CodingVerse"));
const CodingOwl = React.lazy(() => import("../pages/CodingOwl"));
const Matchmaker = React.lazy(() => import("../pages/Matchmaker"));
const Profile = React.lazy(() => import("../pages/Profile"));
const Friends = React.lazy(() => import("../pages/Friends"));
const Login = React.lazy(() => import("../pages/Login"));
const Onboarding = React.lazy(() => import("../pages/Onboarding"));
const NotFound = React.lazy(() => import("../pages/NotFound"));
const Achievements = React.lazy(() => import("../pages/Achievements"));
const About = React.lazy(() => import("../pages/About"));
const Terms = React.lazy(() => import("../pages/Terms"));
const Privacy = React.lazy(() => import("../pages/Privacy"));
const Auditor = React.lazy(() => import("../pages/Auditor"));
const CardBuilder = React.lazy(() => import("../pages/CardBuilder"));

// Inline loading indicator
const LoadingScreen = ({ message }) => (
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-[#090D1A]">
<div className="flex flex-col items-center space-y-4">
<div className="w-10 h-10 border-4 border-violet-500 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-slate-400 font-bold tracking-widest uppercase">{message || "Syncing Session..."}</span>
<span className="text-sm text-slate-400 font-bold tracking-widest uppercase">{message || "Loading..."}</span>
</div>
</div>
);
Expand Down Expand Up @@ -65,8 +69,6 @@ const OnboardingRoute = ({ children }) => {
return <Navigate to="/login" replace />;
}

// Strict guard: if the user's data explicitly says they are complete, OR if isOnboarding is false, redirect.
// We only allow access if they are explicitly incomplete.
if (userData?.onboardingStatus === "complete" || !isOnboarding || (userData && userData.onboardingStatus !== "incomplete")) {
return <Navigate to="/dashboard" replace />;
}
Expand All @@ -79,7 +81,7 @@ const GuestRoute = ({ children }) => {
const { user, loading, isOnboarding } = useAuth();

if (loading) {
return null; // Don't redirect prematurely while state is resolving
return null;
}

if (user) {
Expand All @@ -92,9 +94,6 @@ const GuestRoute = ({ children }) => {
return children;
};

import CardBuilder from "../pages/CardBuilder";

// An inline settings page to keep route integrated
const SettingsPage = () => (
<div className="space-y-6">
<ComingSoonCard
Expand All @@ -113,58 +112,67 @@ const SettingsPage = () => (
</div>
);

// Helper wrapper to easily wrap lazy components with ErrorBoundary
const withErrorBoundary = (Component, componentName, fallbackMessage) => (
<ErrorBoundary componentName={componentName} fallbackMessage={fallbackMessage}>
<Component />
</ErrorBoundary>
);

export const AppRoutes = () => {
return (
<>
<Routes>
{/* Public Site Layout & Pages */}
<Route element={<PublicLayout />}>
<Route path="/" element={<Home />} />
<Route path="/gitrank" element={<GitRank />} />
<Route path="/rankher" element={<RankHer />} />
<Route path="/codingverse" element={<CodingVerse />} />
<Route path="/codingowl" element={<CodingOwl />} />
</Route>

{/* Standalone About Us page */}
<Route path="/about" element={<About />} />

{/* Standalone Legal pages */}
<Route path="/terms" element={<Terms />} />
<Route path="/privacy" element={<Privacy />} />

{/* Public Login page (standalone) - guarded from logged in users */}
<Route path="/login" element={<GuestRoute><Login /></GuestRoute>} />

{/* Onboarding page (standalone) - guarded so only incomplete profiles see it */}
<Route path="/onboarding" element={<OnboardingRoute><Onboarding /></OnboardingRoute>} />

{/* Layout dashboard sub-pages - locked to authenticated & fully onboarded users */}
<Route element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/dashboard/gitrank" element={<GitRank />} />
<Route path="/dashboard/rankher" element={<RankHer />} />
<Route path="/dashboard/achievements" element={<Achievements />} />
<Route path="/dashboard/codingverse" element={<CodingVerse />} />
<Route path="/dashboard/codingowl" element={<CodingOwl />} />
<Route path="/dashboard/matchmaker" element={<Matchmaker />} />
<Route path="/dashboard/friends" element={<Friends />} />
<Route path="/dashboard/friends/leaderboard" element={<Friends />} />
<Route path="/dashboard/friends/followers" element={<Friends />} />
<Route path="/dashboard/friends/following" element={<Friends />} />
<Route path="/dashboard/profile" element={<Profile />} />
<Route path="/dashboard/profile/card-builder" element={<CardBuilder />} />
<Route path="/dashboard/profile/:username" element={<Profile />} />
<Route path="/dashboard/settings" element={<SettingsPage />} />
<Route path="/dashboard/auditor" element={<Auditor />} />
</Route>

{/* 404 Catch All */}
<Route path="*" element={<NotFound />} />
</Routes>
<Suspense fallback={<LoadingScreen message="Loading Page..." />}>
<Routes>
{/* Public Site Layout & Pages */}
<Route element={<PublicLayout />}>
<Route path="/" element={withErrorBoundary(Home, "Home", "Failed to load the homepage. Please try refreshing.")} />
<Route path="/gitrank" element={withErrorBoundary(GitRank, "GitRank", "GitRank couldn't load right now. Try refreshing, or sync your GitHub stats.")} />
<Route path="/rankher" element={withErrorBoundary(RankHer, "RankHer", "Failed to load RankHer leaderboard. Please check your connection.")} />
<Route path="/codingverse" element={withErrorBoundary(CodingVerse, "CodingVerse", "CodingVerse arenas failed to load. Please try again.")} />
<Route path="/codingowl" element={withErrorBoundary(CodingOwl, "CodingOwl", "Failed to load your habit tracker. Please try again.")} />
</Route>

{/* Standalone About Us page */}
<Route path="/about" element={withErrorBoundary(About, "About", "Failed to load the About page.")} />

{/* Standalone Legal pages */}
<Route path="/terms" element={withErrorBoundary(Terms, "Terms", "Failed to load Terms of Service.")} />
<Route path="/privacy" element={withErrorBoundary(Privacy, "Privacy", "Failed to load Privacy Policy.")} />

{/* Public Login page */}
<Route path="/login" element={<GuestRoute>{withErrorBoundary(Login, "Login", "Failed to initialize the login portal.")}</GuestRoute>} />

{/* Onboarding page */}
<Route path="/onboarding" element={<OnboardingRoute>{withErrorBoundary(Onboarding, "Onboarding", "Failed to load the onboarding flow.")}</OnboardingRoute>} />

{/* Layout dashboard sub-pages */}
<Route element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
<Route path="/dashboard" element={withErrorBoundary(Dashboard, "Dashboard", "Your dashboard failed to load. Try refreshing the page.")} />
<Route path="/dashboard/gitrank" element={withErrorBoundary(GitRank, "GitRank", "GitRank couldn't load right now. Try refreshing, or sync your GitHub stats.")} />
<Route path="/dashboard/rankher" element={withErrorBoundary(RankHer, "RankHer", "Failed to load RankHer data.")} />
<Route path="/dashboard/achievements" element={withErrorBoundary(Achievements, "Achievements", "Failed to load your badges and trophies.")} />
<Route path="/dashboard/codingverse" element={withErrorBoundary(CodingVerse, "CodingVerse", "CodingVerse challenges failed to load.")} />
<Route path="/dashboard/codingowl" element={withErrorBoundary(CodingOwl, "CodingOwl", "Failed to load CodingOwl streaks.")} />
<Route path="/dashboard/matchmaker" element={withErrorBoundary(Matchmaker, "Matchmaker", "Failed to find developer matches.")} />
<Route path="/dashboard/friends" element={withErrorBoundary(Friends, "Friends", "Failed to load your social network.")} />
<Route path="/dashboard/friends/leaderboard" element={withErrorBoundary(Friends, "Friends Leaderboard", "Failed to load friends leaderboard.")} />
<Route path="/dashboard/friends/followers" element={withErrorBoundary(Friends, "Followers", "Failed to load followers list.")} />
<Route path="/dashboard/friends/following" element={withErrorBoundary(Friends, "Following", "Failed to load following list.")} />
<Route path="/dashboard/profile" element={withErrorBoundary(Profile, "Profile", "Your profile data failed to load.")} />
<Route path="/dashboard/profile/card-builder" element={withErrorBoundary(CardBuilder, "Card Builder", "Failed to initialize the dev card builder.")} />
<Route path="/dashboard/profile/:username" element={withErrorBoundary(Profile, "User Profile", "This developer's profile failed to load.")} />
<Route path="/dashboard/settings" element={<SettingsPage />} />
<Route path="/dashboard/auditor" element={withErrorBoundary(Auditor, "Auditor", "Security audit logs failed to load.")} />
</Route>

{/* 404 Catch All */}
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
<GlobalModals />
</>
);
};

export default AppRoutes;
export default AppRoutes;
5 changes: 5 additions & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export default defineConfig({
if (id.includes('framer-motion')) {
return 'vendor-motion';
}

// Swiper (Carousel)
if (id.includes('swiper')) {
return 'vendor-swiper';
}

// All other node_modules
return 'vendor';
Expand Down
Loading