Skip to content

Commit

Permalink
frontend: add default user page for authenticated users
Browse files Browse the repository at this point in the history
  • Loading branch information
PoustouFlan committed Aug 1, 2024
1 parent 7e9236f commit d4e7c18
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 66 deletions.
35 changes: 20 additions & 15 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import UserInfo from './user/info';
import ScoreboardInfo from './scoreboard/info';
import ScoreboardPage from "./scoreboard/page.tsx";
Expand All @@ -8,27 +8,32 @@ import ChallengeList from "./challenge/list.tsx";
import ChallengeFlaggers from "./challenge/flaggers.tsx";
import LoginPage from "./auth/Login.tsx";
import LogoutPage from "./auth/Logout.tsx";
import ProtectedRoute from './auth/ProtectedRoute';

const App: React.FC = () => {
const jwt = localStorage.getItem('jwt');
const isAuthenticated = Boolean(jwt);

return (
<Router>
<Routes>
<Route path="/user/:username" element={<UserInfo/>}/>
<Route path="/scoreboard/:scoreboardName/user/:username" element={<UserInfo/>}/>
<Route path="/scoreboard" element={<ScoreboardPage/>}/>
<Route path="/scoreboard/:scoreboardName" element={<ScoreboardInfo/>}/>
<Route path="/category" element={<CategoryList/>}/>
<Route path="/scoreboard/:scoreboardName/category" element={<CategoryList/>}/>
<Route path="/category/:categoryName" element={<ChallengeList/>}/>
<Route path="/scoreboard/:scoreboardName/category/:categoryName" element={<ChallengeList/>}/>
<Route path="/category/:categoryName/:challengeName" element={<ChallengeFlaggers/>}/>
<Route path="/scoreboard/:scoreboardName/category/:categoryName/:challengeName"
element={<ChallengeFlaggers/>}/>
<Route path="/login" element={<LoginPage/>}/>
<Route path="/logout" element={<LogoutPage/>} />
<Route path="/user" element={<ProtectedRoute element={<UserInfo />} isAuthenticated={isAuthenticated} />} />
<Route path="/user/:username" element={<UserInfo />} />
<Route path="/scoreboard/:scoreboardName/user/" element={<ProtectedRoute element={<UserInfo />} isAuthenticated={isAuthenticated} />} />
<Route path="/scoreboard/:scoreboardName/user/:username" element={<UserInfo />} />
<Route path="/scoreboard" element={<ScoreboardPage />} />
<Route path="/scoreboard/:scoreboardName" element={<ScoreboardInfo />} />
<Route path="/category" element={<CategoryList />} />
<Route path="/scoreboard/:scoreboardName/category" element={<CategoryList />} />
<Route path="/category/:categoryName" element={<ChallengeList />} />
<Route path="/scoreboard/:scoreboardName/category/:categoryName" element={<ChallengeList />} />
<Route path="/category/:categoryName/:challengeName" element={<ChallengeFlaggers />} />
<Route path="/scoreboard/:scoreboardName/category/:categoryName/:challengeName" element={<ChallengeFlaggers />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />
</Routes>
</Router>
);
};

export default App;
export default App;
22 changes: 10 additions & 12 deletions frontend/src/auth/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useEffect, useState} from 'react';
import { useNavigate } from 'react-router-dom';
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { generateAuthToken, verifyAuthToken } from '../api';

