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:
2026-03-20 13:40:33 +01:00
parent a52746ed0c
commit d215e9a33e
4 changed files with 127 additions and 25 deletions

View File

@@ -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 (
<AuthContext.Provider value={authContextValue}>
{children}
</AuthContext.Provider>
);
};
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;
};
};
export { AuthProvider, useAuth };