feat(sprint1-step4): SuperOAuth login frontend
- 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)
This commit is contained in:
@@ -3,5 +3,5 @@
|
|||||||
# Backend API URL (call it in React with import.meta.env.VITE_BACKEND_URL)
|
# Backend API URL (call it in React with import.meta.env.VITE_BACKEND_URL)
|
||||||
VITE_BACKEND_URL=http://localhost:3310
|
VITE_BACKEND_URL=http://localhost:3310
|
||||||
|
|
||||||
# Other Environment Variables (if needed)
|
# SuperOAuth URL (OAuth login provider)
|
||||||
# VITE_OTHER_VARIABLE=value
|
VITE_SUPEROAUTH_URL=https://superoauth.tetardtek.com
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import axios from "axios";
|
const decodeJwtPayload = (token) =>
|
||||||
import { jwtDecode } from "jwt-decode";
|
JSON.parse(atob(token.split(".")[1]));
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
@@ -21,13 +21,19 @@ import React, {
|
|||||||
|
|
||||||
if (jwtToken) {
|
if (jwtToken) {
|
||||||
try {
|
try {
|
||||||
const decodedPayload = jwtDecode(jwtToken);
|
const decodedPayload = decodeJwtPayload(jwtToken);
|
||||||
const res = await axios.get(
|
const res = await fetch(
|
||||||
`${import.meta.env.VITE_BACKEND_URL}/api/users/${decodedPayload.user}`
|
`${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) {
|
} catch (error) {
|
||||||
console.error("Error fetching user data:", error);
|
console.error("Error fetching user data:", error);
|
||||||
|
localStorage.removeItem("token");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -39,11 +45,26 @@ import React, {
|
|||||||
fetchData();
|
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 = () => {
|
const logout = () => {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const editUser = async (updatedFields) => {
|
const editUser = async (updatedFields) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -52,7 +73,7 @@ import React, {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
"x-auth-token": localStorage.getItem("token"),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(updatedFields),
|
body: JSON.stringify(updatedFields),
|
||||||
}
|
}
|
||||||
@@ -114,6 +135,7 @@ import React, {
|
|||||||
user,
|
user,
|
||||||
loading,
|
loading,
|
||||||
logout,
|
logout,
|
||||||
|
loginWithOAuth,
|
||||||
editUser,
|
editUser,
|
||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
setUser: (newUser) => {
|
setUser: (newUser) => {
|
||||||
@@ -140,3 +162,5 @@ import React, {
|
|||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { AuthProvider, useAuth };
|
||||||
|
|||||||
46
Frontend/src/pages/AuthCallback.jsx
Normal file
46
Frontend/src/pages/AuthCallback.jsx
Normal file
@@ -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 (
|
||||||
|
<section>
|
||||||
|
<div className="containererror">
|
||||||
|
<h1>Erreur de connexion</h1>
|
||||||
|
<p className="message">{error}</p>
|
||||||
|
<Link className="btn-return" to="/login">
|
||||||
|
Retour au login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="containererror">
|
||||||
|
<p className="message">Connexion en cours...</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
Frontend/src/pages/Login.jsx
Normal file
32
Frontend/src/pages/Login.jsx
Normal file
@@ -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 (
|
||||||
|
<section>
|
||||||
|
<div className="containererror">
|
||||||
|
<h1>Connexion</h1>
|
||||||
|
<p className="message">Connecte-toi pour sauvegarder ta progression.</p>
|
||||||
|
<button className="btn-return" onClick={handleLogin} type="button">
|
||||||
|
Se connecter avec SuperOAuth
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user