Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300&family=Roboto&family=Roboto+Slab:wght@900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Mulish:ital,wght@0,200..1000;1,200..1000&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Mullish:wght@300;400;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<title>RECursion</title>
Expand Down
2 changes: 1 addition & 1 deletion src/api/axios.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/api/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/api/loginWithGoogle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/api/register.js
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
35 changes: 29 additions & 6 deletions src/api/userInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
49 changes: 41 additions & 8 deletions src/components/GoogleLogin/Glogin.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,64 @@ 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 (
<div
<button
type="button"
onClick={() => 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 */}
<div className="mr-4">
<GoogleIcon />
</div>
{/* Text */}
<div className="font-head font-bold text-sm ml-0">CONTINUE WITH GOOGLE</div>
</div>
</button>
);
};

Expand Down
3 changes: 3 additions & 0 deletions src/components/Layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const Layout = () => {
}
} catch (error) {
console.error("Profile check failed", error);
if (error.response?.status === 404) {
navigate("/profile/edit", { replace: true });
}
}
}
};
Expand Down
22 changes: 14 additions & 8 deletions src/components/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,30 +56,35 @@ 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");
}
}
};
checkProfileAndRedirect();
}, [token, setLoading, setStatus, navigate, from, justLoggedInWithGoogle]);
}, [token, justLoggedInWithGoogle]);

const handleUsernameChange = (e) => setUsername(e.target.value);
const handlePasswordChange = (e) => setPassword(e.target.value);
Expand Down
126 changes: 71 additions & 55 deletions src/context/AuthContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down