const LoginPage: React.FC = () => {
Expand All @@ -9,17 +9,16 @@ const LoginPage: React.FC = () => {
const [error, setError] = useState('');
const [loggedInUser, setLoggedInUser] = useState<string | null>(null);
const navigate = useNavigate();
const location = useLocation();
const redirectTo = (location.state as any)?.from?.pathname || '/';

useEffect(() => {
// Check if a JWT is stored and decode it to get the username
const jwt = localStorage.getItem('jwt');
if (jwt) {
// Decode JWT to get the username
try {
const decoded = JSON.parse(atob(jwt.split('.')[1])); // Decode JWT payload
setLoggedInUser(decoded.sub); // 'sub' is the claim for the subject (username)
const decoded = JSON.parse(atob(jwt.split('.')[1]));
setLoggedInUser(decoded.sub);
} catch (e) {
// Handle invalid JWT or decoding error
console.error('Invalid JWT:', e);
}
}
Expand All @@ -44,17 +43,16 @@ const LoginPage: React.FC = () => {

try {
await verifyAuthToken(username, payload);
// Redirect to the main page upon successful authentication
navigate('/');
navigate(redirectTo); // Redirect to the intended page
} catch (err) {
setError('Invalid token or username. Please try again.');
}
};

const handleLogout = () => {
localStorage.removeItem('jwt'); // Remove JWT from local storage
setLoggedInUser(null); // Update local state
navigate('/login'); // Redirect to the login page
localStorage.removeItem('jwt');
setLoggedInUser(null);
navigate('/login');
};

if (loggedInUser) {
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/auth/Logout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ const LogoutPage: React.FC = () => {
const navigate = useNavigate();

useEffect(() => {
localStorage.removeItem('jwt'); // Remove JWT from local storage
navigate('/'); // Redirect to the main page
localStorage.removeItem('jwt');
navigate('/');
}, [navigate]);

return (
<div style={{ padding: '20px' }}>
<div>
<h1>Logging out...</h1>
<p>You are being logged out. Redirecting to the main page...</p>
</div>
);
};

export default LogoutPage;
export default LogoutPage;
20 changes: 20 additions & 0 deletions frontend/src/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ProtectedRoute.tsx
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';

interface ProtectedRouteProps {
element: React.ReactElement;
isAuthenticated: boolean;
}

const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ element, isAuthenticated }) => {
const location = useLocation();

if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}

return element;
};

export default ProtectedRoute;
85 changes: 52 additions & 33 deletions frontend/src/user/info.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,65 @@
// user/info.tsx
import React, {useEffect, useState} from 'react';
import React, { useEffect, useState } from 'react';
import './user.css';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faSyncAlt} from '@fortawesome/free-solid-svg-icons';
import {fetchChallengeDetails, fetchUserData, refreshUserData, SolvedChallenge, User} from '../api';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSyncAlt } from '@fortawesome/free-solid-svg-icons';
import { fetchChallengeDetails, fetchUserData, refreshUserData, SolvedChallenge, User } from '../api';
import ScoreGraph from './graph'; // Adjusted import path
import UserCompletionRadar from './radar';
import SolvedChallenges from './solved'; // Import the SolvedChallenges component
import UserHeader from './header'; // Import UserHeader
import {useParams} from 'react-router-dom';
import { useParams } from 'react-router-dom';

interface UserInfoProps extends Record<string, string | undefined> {
username: string;
}

