Terms of Service
Privacy Policy
diff --git a/src/middleware.js b/src/middleware.js
index a3d69092..39cdd83c 100644
--- a/src/middleware.js
+++ b/src/middleware.js
@@ -33,6 +33,12 @@ export function middleware(req) {
return NextResponse.next();
}
+ // Allow access to specific challenge pages (UUID format)
+ const challengePathRegex = /^\/challenges\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/;
+ if (challengePathRegex.test(pathname)) {
+ return NextResponse.next();
+ }
+
const idToken = req.cookies.get('idToken');
// Redirect to login if token is missing or invalid
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index cc9bae2b..cfe16a45 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -28,6 +28,24 @@ export default function App({ Component, pageProps }) {
};
const fetchUser = async () => {
+ // Check if user has a token before making the request
+ const getCookie = () => {
+ try {
+ const value = `; ${document.cookie}`;
+ const parts = value.split(`; idToken=`);
+ if (parts.length === 2) return parts.pop().split(';').shift();
+ } catch (error) {
+ return "";
+ }
+ return "";
+ };
+
+ const token = getCookie();
+ if (!token) {
+ // No token, user is not authenticated - don't make the API call
+ return;
+ }
+
const url = process.env.NEXT_PUBLIC_API_URL + "/account";
const user = await request(url, "GET", null);
diff --git a/src/pages/challenges/[...id].jsx b/src/pages/challenges/[...id].jsx
index 6cd52aa1..621573a9 100644
--- a/src/pages/challenges/[...id].jsx
+++ b/src/pages/challenges/[...id].jsx
@@ -1,5 +1,6 @@
import { MarkdownViewer } from "@/components/MarkdownViewer";
import { StandardNav } from "@/components/StandardNav";
+import { Logo } from "@/components/Logo";
import request from "@/utils/request";
import { Dialog } from "@headlessui/react";
import { DocumentTextIcon } from "@heroicons/react/20/solid";
@@ -24,6 +25,164 @@ import WriteupModal from '@/components/WriteupModal';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import { useSearchParams } from 'next/navigation';
+// Locked content component for restricted tabs
+function LockedContent({ tabName }) {
+ const features = {
+ 'Hints': {
+ icon: 'fas fa-question',
+ title: 'Progressive Hints System',
+ description: 'Get step-by-step guidance when you\'re stuck',
+ benefits: ['Smart hint progression', 'Learn at your own pace', 'Detailed explanations']
+ },
+ 'Comments': {
+ icon: 'fas fa-comments',
+ title: 'Community Discussions',
+ description: 'Connect with other learners and share insights',
+ benefits: ['Ask questions', 'Share discoveries', 'Learn from others']
+ },
+ 'Writeups': {
+ icon: 'fas fa-book',
+ title: 'Solution Writeups',
+ description: 'Detailed explanations of how to solve challenges',
+ benefits: ['Step-by-step solutions', 'Multiple approaches', 'Learn new techniques']
+ },
+ 'AI': {
+ icon: 'fas fa-robot',
+ title: 'AI Assistant',
+ description: 'Get personalized help from our AI tutor',
+ benefits: ['Instant guidance', 'Tailored explanations', 'Available 24/7']
+ }
+ };
+
+ const feature = features[tabName] || features['Hints'];
+
+ return (
+
+
+
+
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+ {feature.benefits.map((benefit, index) => (
+
+
+ {benefit}
+
+ ))}
+
+
+
+ Login to Access
+
+
+ Create Free Account
+
+
+
+
+ );
+}
+
+// Simple public navigation component for unauthenticated users
+function PublicNav() {
+ return (
+
+ );
+}
+
+// Utility function to check if user is authenticated
+function useAuthentication() {
+ const { username } = useContext(Context);
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [authLoading, setAuthLoading] = useState(true);
+
+ useEffect(() => {
+ const token = getCookie('idToken');
+ setIsAuthenticated(!!token && !!username);
+
+ // Set loading to false after checking authentication
+ // Small delay to prevent flash of unauthenticated content
+ setTimeout(() => {
+ setAuthLoading(false);
+ }, 100);
+ }, [username]);
+
+ return { isAuthenticated, authLoading };
+}
+
+// Counting animation hook for points
+function useCountingAnimation(targetValue, duration = 1500) {
+ const [count, setCount] = useState(targetValue || 0);
+ const [isAnimating, setIsAnimating] = useState(false);
+
+ const startAnimation = (newTarget) => {
+ if (isAnimating) return;
+
+ setIsAnimating(true);
+ const startValue = count;
+ const startTime = Date.now();
+
+ const animate = () => {
+ const now = Date.now();
+ const elapsed = now - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+
+ // Easing function for smooth animation
+ const easeOutQuart = 1 - Math.pow(1 - progress, 4);
+ const currentCount = Math.floor(startValue + (newTarget - startValue) * easeOutQuart);
+
+ setCount(currentCount);
+
+ if (progress < 1) {
+ requestAnimationFrame(animate);
+ } else {
+ setCount(newTarget);
+ setIsAnimating(false);
+ }
+ };
+
+ requestAnimationFrame(animate);
+ };
+
+ return { count, startAnimation, isAnimating };
+}
+
// Move styles to a separate useEffect
function useHighlightStyles() {
useEffect(() => {
@@ -140,6 +299,20 @@ export default function Challenge() {
const [selectedWriteup, setSelectedWriteup] = useState(null);
const [isTerminalFullscreen, setIsTerminalFullscreen] = useState(false);
const [isChallengeFullscreen, setIsChallengeFullscreen] = useState(true);
+ const { isAuthenticated, authLoading } = useAuthentication();
+
+ // Context for navbar points update
+ const { points, setPoints } = useContext(Context);
+
+ // Points animation for navbar
+ const navbarPointsAnimation = useCountingAnimation(points, 1500);
+
+ // Initialize animation with current points
+ useEffect(() => {
+ if (points > 0 && navbarPointsAnimation.count !== points) {
+ navbarPointsAnimation.startAnimation(points);
+ }
+ }, [points]);
// I hate this
const [urlChallengeId, urlSelectedTab, urlWriteupId] = (router ?? {})?.query?.id ?? [undefined, undefined, undefined];
@@ -158,16 +331,14 @@ export default function Challenge() {
// Tab system is designed to keep browser state in url,
// while mainting persistence of the terminal.
const tabs = {
- 'description': { text: 'Description', element: DescriptionPage, },
- 'hints': { text: 'Hints', element: HintsPage, },
-
- 'comments': { text: 'Comments', element: CommentsPage, },
-
- 'write-up': { text: 'Writeups', element: WriteUpPage, },
- 'leaderboard': { text: 'Leaderboard', element: LeaderboardPage, },
- 'AI': { text: 'AI', element: AIPage, },
-
+ 'description': { text: 'Description', element: DescriptionPage, requiresAuth: false },
+ 'hints': { text: 'Hints', element: HintsPage, requiresAuth: true },
+ 'comments': { text: 'Comments', element: CommentsPage, requiresAuth: true },
+ 'write-up': { text: 'Writeups', element: WriteUpPage, requiresAuth: true },
+ 'leaderboard': { text: 'Leaderboard', element: LeaderboardPage, requiresAuth: false },
+ 'AI': { text: 'AI', element: AIPage, requiresAuth: true },
}
+ // Allow selection of restricted tabs for unauthenticated users (will show locked content)
const selectedTab = tabs[urlSelectedTab] ?? tabs.description;
useEffect(() => {
@@ -180,21 +351,77 @@ export default function Challenge() {
}
try {
const getChallengeByIdEndPoint = `${process.env.NEXT_PUBLIC_API_URL}/challenges/${urlChallengeId}`;
- const getChallengeResult = await request(getChallengeByIdEndPoint, "GET", null);
- if (getChallengeResult.success) {
- setCache("challenge", getChallengeResult.body);
+
+ if (isAuthenticated) {
+ // For authenticated users, use the normal authenticated endpoint
+ const getChallengeResult = await request(getChallengeByIdEndPoint, "GET", null);
+ if (getChallengeResult.success) {
+ setCache("challenge", getChallengeResult.body);
+ }
+ } else {
+ // For unauthenticated users, use the public endpoint
+ const publicEndpoint = `${process.env.NEXT_PUBLIC_API_URL}/public/challenges/${urlChallengeId}`;
+
+ const response = await fetch(publicEndpoint, {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ if (response.ok) {
+ const result = await response.json();
+
+ if (result.success && result.challenge) {
+ setCache("challenge", result.challenge);
+ }
+ } else {
+
+ // Fallback to placeholder if the public endpoint fails
+ const placeholderChallenge = {
+ id: urlChallengeId,
+ title: "Challenge Preview",
+ content: `
+## Access Required
+
+This challenge requires you to **login to CTFGuide** to view the full content, access hints, submit flags, and track your progress.
+
+### What you'll get with a free account:
+- 🎯 Full challenge descriptions and content
+- 💡 Progressive hints system
+- 🖥️ Interactive Linux terminals
+- 📊 Progress tracking and leaderboards
+- 💬 Community discussions and writeups
+- 🏆 Points and achievement system
+
+**Ready to start your cybersecurity journey?**
+ `,
+ difficulty: "PREVIEW",
+ category: ["Preview"],
+ creator: 'CTFGuide',
+ views: 0,
+ upvotes: 0,
+ downvotes: 0,
+ };
+ setCache("challenge", placeholderChallenge);
+ }
}
- } catch (error) { throw "Failed to fetch challenge: " + error; }
+ } catch (error) {
+ console.error("Failed to fetch challenge: " + error);
+ }
})();
- }, [urlChallengeId]);
+ }, [urlChallengeId, isAuthenticated]);
const [loadingFlagSubmit, setLoadingFlagSubmit] = useState(false);
const [isPointsModalOpen, setIsPointsModalOpen] = useState(false);
const [awardedPoints, setAwardedPoints] = useState(0);
- const showPointsModal = (points) => {
- setAwardedPoints(points);
+ const showPointsModal = (newPoints) => {
+ setAwardedPoints(newPoints);
setIsPointsModalOpen(true);
+
+ // Update navbar points with animation
+ const newTotalPoints = points + newPoints;
+ setPoints(newTotalPoints);
+ navbarPointsAnimation.startAnimation(newTotalPoints);
};
const onSubmitFlag = (e) => {
@@ -470,6 +697,37 @@ export default function Challenge() {
}
};
+
+ // Show loading state while determining authentication
+ if (authLoading) {
+ return (
+ <>
+
+
Challenge - CTFGuide
+
+
+
+
+
+
+
+
+
+
+
Loading challenge...
+
+
+
+ >
+ );
+ }
+
return (
<>
@@ -487,23 +745,34 @@ export default function Challenge() {
-
+ {isAuthenticated ? (
+
+ ) : (
+
+ )}
{isChallengeFullscreen && (
-
+
{Object.entries(tabs).map(([url, tab]) => (
))}
{selectedWriteup ? (
setSelectedWriteup(null)} writeup={selectedWriteup} />
+ ) : selectedTab.requiresAuth && !isAuthenticated ? (
+
) : (
)}
-
-
-
+ {isAuthenticated ? (
+
+
+
+ ) : (
+
+
+
+
+ Login to CTFGuide
+ to submit flags and track your progress
+
+
+ )}