From 10f002b2c949ddb9cec7f388422b7cced8890e8f Mon Sep 17 00:00:00 2001 From: m-danya Date: Sat, 2 Nov 2024 19:13:03 +0300 Subject: [PATCH] Add login and registration page to the frontend --- frontend/app/login/page.tsx | 9 ++ frontend/app/page.tsx | 23 ++-- frontend/components/login-form.tsx | 139 +++++++++++++++++++++ frontend/components/nav-sections.tsx | 5 +- frontend/components/ui/card.tsx | 76 +++++++++++ frontend/components/ui/label.tsx | 26 ++++ frontend/hooks/fetcher.tsx | 4 +- frontend/hooks/use-user.tsx | 17 +++ frontend/package-lock.json | 23 ++++ frontend/package.json | 1 + planty/application/router.py | 6 - planty/application/tests/test_endpoints.py | 2 +- planty/main.py | 12 +- 13 files changed, 322 insertions(+), 21 deletions(-) create mode 100644 frontend/app/login/page.tsx create mode 100644 frontend/components/login-form.tsx create mode 100644 frontend/components/ui/card.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/hooks/use-user.tsx diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx new file mode 100644 index 0000000..d33d29e --- /dev/null +++ b/frontend/app/login/page.tsx @@ -0,0 +1,9 @@ +import { LoginForm } from "@/components/login-form" + +export default function Page() { + return ( +
+ +
+ ) +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index bd7ef96..14b44c8 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,20 +1,27 @@ +"use client"; import { AppSidebar } from "@/components/app-sidebar"; -import { NavActions } from "@/components/nav-actions"; import { TaskList } from "@/components/tasks/task-list"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, -} from "@/components/ui/breadcrumb"; -import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar"; +import useUser from "@/hooks/use-user"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; export default function Page() { + const router = useRouter(); + const { user, loading, loggedIn, mutate } = useUser(); + + useEffect(() => { + console.log(user, loading, loggedIn, mutate); + if (!loggedIn) { + router.push("/login"); + } + }, [loggedIn]); + if (!loggedIn) return ""; + return ( diff --git a/frontend/components/login-form.tsx b/frontend/components/login-form.tsx new file mode 100644 index 0000000..d111550 --- /dev/null +++ b/frontend/components/login-form.tsx @@ -0,0 +1,139 @@ +"use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import useUser from "@/hooks/use-user"; + +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export function LoginForm() { + const router = useRouter(); + const { mutate } = useUser(); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const handleLogin = async () => { + try { + const body = new URLSearchParams(); + body.append("username", email || ""); + body.append("password", password || ""); + + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + credentials: "include", + body: body.toString(), + }); + + if (!response.ok) { + const errorData = await response.json(); + alert("Login failed: " + errorData.detail); + return; + } + + await mutate(); + + if (typeof window !== "undefined" && window.history.length > 1) { + router.back(); + } else { + router.push("/"); + } + } catch (error) { + console.error("An unexpected error occurred:", error); + alert("An unexpected error occurred"); + } + }; + + const handleRegister = async () => { + try { + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify({ + email, + password, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + alert("Registration failed: " + errorData.detail); + return; + } + + await mutate(); + + handleLogin(); + } catch (error) { + console.error("An unexpected error occurred:", error); + alert("An unexpected error occurred"); + } + }; + + return ( + + + Login or Sign Up + + +
+
+ + setEmail(e.target.value)} + /> +
+
+
+ + {/* + Forgot your password? + */} +
+ setPassword(e.target.value)} + /> +
+ + +
+ {/*
+ Don't have an account?{" "} + + Sign up + +
*/} +
+
+ ); +} diff --git a/frontend/components/nav-sections.tsx b/frontend/components/nav-sections.tsx index ce079a8..b1acf41 100644 --- a/frontend/components/nav-sections.tsx +++ b/frontend/components/nav-sections.tsx @@ -19,6 +19,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import React from "react"; import { useSections } from "@/hooks/use-sections"; +import Link from "next/link"; export function NavSections() { const { sections, rootSectionId, isLoading, isError } = useSections(); @@ -123,7 +124,7 @@ const TreeElement = React.forwardRef< return mainContent; } else { return ( - {mainContent} - + ); } } diff --git a/frontend/components/ui/card.tsx b/frontend/components/ui/card.tsx new file mode 100644 index 0000000..77e9fb7 --- /dev/null +++ b/frontend/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/frontend/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/frontend/hooks/fetcher.tsx b/frontend/hooks/fetcher.tsx index c1fe520..24ae146 100644 --- a/frontend/hooks/fetcher.tsx +++ b/frontend/hooks/fetcher.tsx @@ -1,7 +1,9 @@ export const fetcher = async (url: string) => { const response = await fetch(url); if (!response.ok) { - throw new Error(`Error fetching ${url}: ${response.statusText}`); + const error = new Error(`Error fetching ${url}: ${response.statusText}`); + (error as any).status = response.status; + throw error; } return response.json(); }; diff --git a/frontend/hooks/use-user.tsx b/frontend/hooks/use-user.tsx new file mode 100644 index 0000000..dd0599a --- /dev/null +++ b/frontend/hooks/use-user.tsx @@ -0,0 +1,17 @@ +import useSWR from "swr"; +import { fetcher } from "@/hooks/fetcher"; + +export default function useUser() { + const { data, mutate, error } = useSWR("/api/auth/me", fetcher); + + const loading = !data && !error; + console.log(error); + const loggedIn = !(error && error.status === 401); + + return { + loading, + loggedIn, + user: data, + mutate, + }; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 43cfa40..a065fa5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -828,6 +829,28 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", + "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9385d32..a27c302 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", diff --git a/planty/application/router.py b/planty/application/router.py index 36565d9..bd81ece 100644 --- a/planty/application/router.py +++ b/planty/application/router.py @@ -51,12 +51,6 @@ async def create_task( return TaskCreateResponse(id=task_id) -# TODO: move to auth router -@router.get("/me") -async def test_auth(user: User = Depends(current_user)) -> User: - return user - - # TODO: use query params for DELETE, body must be empty! @router.delete("/task") async def remove_task( diff --git a/planty/application/tests/test_endpoints.py b/planty/application/tests/test_endpoints.py index cda0fb5..fbf7274 100644 --- a/planty/application/tests/test_endpoints.py +++ b/planty/application/tests/test_endpoints.py @@ -521,5 +521,5 @@ async def test_root_section_is_created(ac: AsyncClient) -> None: "email": "new_user@example.com", "password": "string", } - response = await ac.post("/auth/register", json=user_data) + response = await ac.post("/api/auth/register", json=user_data) assert response.is_success diff --git a/planty/main.py b/planty/main.py index 1a03b2f..ec260a6 100644 --- a/planty/main.py +++ b/planty/main.py @@ -6,7 +6,7 @@ fastapi_users_obj, cookie_auth_backend, ) -from planty.application.schemas import UserCreate, UserRead +from planty.application.schemas import UserCreate, UserRead, UserUpdate app = FastAPI( title="Planty", @@ -17,13 +17,19 @@ app.include_router( fastapi_users_obj.get_auth_router(cookie_auth_backend), - prefix="/auth/db", + prefix="/api/auth", tags=["auth"], ) app.include_router( fastapi_users_obj.get_register_router(UserRead, UserCreate), - prefix="/auth", + prefix="/api/auth", + tags=["auth"], +) + +app.include_router( + fastapi_users_obj.get_users_router(UserRead, UserUpdate), + prefix="/api/auth", tags=["auth"], )