const UserInfo: React.FC = () => {
const {username} = useParams<UserInfoProps>();
const { username } = useParams<UserInfoProps>();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

if (!username) {
setError(new Error('Unknown category or challenge'));
setLoading(false);
return;
}

// Retrieve the JWT from local storage
const getAuthenticatedUser = () => {
const jwt = localStorage.getItem('jwt');
if (!jwt) return null;
try {
const decoded = JSON.parse(atob(jwt.split('.')[1]));
return decoded.sub; // Assuming the 'sub' field contains the username
} catch (e) {
console.error('Invalid JWT:', e);
return null;
}
};

const fetchChallengeDetailsForUser = async (solvedChallenges: SolvedChallenge[]) => {
return await Promise.all(solvedChallenges.map(async (challenge) => {
try {
if (!challenge.name || !challenge.category)
return {...challenge, points: 0, loadingPoints: false};
return { ...challenge, points: 0, loadingPoints: false };
const challengeDetails = await fetchChallengeDetails(challenge.name, challenge.category);
return {...challenge, points: challengeDetails.points, loadingPoints: false};
return { ...challenge, points: challengeDetails.points, loadingPoints: false };
} catch (err) {
console.error(`Failed to fetch details for challenge ${challenge.name}:`, err);
return {...challenge, points: 0, loadingPoints: false};
return { ...challenge, points: 0, loadingPoints: false };
}
}));
};

const fetchData = async () => {
const fetchData = async (usernameToFetch: string) => {
try {
const userData = await fetchUserData(username);
const userData = await fetchUserData(usernameToFetch);

const initialSolvedChallenges = userData.solved_challenges.map(challenge => ({
...challenge,
loadingPoints: true
}));

setUser({...userData, solved_challenges: initialSolvedChallenges});
setUser({ ...userData, solved_challenges: initialSolvedChallenges });
setLoading(false);

const updatedSolvedChallenges = await fetchChallengeDetailsForUser(userData.solved_challenges);
setUser(prevUser => prevUser ? {...prevUser, solved_challenges: updatedSolvedChallenges} : prevUser);
setUser(prevUser => prevUser ? { ...prevUser, solved_challenges: updatedSolvedChallenges } : prevUser);

} catch (err) {
setError(err as Error);
Expand All @@ -63,24 +68,38 @@ const UserInfo: React.FC = () => {
};

useEffect(() => {
fetchData();
if (username) {
fetchData(username);
} else {
const authenticatedUser = getAuthenticatedUser();
if (authenticatedUser) {
fetchData(authenticatedUser);
} else {
setError(new Error('User is not authenticated'));
setLoading(false);
}
}
}, [username]);

const handleRefresh = async () => {
setLoading(true);
try {
const userData = await refreshUserData(username);
if (user) {
const userData = await refreshUserData(user.username);

const initialSolvedChallenges = userData.solved_challenges.map(challenge => ({
...challenge,
loadingPoints: true
}));
const initialSolvedChallenges = userData.solved_challenges.map(challenge => ({
...challenge,
loadingPoints: true
}));

setUser({...userData, solved_challenges: initialSolvedChallenges});
setUser({ ...userData, solved_challenges: initialSolvedChallenges });

const updatedSolvedChallenges = await fetchChallengeDetailsForUser(userData.solved_challenges);
setUser(prevUser => prevUser ? {...prevUser, solved_challenges: updatedSolvedChallenges} : prevUser);
const updatedSolvedChallenges = await fetchChallengeDetailsForUser(userData.solved_challenges);
setUser(prevUser => prevUser ? { ...prevUser, solved_challenges: updatedSolvedChallenges } : prevUser);

} else {
setError(new Error('No user data available'));
}
} catch (err) {
setError(err as Error);
} finally {
Expand All @@ -106,13 +125,13 @@ const UserInfo: React.FC = () => {
last_refreshed={user.last_refreshed}
/>
<button className="refresh-button" onClick={handleRefresh}>
<FontAwesomeIcon icon={faSyncAlt}/>
<FontAwesomeIcon icon={faSyncAlt} />
</button>
<div className="user-graph">
<ScoreGraph users={[user]} singleUser={true}/>
<ScoreGraph users={[user]} singleUser={true} />
</div>
{user.completion && <UserCompletionRadar completion={user.completion} useScore={false}/>}
{user.solved_challenges && <SolvedChallenges solvedChallenges={user.solved_challenges}/>}
{user.completion && <UserCompletionRadar completion={user.completion} useScore={false} />}
{user.solved_challenges && <SolvedChallenges solvedChallenges={user.solved_challenges} />}
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import javax.ws.rs.*;
import javax.ws.rs.core.Response;

@Path("/auth")
@Path("/api/auth")
@Consumes("application/json")
@Produces("application/json")
public class AuthEndpoints {
Expand Down

0 comments on commit d4e7c18

Please sign in to comment.