diff --git a/src/App.jsx b/src/App.jsx index 129a9e7..ff561c7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,20 +4,30 @@ import Register from "./pages/Register"; import CreateNote from "./pages/CreateNote"; import ViewNotes from "./pages/ViewNotes"; import Navbar from "./components/Navbar"; +import ProtectedRoute from "./components/auth/ProtectedRoute"; +import { checkAuthStatus } from "./store/slices/authSlice"; +import { useDispatch, useSelector } from "react-redux"; +import { useEffect } from "react"; const App = () => { + const dispatch = useDispatch(); + const { status } = useSelector((state) => state.auth); + + useEffect(() => { + dispatch(checkAuthStatus()); + }, [dispatch]); + return (
- - - } /> - } /> - } /> - } /> - + + } /> + } /> + } /> + } /> +
); diff --git a/src/components/auth/ProtectedRoute.jsx b/src/components/auth/ProtectedRoute.jsx index 0c71b6c..231766a 100644 --- a/src/components/auth/ProtectedRoute.jsx +++ b/src/components/auth/ProtectedRoute.jsx @@ -1,11 +1,11 @@ -import { Navigate, useLocation } from "react-router-dom"; +import { Navigate } from "react-router-dom"; import { useSelector } from "react-redux"; const ProtectedRoute = ({ children, requireAuth }) => { - const { isAuthenticated, loading } = useSelector((state) => state.auth); + const { isAuthenticated, status } = useSelector((state) => state.auth); // Show loading state while checking authentication - if (loading) { + if (status === "loading") { return (
@@ -17,10 +17,14 @@ const ProtectedRoute = ({ children, requireAuth }) => { } // TODO: If route requires authentication and user is not authenticated, redirect to login - + if (requireAuth && !isAuthenticated) { + return ; + } //TODO: If route requires unauthenticated user and user is authenticated, redirect to notes - + if (!requireAuth && isAuthenticated) { + return ; + } // Otherwise, render the children return children; diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 4f6d4f7..c48b58c 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -1,12 +1,53 @@ + + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { login } from "../store/slices/authSlice"; + +const loginSchema = z.object({ + email: z.string().email("valid email is required"), + password: z.string().min(6, "Password must be at least 6 characters"), +}); + + + + const Login = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { status, error } = useSelector((state) => state.auth); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); + + const onSubmit = async (data) => { + try { + await dispatch(login(data)).unwrap(); + navigate("/notes"); + } catch (err) { + console.error("Failed to login:", err); + } + }; + return (

Login to Your Account

- -
+ {error && ( +
+ {error} +
+ )} +
@@ -32,16 +79,26 @@ const Login = () => { + {errors.password && ( +

+ {errors.password.message} +

+ )}
diff --git a/src/pages/Register.jsx b/src/pages/Register.jsx index ba6f0bd..06c0b43 100644 --- a/src/pages/Register.jsx +++ b/src/pages/Register.jsx @@ -1,12 +1,48 @@ + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { register as registerPage } from "../store/slices/authSlice"; +import { registerSchema } from "../schema/authSchema"; + + + + const Register = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { status, error } = useSelector((state) => state.auth); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + }); + + const onSubmit = async (data) => { + try { + await dispatch(registerPage(data)).unwrap(); + + navigate("/login"); + } catch (err) { + console.error("Failed to register:", err); + } + }; return (

- Create an Account + Create An Account

- -
+ {error && ( +
+ {error} +
+ )} +
@@ -29,12 +69,19 @@ const Register = () => { > Password + + {errors.password && ( +

+ {errors.password.message} +

+ )}
@@ -47,16 +94,24 @@ const Register = () => { + {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )}
@@ -72,3 +127,6 @@ const Register = () => { }; export default Register; + + + diff --git a/src/pages/ViewNotes.jsx b/src/pages/ViewNotes.jsx index 0b7eaca..e85f40c 100644 --- a/src/pages/ViewNotes.jsx +++ b/src/pages/ViewNotes.jsx @@ -11,7 +11,8 @@ const ViewNotes = () => { useEffect(() => { dispatch(fetchNotes()); - }, [dispatch]); + + }, []); const handleDelete = async (id) => { if (!window.confirm("Are you sure you want to delete this note?")) { diff --git a/src/store/slices/authSlice.js b/src/store/slices/authSlice.js index 35778d0..125bcac 100644 --- a/src/store/slices/authSlice.js +++ b/src/store/slices/authSlice.js @@ -2,6 +2,7 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import axios from "axios"; import { BASE_URL } from "../BaseUrl"; + // Configure axios to include credentials axios.defaults.withCredentials = true; @@ -13,9 +14,17 @@ export const checkAuthStatus = createAsyncThunk( // 1. Make a GET request to /auth/check // 2. Return the response data // 3. Handle errors appropriately + // try { + + try { + const response = await axios.get(`${BASE_URL}/auth/check`); + return response.data; + } catch (error) { + return rejectWithValue(error.response.data); + } + } ); - // TODO: Implement login thunk export const login = createAsyncThunk( "auth/login", @@ -24,6 +33,12 @@ export const login = createAsyncThunk( // 1. Make a POST request to /auth/login with credentials // 2. Return the response data // 3. Handle errors appropriately + try { + const response = await axios.post(`${BASE_URL}/auth/login`, credentials) + return response.data.user; + } catch (error) { + return rejectWithValue(error.response.data); + } } ); @@ -35,6 +50,13 @@ export const register = createAsyncThunk( // 1. Make a POST request to /auth/register with userData // 2. Return the response data // 3. Handle errors appropriately + try { + const response = await axios.post(`${BASE_URL}/auth/register`, userData) + return response.data.user; + } catch (error) { + return rejectWithValue(error.response.data); + } + } ); @@ -45,13 +67,20 @@ export const logout = createAsyncThunk( // TODO: Implement logout functionality // 1. Make a POST request to /auth/logout // 2. Handle errors appropriately + try { + const response = await axios.post(`${BASE_URL}/auth/logout`) + + } catch (error) { + + return rejectWithValue(error.response.data); + } } ); const initialState = { user: null, isAuthenticated: false, - loading: true, + status: "idle", error: null, }; @@ -66,9 +95,58 @@ const authSlice = createSlice({ extraReducers: (builder) => { builder; // TODO: Add cases for checkAuthStatus + builder.addCase(checkAuthStatus.pending, (state) => { + state.status = "loading"; + }) + builder.addCase(checkAuthStatus.fulfilled, (state, action) => { + state.status = "succeeded"; + state.isAuthenticated = true; + state.user = action.payload; + state.error = null; + }) + builder.addCase(checkAuthStatus.rejected, (state) => { + state.status = "idle"; + state.isAuthenticated = false; + state.user = null; + }) // TODO: Add cases for login + builder.addCase(login.pending, (state) => { + state.status = "loading"; + state.error = null; + }) + builder.addCase(login.fulfilled, (state, action) => { + state.status = "succeeded"; + state.isAuthenticated = true; + state.user = action.payload; + state.error = null; + }) + builder.addCase(login.rejected, (state, action) => { + state.status = "failed"; + state.error = action.error.message; + }) // TODO: Add cases for register + builder.addCase(register.pending, (state) => { + state.status = "loading"; + state.error = null; + }) + builder.addCase(register.fulfilled, (state, action) => { + state.status = "succeeded"; + state.isAuthenticated = false; + state.user = action.payload; + state.error = null; + }) + builder.addCase(register.rejected, (state, action) => { + state.status = "failed"; + state.error = action.error.message; + }) // TODO: Add cases for logout + builder.addCase(logout.fulfilled, (state) => { + state.status = "idle"; + state.isAuthenticated = false; + state.user = null; + state.error = null; + }); + }, });