diff --git a/src/.env b/src/.env
index 93fa24a..9eeb008 100644
--- a/src/.env
+++ b/src/.env
@@ -1 +1 @@
-VITE_API_URL=http://localhost:8000
\ No newline at end of file
+VITE_API_URL=http://127.0.0.1:8000
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 0875ba0..e3614bd 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,24 +3,30 @@ import "./App.css";
import router from "./router/router";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { AuthProvider } from "./context/AuthContext";
+
+const queryClient = new QueryClient();
function App() {
return (
- <>
-
-
- >
+
+
+
+
+
+
);
}
diff --git a/src/api/authApi.ts b/src/api/authApi.ts
new file mode 100644
index 0000000..1f24552
--- /dev/null
+++ b/src/api/authApi.ts
@@ -0,0 +1,19 @@
+import type {
+ LoginPayload,
+ LoginResponse,
+ MeResponse,
+ User,
+} from "../components/shared/types/AuthTypes";
+import axiosInstance from "./axiosInstance";
+
+export const loginRequest = async (
+ data: LoginPayload
+): Promise => {
+ const res = await axiosInstance.post("/api/auth/login", data);
+ return res.data;
+};
+
+export const fetchMe = async (): Promise => {
+ const res = await axiosInstance.get("/api/auth/me");
+ return res.data.user;
+};
diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts
index 0981bf0..686a88b 100644
--- a/src/api/axiosInstance.ts
+++ b/src/api/axiosInstance.ts
@@ -1,7 +1,7 @@
import axios from "axios";
const axiosInstance = axios.create({
- baseURL: import.meta.env.VITE_API_URL,
+ baseURL: "http://127.0.0.1:8000",
timeout: 30 * 1000,
});
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 3c860f4..ea23ba5 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -7,11 +7,16 @@ import Typography from "@mui/material/Typography";
import Menu from "@mui/material/Menu";
import MenuIcon from "@mui/icons-material/Menu";
import Container from "../../components/shared/Container";
-import StarIcon from '@mui/icons-material/Star';
+import StarIcon from "@mui/icons-material/Star";
import Button from "@mui/material/Button";
import MenuItem from "@mui/material/MenuItem";
+import { useAuth } from "../../context/AuthContext";
+import { useNavigate } from "react-router-dom";
function Header() {
+ const { user, logout } = useAuth();
+ const navigate = useNavigate();
+
const [anchorElNav, setAnchorElNav] = React.useState(
null
);
@@ -24,11 +29,13 @@ function Header() {
setAnchorElNav(null);
};
+ const goToAdmin = () => navigate("/admin");
+
return (
-
+
-
+
-
-
-
+ {!user && (
+
+ )}
+
+ {user && (
+ <>
+
+ {user.type === "admin" && (
+
+ )}
+ >
+ )}
diff --git a/src/components/ProtectedRoute/ProtectedRoute.tsx b/src/components/ProtectedRoute/ProtectedRoute.tsx
new file mode 100644
index 0000000..1b4fcbc
--- /dev/null
+++ b/src/components/ProtectedRoute/ProtectedRoute.tsx
@@ -0,0 +1,18 @@
+import type { ReactNode } from "react";
+import { useAuth } from "../../context/AuthContext";
+import Unauthorized from "../../pages/Unauthorized/Unauthorized";
+import Container from "../shared/Container";
+
+interface ProtectedRouteProps {
+ children: ReactNode;
+}
+
+export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
+ const { user, isLoading } = useAuth();
+
+ if (isLoading) return Loading...;
+
+ if (!user) return ;
+
+ return <>{children}>;
+};
diff --git a/src/components/Question/Question.tsx b/src/components/Question/Question.tsx
index 95b9b9d..ec13965 100644
--- a/src/components/Question/Question.tsx
+++ b/src/components/Question/Question.tsx
@@ -40,7 +40,7 @@ const Question = ({ question }: QuestionProps) => {
Difficulty: {question.difficulty}
-
+
{question.author}
diff --git a/src/components/shared/types/AuthTypes.ts b/src/components/shared/types/AuthTypes.ts
new file mode 100644
index 0000000..39fb308
--- /dev/null
+++ b/src/components/shared/types/AuthTypes.ts
@@ -0,0 +1,24 @@
+export interface User {
+ id: number;
+ username: string;
+ email: string;
+ email_verified_at: string | null;
+ type: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface LoginResponse {
+ access_token: string;
+ token_type: string;
+ user: User;
+}
+
+export interface LoginPayload {
+ email: string;
+ password: string;
+}
+
+export interface MeResponse {
+ user: User;
+}
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
new file mode 100644
index 0000000..79a330b
--- /dev/null
+++ b/src/context/AuthContext.tsx
@@ -0,0 +1,55 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { toast } from "react-toastify";
+import { useCurrentUser } from "../hooks/useCurrentUser";
+import { useLogin } from "../hooks/useLogin";
+import type { LoginPayload, User } from "../components/shared/types/AuthTypes";
+import { createContext, useContext, type ReactNode } from "react";
+
+interface AuthContextType {
+ user: User | null;
+ login: (data: LoginPayload) => Promise;
+ logout: () => void;
+ isLoading: boolean;
+}
+
+const AuthContext = createContext(undefined);
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) throw new Error("useAuth must be used within AuthProvider");
+ return context;
+};
+
+export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ const queryClient = useQueryClient();
+ const { data: user, isLoading } = useCurrentUser();
+ const loginMutation = useLogin();
+
+ const login = async (payload: LoginPayload) => {
+ await loginMutation.mutateAsync(payload, {
+ onSuccess: (data) => {
+ localStorage.setItem("access_token", data.access_token);
+ queryClient.setQueryData(["me"], data.user);
+ toast.success(`Welcome ${data.user.username}`);
+ },
+ onError: (err: any) => {
+ toast.error(err.response?.data?.message || "Login failed");
+ },
+ });
+ };
+
+ const logout = () => {
+ localStorage.removeItem("access_token");
+ queryClient.removeQueries({ queryKey: ["me"] });
+ toast.info("Logged out");
+ window.location.href = "/login";
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts
new file mode 100644
index 0000000..42c056b
--- /dev/null
+++ b/src/hooks/useCurrentUser.ts
@@ -0,0 +1,12 @@
+import { useQuery } from "@tanstack/react-query";
+import type { User } from "../components/shared/types/AuthTypes";
+import { fetchMe } from "../api/authApi";
+
+export const useCurrentUser = () =>
+ useQuery({
+ queryKey: ["me"],
+ queryFn: fetchMe,
+ enabled: !!localStorage.getItem("access_token"),
+ retry: false,
+ staleTime: 5 * 60 * 1000,
+ });
diff --git a/src/hooks/useLogin.ts b/src/hooks/useLogin.ts
new file mode 100644
index 0000000..ddb965f
--- /dev/null
+++ b/src/hooks/useLogin.ts
@@ -0,0 +1,11 @@
+import { useMutation } from "@tanstack/react-query";
+import type {
+ LoginPayload,
+ LoginResponse,
+} from "../components/shared/types/AuthTypes";
+import { loginRequest } from "../api/authApi";
+
+export const useLogin = () =>
+ useMutation({
+ mutationFn: loginRequest,
+ });
diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx
new file mode 100644
index 0000000..7656dce
--- /dev/null
+++ b/src/pages/LoginPage/LoginPage.tsx
@@ -0,0 +1,64 @@
+import { useEffect, useState } from "react";
+import { useAuth } from "../../context/AuthContext";
+import Container from "../../components/shared/Container";
+import { Box, Button, Paper, TextField, Typography } from "@mui/material";
+import { useNavigate } from "react-router-dom";
+
+const LoginPage = () => {
+ const { login, user } = useAuth();
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (user) {
+ navigate("/");
+ }
+ }, [user, navigate]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ await login({ email, password });
+ };
+
+ return (
+
+
+
+ Login
+
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/src/pages/ProfilePage/Profilepage.tsx b/src/pages/ProfilePage/Profilepage.tsx
new file mode 100644
index 0000000..deb835f
--- /dev/null
+++ b/src/pages/ProfilePage/Profilepage.tsx
@@ -0,0 +1,43 @@
+import { Box, Button, Typography } from "@mui/material";
+import { useAuth } from "../../context/AuthContext";
+import Container from "../../components/shared/Container";
+import { useNavigate } from "react-router-dom";
+
+const ProfilePage = () => {
+ const { user, logout } = useAuth();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ Profile
+
+
+ Username: {user?.username}
+
+
+ E-mail: {user?.email}
+
+
+ Role: {user?.type}
+
+
+ {user?.type === "admin" && (
+
+ )}
+
+
+ );
+};
+
+export default ProfilePage;
diff --git a/src/pages/Unauthorized/Unauthorized.tsx b/src/pages/Unauthorized/Unauthorized.tsx
new file mode 100644
index 0000000..b8d16d5
--- /dev/null
+++ b/src/pages/Unauthorized/Unauthorized.tsx
@@ -0,0 +1,18 @@
+import { Box, Typography } from "@mui/material";
+
+export default function Unauthorized() {
+ return (
+
+
+ 401
+
+ Unauthorized
+ You do not have access to this page.
+
+ );
+}
diff --git a/src/router/router.tsx b/src/router/router.tsx
index c2201c4..ff5b336 100644
--- a/src/router/router.tsx
+++ b/src/router/router.tsx
@@ -1,23 +1,33 @@
import { createBrowserRouter } from "react-router-dom";
import MainLayout from "../layouts/MainLayout/MainLayout";
import IndexPage from "../pages/IndexPage/IndexPage";
-import QuestionsPage from "../pages/QuestionsPage/QuestionsPage";
+import LoginPage from "../pages/LoginPage/LoginPage";
+import ProfilePage from "../pages/ProfilePage/Profilepage";
+import { ProtectedRoute } from "../components/ProtectedRoute/ProtectedRoute";
-const router = createBrowserRouter ([
- {
- path:'/',
- element: ,
- children: [
- {
- index:true,
- element:
- },
- {
- path: "/questions",
- element:
- }
- ]
- }
-])
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: "/login",
+ element: ,
+ },
+ {
+ path: "/profile",
+ element: (
+
+
+
+ ),
+ },
+ ],
+ },
+]);
-export default router;
\ No newline at end of file
+export default router;