From b63d041ca76eaf20c97ea4a6d091107ca3a512d8 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 8 Jan 2026 15:10:01 +0800 Subject: [PATCH] please work --- client/src/components/ui/logout.tsx | 22 +++++ client/src/components/ui/navbar.tsx | 41 ++++++++++ client/src/pages/[id]/tasks.tsx | 45 ++++++++--- client/src/pages/_app.tsx | 8 ++ client/src/pages/index.tsx | 46 ++++------- client/src/pages/login.tsx | 119 ++++++++++++++++++++++++++++ client/src/pages/register.tsx | 96 ++++++++++++++++++++++ server/server/settings.py | 3 +- server/server/urls.py | 3 + server/task_planner/urls.py | 4 + server/task_planner/views.py | 55 ++++++++++--- 11 files changed, 387 insertions(+), 55 deletions(-) create mode 100644 client/src/components/ui/logout.tsx create mode 100644 client/src/components/ui/navbar.tsx create mode 100644 client/src/pages/login.tsx create mode 100644 client/src/pages/register.tsx diff --git a/client/src/components/ui/logout.tsx b/client/src/components/ui/logout.tsx new file mode 100644 index 0000000..22d787b --- /dev/null +++ b/client/src/components/ui/logout.tsx @@ -0,0 +1,22 @@ +"use client"; +import { useRouter } from "next/navigation"; + +export function LogoutButton() { + const router = useRouter(); + function logout() { + localStorage.removeItem("access"); + localStorage.removeItem("refresh"); + localStorage.removeItem("user_id"); + + router.push("/login"); + } + + return ( + + ); +} diff --git a/client/src/components/ui/navbar.tsx b/client/src/components/ui/navbar.tsx new file mode 100644 index 0000000..8706edf --- /dev/null +++ b/client/src/components/ui/navbar.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { LogoutButton } from "@/components/ui/logout"; + +export function Navbar() { + return ( + + ); +} diff --git a/client/src/pages/[id]/tasks.tsx b/client/src/pages/[id]/tasks.tsx index 7529bc1..563fee9 100644 --- a/client/src/pages/[id]/tasks.tsx +++ b/client/src/pages/[id]/tasks.tsx @@ -29,20 +29,43 @@ interface Item { export default function TasksPage() { const router = useRouter(); - const { id } = router.query; + const [userId, setUserId] = useState(null); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { - if (!id) return; - - async function fetchTasks() { + async function fetchData() { try { - // Remove the query bit when authentication is added. - const response = await fetch( - `http://localhost:8000/api/planner/tasks/?user_id=${id}`, + const token = localStorage.getItem("access"); + if (!token) { + router.push("/login"); //redirect to login if unauthorised + return; + } + const auth = await fetch( + "http://localhost:8000/api/planner/protected/", + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + if (!auth.ok) { + router.push("/login"); //redirect to login if unauthorised + return; + } + + const user = await auth.json(); + setUserId(user.user_id); + const tasksFetch = await fetch( + `http://localhost:8000/api/planner/tasks/`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, ); - const data = await response.json(); + + const data = await tasksFetch.json(); setItems(data); } catch (err) { console.error("Failed to load tasks:", err); @@ -50,8 +73,8 @@ export default function TasksPage() { setLoading(false); } } - fetchTasks(); - }, [id]); + fetchData(); + }, [router]); if (loading) { return

Loading tasks...

; @@ -99,7 +122,7 @@ export default function TasksPage() {

Add Task

- +
); diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx index 628e9f2..69bedab 100644 --- a/client/src/pages/_app.tsx +++ b/client/src/pages/_app.tsx @@ -3,13 +3,21 @@ import "@/styles/globals.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import type { AppProps } from "next/app"; +import { useRouter } from "next/router"; + +import { Navbar } from "@/components/ui/navbar"; const queryClient = new QueryClient(); export default function App({ Component, pageProps }: AppProps) { + const router = useRouter(); + const noNavbarRoutes = ["/login", "/register", "/"]; + const showNavbar = !noNavbarRoutes.includes(router.pathname); + return ( + {showNavbar && } ); diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index df27d53..cbb2744 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -1,36 +1,18 @@ -import { Inter as FontSans } from "next/font/google"; -import { useState } from "react"; - -import { usePings } from "@/hooks/pings"; -import { cn } from "@/lib/utils"; - -import { Button } from "../components/ui/button"; - -const fontSans = FontSans({ - subsets: ["latin"], - variable: "--font-sans", -}); - -export default function Home() { - const [clicked, setClicked] = useState(false); - const { data, isLoading } = usePings({ - enabled: clicked, - }); +import { useRouter } from "next/navigation"; +export default function Landing() { + const router = useRouter(); + function ToSignIn() { + router.push("/register"); + } return ( -
-

wTest title

- -

- Response from server: {data as string} -

-
+
+ +
); } diff --git a/client/src/pages/login.tsx b/client/src/pages/login.tsx new file mode 100644 index 0000000..48b2c77 --- /dev/null +++ b/client/src/pages/login.tsx @@ -0,0 +1,119 @@ +"use client"; +import { useRouter } from "next/navigation"; + +export default function Login() { + const router = useRouter(); + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + const formData = new FormData(e.currentTarget); + + const res = await fetch("http://127.0.0.1:8000/api/auth/login/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: formData.get("username"), + email: formData.get("email"), + password: formData.get("password"), + }), + }); + + const data = await res.json(); + if (!res.ok) { + alert("Invalid login"); + return; + } + + localStorage.setItem("access", data.access); + localStorage.setItem("refresh", data.refresh); + + const token = localStorage.getItem("access"); + + const userFetch = await fetch( + "http://127.0.0.1:8000/api/planner/protected", + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + + const user = await userFetch.json(); + router.push(`/${user.user_id}/tasks`); + } + + return ( +
+
+

+ Log in to your account +

+
+ +
+
+ +
+ +
+ + +
+ +
+ +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+
+ ); +} diff --git a/client/src/pages/register.tsx b/client/src/pages/register.tsx new file mode 100644 index 0000000..14ccd81 --- /dev/null +++ b/client/src/pages/register.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export default function Register() { + const router = useRouter(); + const [error, setError] = useState(""); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + const res = await fetch("http://127.0.0.1:8000/api/planner/register/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: formData.get("username"), + email: formData.get("email"), + password: formData.get("password"), + }), + }); + + const data = await res.json(); + if (!res.ok) { + setError(data.error || "Registration failed"); + return; + } + router.push("/login"); + } + + return ( +
+
+

+ Create an Account +

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + {error &&

{error}

} +
+
+
+
+ ); +} diff --git a/server/server/settings.py b/server/server/settings.py index 1e05ddf..b9edee7 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -46,6 +46,7 @@ "django.contrib.staticfiles", # "django_extensions", "rest_framework", + "rest_framework_simplejwt", "corsheaders", "healthcheck", "user_profile", @@ -53,6 +54,7 @@ ] MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", @@ -60,7 +62,6 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "corsheaders.middleware.CorsMiddleware", ] diff --git a/server/server/urls.py b/server/server/urls.py index d9eaca5..801886e 100644 --- a/server/server/urls.py +++ b/server/server/urls.py @@ -28,4 +28,7 @@ path("api/healthcheck/", include("healthcheck.urls")), path("api/user/", include("user_profile.urls")), path("api/planner/", include("task_planner.urls")), + path('', include("django.contrib.auth.urls")), + path("api/auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/auth/refresh/", TokenObtainPairView.as_view(), name="token_refresh"), ] diff --git a/server/task_planner/urls.py b/server/task_planner/urls.py index 694537e..d0e9522 100644 --- a/server/task_planner/urls.py +++ b/server/task_planner/urls.py @@ -1,6 +1,8 @@ from django.urls import path from . import views +from .views import ProtectedView +from .views import RegisterView app_name = "task_planner" urlpatterns = [ @@ -9,4 +11,6 @@ path("tasks/", views.TaskViewSet.as_view({'get': 'list', 'post': 'create'}), name="task-viewset"), path("tasks//", views.TaskViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}), name="task-detail"), path("tasks//toggle_complete/", views.TaskViewSet.as_view({'patch': 'toggle_complete'}), name="task-toggle-complete"), + path("protected/", ProtectedView.as_view()), + path('register/', RegisterView.as_view()), ] diff --git a/server/task_planner/views.py b/server/task_planner/views.py index f99686d..96b5861 100644 --- a/server/task_planner/views.py +++ b/server/task_planner/views.py @@ -7,13 +7,52 @@ from rest_framework.viewsets import ModelViewSet from rest_framework import status from rest_framework.decorators import action -#from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated +from rest_framework_simplejwt.authentication import JWTAuthentication +from django.contrib.auth.models import User from .models import Task, Topic, Time from .serializers import TaskReadSerializer, TaskWriteSerializer, TopicReadSerializer, TimeReadSerializer, TaskCompleteSerializer +class RegisterView(APIView): + def post(self, request): + username = request.data.get('username') + email = request.data.get('email') + password = request.data.get('password') -# Create your views here. + if not email or not password or not username: + return Response( + {"error": "Missing fields required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if User.objects.filter(email=email).exists(): + return Response( + {"error": "Email already exists"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if User.objects.filter(username=username).exists(): + return Response( + {"error": "Username already exists"}, + status=status.HTTP_400_BAD_REQUEST + ) + + user = User.objects.create_user(username=username,email=email, password=password) + return Response( + {"message": "User created successfully"}, + status=status.HTTP_201_CREATED + ) + +class ProtectedView(APIView): + permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + def get(self, request): + return Response({ + "user_id": request.user.id, + "username": request.user.username, + "email": request.user.email, + }) class TopicList(APIView): def get(self, request): @@ -29,8 +68,8 @@ def get(self, request): return Response(serializer.data) class TaskViewSet(ModelViewSet): - - #permission_classes = [IsAuthenticated] # Uncomment when authentication is added. + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] def get_serializer_class(self): if self.action in ['list', 'retrieve']: @@ -38,13 +77,7 @@ def get_serializer_class(self): return TaskWriteSerializer def get_queryset(self): - # -- Temporary Code -- - user_id = self.request.query_params.get('user_id') - if user_id: - return Task.objects.filter(user_id=user_id) - # -- End Temporary Code -- - - #return Task.objects.filter(user=self.request.user) # Will restrict tasks to a given user when authentication is added. + return Task.objects.filter(user=self.request.user) # Will restrict tasks to a given user when authentication is added. return Task.objects.all() def perform_create(self, serializer):