From d215e9a33eefbbd21c8d0cdeab703859f52545fb Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Fri, 20 Mar 2026 13:40:33 +0100 Subject: [PATCH] feat(sprint1-step4): SuperOAuth login frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthContext : fix exports, x-auth-token header, loginWithOAuth(), suppression axios/jwt-decode - Login.jsx : redirect SuperOAuth Discord avec tenantId=clickerz - AuthCallback.jsx : extraction token query param, flow OAuth complet - .env.sample : ajout VITE_SUPEROAUTH_URL - Mode invité préservé (pas de route guard) --- Frontend/.env.sample | 4 +- Frontend/src/context/AuthContext.jsx | 70 +++++++++++++++++++--------- Frontend/src/pages/AuthCallback.jsx | 46 ++++++++++++++++++ Frontend/src/pages/Login.jsx | 32 +++++++++++++ 4 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 Frontend/src/pages/AuthCallback.jsx create mode 100644 Frontend/src/pages/Login.jsx diff --git a/Frontend/.env.sample b/Frontend/.env.sample index 904f873..d04457a 100755 --- a/Frontend/.env.sample +++ b/Frontend/.env.sample @@ -3,5 +3,5 @@ # Backend API URL (call it in React with import.meta.env.VITE_BACKEND_URL) VITE_BACKEND_URL=http://localhost:3310 -# Other Environment Variables (if needed) -# VITE_OTHER_VARIABLE=value +# SuperOAuth URL (OAuth login provider) +VITE_SUPEROAUTH_URL=https://superoauth.tetardtek.com diff --git a/Frontend/src/context/AuthContext.jsx b/Frontend/src/context/AuthContext.jsx index c4c7983..e9110f6 100755 --- a/Frontend/src/context/AuthContext.jsx +++ b/Frontend/src/context/AuthContext.jsx @@ -6,28 +6,34 @@ import React, { useEffect, } from "react"; import PropTypes from "prop-types"; - import axios from "axios"; - import { jwtDecode } from "jwt-decode"; - + const decodeJwtPayload = (token) => + JSON.parse(atob(token.split(".")[1])); + const AuthContext = createContext(); - + const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - + useEffect(() => { const fetchData = async () => { const jwtToken = localStorage.getItem("token"); - + if (jwtToken) { try { - const decodedPayload = jwtDecode(jwtToken); - const res = await axios.get( - `${import.meta.env.VITE_BACKEND_URL}/api/users/${decodedPayload.user}` + const decodedPayload = decodeJwtPayload(jwtToken); + const res = await fetch( + `${import.meta.env.VITE_BACKEND_URL}/api/users/${decodedPayload.user}`, + { + headers: { "x-auth-token": jwtToken }, + } ); - setUser(res.data); + if (!res.ok) throw new Error("Failed to fetch user"); + const data = await res.json(); + setUser(data); } catch (error) { console.error("Error fetching user data:", error); + localStorage.removeItem("token"); } finally { setLoading(false); } @@ -35,15 +41,30 @@ import React, { setLoading(false); } }; - + fetchData(); }, []); - - + + const loginWithOAuth = async (token) => { + const res = await fetch( + `${import.meta.env.VITE_BACKEND_URL}/api/auth/callback?code=${encodeURIComponent(token)}` + ); + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.message || "OAuth login failed"); + } + + localStorage.setItem("token", data.token); + setUser(data.user); + return data.user; + }; + const logout = () => { localStorage.removeItem("token"); setUser(null); }; + const editUser = async (updatedFields) => { try { const response = await fetch( @@ -52,12 +73,12 @@ import React, { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("token")}`, + "x-auth-token": localStorage.getItem("token"), }, body: JSON.stringify(updatedFields), } ); - + if (response.ok) { const updatedUser = await response.json(); setUser((prevUser) => ({ @@ -81,7 +102,7 @@ import React, { throw new Error("An error occurred during user update"); } }; - + const sendPasswordResetEmail = async (email) => { try { const response = await fetch( @@ -94,11 +115,11 @@ import React, { body: JSON.stringify({ mail: email }), } ); - + if (response.ok) { return "Password reset email sent successfully"; } - + const data = await response.json(); throw new Error(data.message || "Error sending password reset email"); } catch (error) { @@ -108,12 +129,13 @@ import React, { ); } }; - + const authContextValue = useMemo(() => { return { user, loading, logout, + loginWithOAuth, editUser, sendPasswordResetEmail, setUser: (newUser) => { @@ -121,22 +143,24 @@ import React, { }, }; }, [user, loading, logout]); - + return ( {children} ); }; - + AuthProvider.propTypes = { children: PropTypes.node.isRequired, }; - + const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within an AuthProvider"); } return context; - }; \ No newline at end of file + }; + + export { AuthProvider, useAuth }; diff --git a/Frontend/src/pages/AuthCallback.jsx b/Frontend/src/pages/AuthCallback.jsx new file mode 100644 index 0000000..f8dead6 --- /dev/null +++ b/Frontend/src/pages/AuthCallback.jsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useSearchParams, Link } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import "../scss/pages.scss"; + +export default function AuthCallback() { + const [searchParams] = useSearchParams(); + const { loginWithOAuth } = useAuth(); + const navigate = useNavigate(); + const [error, setError] = useState(null); + + useEffect(() => { + const token = searchParams.get("token"); + + if (!token) { + setError("Token manquant dans l'URL."); + return; + } + + loginWithOAuth(token) + .then(() => navigate("/", { replace: true })) + .catch((err) => setError(err.message || "Erreur de connexion.")); + }, []); + + if (error) { + return ( +
+
+

Erreur de connexion

+

{error}

+ + Retour au login + +
+
+ ); + } + + return ( +
+
+

Connexion en cours...

+
+
+ ); +} diff --git a/Frontend/src/pages/Login.jsx b/Frontend/src/pages/Login.jsx new file mode 100644 index 0000000..c502a24 --- /dev/null +++ b/Frontend/src/pages/Login.jsx @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import "../scss/pages.scss"; + +const SUPEROAUTH_URL = import.meta.env.VITE_SUPEROAUTH_URL; + +export default function Login() { + const { user } = useAuth(); + const navigate = useNavigate(); + + useEffect(() => { + if (user) navigate("/", { replace: true }); + }, [user, navigate]); + + const handleLogin = () => { + const callbackUrl = `${window.location.origin}/callback`; + window.location.href = `${SUPEROAUTH_URL}/api/v1/oauth/discord?redirectUrl=${encodeURIComponent(callbackUrl)}&tenantId=clickerz`; + }; + + return ( +
+
+

Connexion

+

Connecte-toi pour sauvegarder ta progression.

+ +
+
+ ); +}