diff --git a/public/index.html b/public/index.html index cd59e32..df03186 100644 --- a/public/index.html +++ b/public/index.html @@ -12,7 +12,6 @@ - RECursion diff --git a/src/api/axios.js b/src/api/axios.js index 3af7885..408c3e1 100644 --- a/src/api/axios.js +++ b/src/api/axios.js @@ -1,6 +1,6 @@ import axios from "axios"; -const api = process.env.REACT_APP_BACKEND_URL + "/api"; +const api = (process.env.REACT_APP_BACKEND_URL || "") + "/api"; export default axios.create({ baseURL: api, diff --git a/src/api/login.js b/src/api/login.js index 9dff8c6..d47ed19 100644 --- a/src/api/login.js +++ b/src/api/login.js @@ -2,7 +2,7 @@ import axios from "./axios"; export const login = async (formData) => { const response = await axios.post( - `${process.env.REACT_APP_BACKEND_URL}/api/token/`, + "/token/", { username: formData.username, password: formData.password, diff --git a/src/api/loginWithGoogle.js b/src/api/loginWithGoogle.js index 32dde71..0eff48f 100644 --- a/src/api/loginWithGoogle.js +++ b/src/api/loginWithGoogle.js @@ -2,7 +2,7 @@ // define apis for login with google export const loginWithGoogle = async (token) => { const response = await fetch( - `${process.env.REACT_APP_BACKEND_URL}/api/token/google/`, // TODO : use axios + `${process.env.REACT_APP_BACKEND_URL || ""}/api/token/google/`, // TODO : use axios { method: "POST", headers: { diff --git a/src/api/register.js b/src/api/register.js index a4def1f..e6a082a 100644 --- a/src/api/register.js +++ b/src/api/register.js @@ -1,7 +1,7 @@ // define apis for login export const register = async (formData) => { const response = await fetch( - `${process.env.REACT_APP_BACKEND_URL}/api/users/register/`, // TODO : use axios + `${process.env.REACT_APP_BACKEND_URL || ""}/api/users/register/`, // TODO : use axios { method: "POST", headers: { diff --git a/src/api/userInfo.js b/src/api/userInfo.js index 0d211b8..570c277 100644 --- a/src/api/userInfo.js +++ b/src/api/userInfo.js @@ -2,19 +2,42 @@ // define apis for profile page import axios from "./axios"; import { API_ROUTES } from "../utils/api_routes"; +import jwtDecode from "jwt-decode"; const USER_URL = API_ROUTES.USERS; export const getProfile = async () => { + let username = null; const user = localStorage.getItem("user"); - if (!user) { - throw new Error("User not found in local storage"); + if (user) { + try { + const parsedUser = JSON.parse(user); + if (parsedUser && parsedUser.username) { + username = parsedUser.username; + } + } catch (e) {} + } + + if (!username) { + const authTokens = localStorage.getItem("authTokens"); + if (authTokens) { + try { + const parsedTokens = JSON.parse(authTokens); + if (parsedTokens?.access) { + const decoded = jwtDecode(parsedTokens.access); + if (decoded?.username) { + username = decoded.username; + } else if (decoded?.email) { + username = decoded.email.split("@")[0]; + } + } + } catch (e) {} + } } - const parsedUser = JSON.parse(user); - if (!parsedUser || !parsedUser.username) { - throw new Error("Invalid user data in local storage"); + + if (!username) { + throw new Error("User not found in local storage"); } - const username = parsedUser.username; const response = await axios.get(`${USER_URL}/${username}/`, { headers: { diff --git a/src/components/GoogleLogin/Glogin.jsx b/src/components/GoogleLogin/Glogin.jsx index 8769189..968be34 100644 --- a/src/components/GoogleLogin/Glogin.jsx +++ b/src/components/GoogleLogin/Glogin.jsx @@ -4,23 +4,56 @@ import { useGoogleLogin } from "@react-oauth/google"; import GoogleIcon from "./GoogleIcon"; import { loginWithGoogle } from "../../api/loginWithGoogle"; import useAuth from "../../hooks/useAuth"; +import { useToast } from "@chakra-ui/react"; + const Glogin = ({ setJustLoggedInWithGoogle }) => { - const { token, decodeTokens, setStatus, status } = useAuth(); + const { decodeTokens, setStatus } = useAuth(); + const toast = useToast(); const login = useGoogleLogin({ - onSuccess: (tokenResponse) => { - loginWithGoogle(tokenResponse.access_token).then((res) => { - if (setJustLoggedInWithGoogle && res.is_new_user) { + onSuccess: async (tokenResponse) => { + try { + setStatus("submitting"); + const res = await loginWithGoogle(tokenResponse.access_token); + if (setJustLoggedInWithGoogle && res?.is_new_user) { setJustLoggedInWithGoogle(true); } - decodeTokens(res); + await decodeTokens(res); + } catch (err) { + console.error("Google login backend error:", err); + toast({ + title: "Google Login Failed", + description: err.message || "Failed to authenticate with backend", + position: "top", + status: "error", + duration: 3000, + isClosable: true, + }); + setStatus("typing"); + } + }, + onError: (errorResponse) => { + console.error("Google OAuth error:", errorResponse); + toast({ + title: "Google Sign-In Error", + description: + errorResponse?.error_description || + errorResponse?.error || + "Google sign-in was cancelled or popup was blocked. Please enable popups for this site.", + position: "top", + status: "error", + duration: 4000, + isClosable: true, }); + setStatus("typing"); }, }); + return ( -
login()} - className="flex items-center justify-center w-full bg-transparent p-2 text-white rounded-lg cursor-pointer hover:bg-[#58cdff] font-opensans border border-[#58cdff]" + className="flex items-center justify-center w-full bg-transparent p-2 text-white rounded-lg cursor-pointer hover:bg-[#58cdff] font-opensans border border-[#58cdff] transition duration-200" > {/* Google Icon */}
@@ -28,7 +61,7 @@ const Glogin = ({ setJustLoggedInWithGoogle }) => {
{/* Text */}
CONTINUE WITH GOOGLE
-
+ ); }; diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index 5a1750b..4cad235 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -51,6 +51,9 @@ const Layout = () => { } } catch (error) { console.error("Profile check failed", error); + if (error.response?.status === 404) { + navigate("/profile/edit", { replace: true }); + } } } }; diff --git a/src/components/Login.jsx b/src/components/Login.jsx index 3aeabc1..3f3d1c3 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -19,10 +19,11 @@ const Login = () => { const stateFrom = location.state?.from; let from = "/"; if (stateFrom) { - from = stateFrom.pathname + (stateFrom.search || ""); + from = stateFrom.pathname + (stateFrom.search || ""); } + const targetPath = from === "/login" ? "/" : from; - const { token, loginUser, setStatus, status } = useAuth(); + const { token, loginUser, setStatus, status, logoutUser } = useAuth(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [rememberMe, setRememberMe] = useState(false); // State for remember me checkbox @@ -55,22 +56,27 @@ const Login = () => { navigate("/profile/edit"); return; } - + setLoading(true); // Keep loading while we check profile try { const profile = await getProfile(); // Check if profile is incomplete (Strict check for Name and College) - const isNameMissing = !profile.name || (typeof profile.name === "string" && profile.name.trim() === ""); - const isCollegeMissing = !profile.college || (typeof profile.college === "string" && profile.college.trim() === ""); + const isNameMissing = !profile?.name || (typeof profile.name === "string" && profile.name.trim() === ""); + const isCollegeMissing = !profile?.college || (typeof profile.college === "string" && profile.college.trim() === ""); if (isNameMissing || isCollegeMissing) { navigate("/profile/edit"); } else { - navigate(from); + navigate(targetPath); } } catch (error) { console.error("Error checking profile:", error); - navigate(from); // Default to home/from on error + if (error.response?.status === 401) { + if (logoutUser) logoutUser(); + } else { + // Profile not found or incomplete - allow user to fill profile details + navigate("/profile/edit"); + } } finally { setLoading(false); setStatus("typing"); @@ -78,7 +84,7 @@ const Login = () => { } }; checkProfileAndRedirect(); - }, [token, setLoading, setStatus, navigate, from, justLoggedInWithGoogle]); + }, [token, justLoggedInWithGoogle]); const handleUsernameChange = (e) => setUsername(e.target.value); const handlePasswordChange = (e) => setPassword(e.target.value); diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js index 9618cf1..8d1da48 100644 --- a/src/context/AuthContext.js +++ b/src/context/AuthContext.js @@ -18,91 +18,107 @@ export const AuthProvider = ({ children }) => { const [status, setStatus] = useState("typing"); const toast = useToast(); const navigate = useNavigate(); - const [user, setUser] = useState( - localStorage.getItem("user") - ? JSON.parse(localStorage.getItem("user")) - : null - ); - const [authToken, setAuthToken] = useState( - localStorage.getItem("authTokens") - ? JSON.parse(localStorage.getItem("authTokens")) - : null - ); + const [authToken, setAuthToken] = useState(() => { + try { + const saved = localStorage.getItem("authTokens"); + return saved ? JSON.parse(saved) : null; + } catch (e) { + return null; + } + }); - const decodeTokens = async (tokens) => { + const [user, setUser] = useState(() => { + try { + const savedUser = localStorage.getItem("user"); + if (savedUser) return JSON.parse(savedUser); + + const savedTokens = localStorage.getItem("authTokens"); + if (savedTokens) { + const parsed = JSON.parse(savedTokens); + if (parsed?.access) { + const decoded = jwtDecode(parsed.access); + return { + id: decoded.user_id, + username: decoded.username || decoded.email?.split("@")[0], + role: "normal", + }; + } + } + return null; + } catch (e) { + return null; + } + }); - if (!tokens.access) { + const decodeTokens = async (tokens) => { + if (!tokens || !tokens.access) { toast({ title: "Cant Authorize", - description: tokens.response, + description: tokens?.response || "Failed to authenticate", position: "top", status: "error", duration: 3000, isClosable: true, }); + setStatus("typing"); return; } - setAuthToken({ + + setAuthToken({ access: tokens.access, refresh: tokens.refresh, }); localStorage.setItem("authTokens", JSON.stringify(tokens)); - + + let role = "normal"; try { const res = await getProfileRoles(jwtDecode(tokens?.access).user_id); - localStorage.setItem( - "user", - JSON.stringify({ - id: jwtDecode(tokens?.access).user_id, - username: jwtDecode(tokens?.access).email.split("@")[0], - role: res.role, - }) - ); - setUser({ - id: jwtDecode(tokens?.access).user_id, - username: jwtDecode(tokens?.access).email.split("@")[0], - email: jwtDecode(tokens?.access).email, - role: res.role, - }); + if (res?.role) role = res.role; } catch (error) { - console.error("Error fetching roles:", error); - // Optional: Toast specific to role fetching failure? - // For now, allow login but maybe warn? Or just log it. - // Ensuring it doesn't crash the Google Login flow entirely (though token is valid). - toast({ - title: "Profile Error", - description: "Could not fetch user roles.", - status: "warning", - duration: 3000, - isClosable: true, - }); + console.error("Error fetching roles:", error); } + + const decoded = jwtDecode(tokens?.access); + const username = decoded.username || decoded.email.split("@")[0]; + const userData = { + id: decoded.user_id, + username: username, + email: decoded.email, + role: role, + }; + + localStorage.setItem("user", JSON.stringify(userData)); + setUser(userData); + setStatus("typing"); }; const loginUser = async (formData) => { try { const data = await login(formData); - localStorage.setItem("authTokens", JSON.stringify(data)); + if (!data || !data.access) { + throw new Error(data?.response || "Invalid login response"); + } - const res = await getProfileRoles(jwtDecode(data?.access).user_id); + localStorage.setItem("authTokens", JSON.stringify(data)); + setAuthToken(data); - localStorage.setItem( - "user", - JSON.stringify({ - id: jwtDecode(data?.access).user_id, - username: formData.username, - role: res.role, - }) - ); + let role = "normal"; + try { + const res = await getProfileRoles(jwtDecode(data?.access).user_id); + if (res?.role) role = res.role; + } catch (e) { + console.warn("Could not fetch user role:", e); + } - setUser({ + const userData = { id: jwtDecode(data?.access).user_id, username: formData.username, - role: res.role, - }); - - setAuthToken(data); + role: role, + }; + localStorage.setItem("user", JSON.stringify(userData)); + setUser(userData); + setStatus("typing"); } catch (err) { setLoading(false); const errorMessage = getApiError(err);