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"],
)