diff --git a/src/components/StandardNav.jsx b/src/components/StandardNav.jsx index 0df2b72c..28d3007c 100644 --- a/src/components/StandardNav.jsx +++ b/src/components/StandardNav.jsx @@ -57,8 +57,14 @@ const DEFAULT_NOTIFICATION = { receivedTime: '', }; -export function StandardNav({ guestAllowed, alignCenter = true }) { +export function StandardNav({ guestAllowed, alignCenter = true, animatedPoints, isPointsAnimating: propIsPointsAnimating }) { const { role, points } = useContext(Context); + + // Use animated points if provided, otherwise use regular points + const displayPoints = animatedPoints !== undefined ? animatedPoints : points; + + // Use prop animation state if provided, otherwise track locally + const isPointsAnimating = propIsPointsAnimating !== undefined ? propIsPointsAnimating : false; const [terminaIsOpen, setTerminalIsOpen] = useState(false); const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); @@ -469,11 +475,15 @@ export function StandardNav({ guestAllowed, alignCenter = true }) { )}
-

- {points} +

+ {displayPoints}

diff --git a/src/components/auth/AuthFooter.jsx b/src/components/auth/AuthFooter.jsx index 8bf0dc8c..ca5cdf62 100644 --- a/src/components/auth/AuthFooter.jsx +++ b/src/components/auth/AuthFooter.jsx @@ -4,7 +4,7 @@ export default function AuthFooter() { return (
- © CTFGuide Corporation 2024
+ © CTFGuide Corporation 2025
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 +
+
+ )} - -

- Password: {password} - -

-

- Remaining Time: {formatTime(minutesRemaining)} - window.open(terminalUrl, '_blank')} className="cursor-pointer hover:text-yellow-500 ml-2 fas fa-expand text-white"> - {showMessage && ( - - Sometimes browsers block iframes, try opening the terminal in full screen if it the terminal is empty. - - - )} -

-
- )} - {fetchingTerminal ? ( -
-
-

- {loadingMessage} -

If you see a black screen, please wait a few seconds and refresh the page.

- -
-
- -
-
+ +

+ Password: {password} + +

+

+ Remaining Time: {formatTime(minutesRemaining)} + window.open(terminalUrl, '_blank')} className="cursor-pointer hover:text-yellow-500 ml-2 fas fa-expand text-white"> + {showMessage && ( + + Sometimes browsers block iframes, try opening the terminal in full screen if it the terminal is empty. + + + )} +

+
+ )} + {fetchingTerminal ? ( +
+
+

+ {loadingMessage} +

If you see a black screen, please wait a few seconds and refresh the page.

+
+
+ ) : ( + isTerminalBooted ? ( +