From ed9fb0d1ced2d650c2d02ff46f79fd078acfa437 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 26 May 2026 17:06:31 +0200 Subject: [PATCH] feat: restructure routing and authentication for hidden layout and user login --- backend/routes/app/users.route.js | 2 +- frontend/src/components/LandingPage.tsx | 7 - frontend/src/components/LoginCard.tsx | 70 ++++++++++ frontend/src/main.tsx | 20 ++- frontend/src/routeTree.gen.ts | 132 +++++++++++------- frontend/src/routes/app/_hiddenLayout.tsx | 15 ++ .../routes/app/_hiddenLayout/add-product.tsx | 17 +++ .../routes/app/_hiddenLayout/inventory.tsx | 21 +++ .../routes/app/_hiddenLayout/view-product.tsx | 17 +++ frontend/src/routes/app/add-product.tsx | 9 -- frontend/src/routes/app/inventory.tsx | 17 --- frontend/src/routes/app/view-product.tsx | 9 -- frontend/src/routes/login.tsx | 13 +- frontend/src/utils/auth.ts | 50 +++---- frontend/src/utils/i18n/locales/de/de.json | 6 + frontend/src/utils/i18n/locales/en/en.json | 6 + 16 files changed, 284 insertions(+), 127 deletions(-) delete mode 100644 frontend/src/components/LandingPage.tsx create mode 100644 frontend/src/components/LoginCard.tsx create mode 100644 frontend/src/routes/app/_hiddenLayout.tsx create mode 100644 frontend/src/routes/app/_hiddenLayout/add-product.tsx create mode 100644 frontend/src/routes/app/_hiddenLayout/inventory.tsx create mode 100644 frontend/src/routes/app/_hiddenLayout/view-product.tsx delete mode 100644 frontend/src/routes/app/add-product.tsx delete mode 100644 frontend/src/routes/app/inventory.tsx delete mode 100644 frontend/src/routes/app/view-product.tsx diff --git a/backend/routes/app/users.route.js b/backend/routes/app/users.route.js index c85f339..019f85b 100644 --- a/backend/routes/app/users.route.js +++ b/backend/routes/app/users.route.js @@ -6,7 +6,7 @@ dotenv.config(); const router = express.Router(); router.post("/verify-token", authenticate, async (req, res) => { - res.status(200); + res.sendStatus(200); }); router.post("/login", async (req, res) => { diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx deleted file mode 100644 index a8155b9..0000000 --- a/frontend/src/components/LandingPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const LandingPage = () => { - return ( - <> -

Landing Page

- - ); -}; diff --git a/frontend/src/components/LoginCard.tsx b/frontend/src/components/LoginCard.tsx new file mode 100644 index 0000000..9e79513 --- /dev/null +++ b/frontend/src/components/LoginCard.tsx @@ -0,0 +1,70 @@ +import { useForm } from "@tanstack/react-form"; +import { Input, Button } from "@mui/joy"; +import { useMutation } from "@tanstack/react-query"; +import { signInUser } from "../utils/auth"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "@tanstack/react-router"; + +export const LoginCard = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const form = useForm({ + defaultValues: { + username: "", + password: "", + }, + onSubmit: async ({ value }) => { + mutate({ username: value.username, password: value.password }); + }, + }); + + const { mutate, isPending } = useMutation({ + mutationFn: ({ + username, + password, + }: { + username: string; + password: string; + }) => signInUser(username, password, t), + onSuccess: (result) => { + if (result.ok) { + navigate({ to: "/app/inventory" }); + } + }, + }); + + return ( + <> +
{ + e.preventDefault(); + form.handleSubmit(); + }} + > + + {(field) => ( + field.handleChange(e.target.value)} + placeholder={t("username")} + /> + )} + + + {(field) => ( + field.handleChange(e.target.value)} + placeholder={t("password")} + /> + )} + + +
+ + ); +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..820ab0f 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,16 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import "./utils/i18n"; +import App from "./App.tsx"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -createRoot(document.getElementById('root')!).render( +const queryClient = new QueryClient(); + +createRoot(document.getElementById("root")!).render( - + + + , -) +); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index dfe1a17..716996f 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -11,9 +11,10 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as LoginRouteImport } from './routes/login' import { Route as IndexRouteImport } from './routes/index' -import { Route as AppViewProductRouteImport } from './routes/app/view-product' -import { Route as AppInventoryRouteImport } from './routes/app/inventory' -import { Route as AppAddProductRouteImport } from './routes/app/add-product' +import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout' +import { Route as AppHiddenLayoutViewProductRouteImport } from './routes/app/_hiddenLayout/view-product' +import { Route as AppHiddenLayoutInventoryRouteImport } from './routes/app/_hiddenLayout/inventory' +import { Route as AppHiddenLayoutAddProductRouteImport } from './routes/app/_hiddenLayout/add-product' const LoginRoute = LoginRouteImport.update({ id: '/login', @@ -25,49 +26,61 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) -const AppViewProductRoute = AppViewProductRouteImport.update({ - id: '/app/view-product', - path: '/app/view-product', - getParentRoute: () => rootRouteImport, -} as any) -const AppInventoryRoute = AppInventoryRouteImport.update({ - id: '/app/inventory', - path: '/app/inventory', - getParentRoute: () => rootRouteImport, -} as any) -const AppAddProductRoute = AppAddProductRouteImport.update({ - id: '/app/add-product', - path: '/app/add-product', +const AppHiddenLayoutRoute = AppHiddenLayoutRouteImport.update({ + id: '/app/_hiddenLayout', + path: '/app', getParentRoute: () => rootRouteImport, } as any) +const AppHiddenLayoutViewProductRoute = + AppHiddenLayoutViewProductRouteImport.update({ + id: '/view-product', + path: '/view-product', + getParentRoute: () => AppHiddenLayoutRoute, + } as any) +const AppHiddenLayoutInventoryRoute = + AppHiddenLayoutInventoryRouteImport.update({ + id: '/inventory', + path: '/inventory', + getParentRoute: () => AppHiddenLayoutRoute, + } as any) +const AppHiddenLayoutAddProductRoute = + AppHiddenLayoutAddProductRouteImport.update({ + id: '/add-product', + path: '/add-product', + getParentRoute: () => AppHiddenLayoutRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute - '/app/add-product': typeof AppAddProductRoute - '/app/inventory': typeof AppInventoryRoute - '/app/view-product': typeof AppViewProductRoute + '/app': typeof AppHiddenLayoutRouteWithChildren + '/app/add-product': typeof AppHiddenLayoutAddProductRoute + '/app/inventory': typeof AppHiddenLayoutInventoryRoute + '/app/view-product': typeof AppHiddenLayoutViewProductRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute - '/app/add-product': typeof AppAddProductRoute - '/app/inventory': typeof AppInventoryRoute - '/app/view-product': typeof AppViewProductRoute + '/app': typeof AppHiddenLayoutRouteWithChildren + '/app/add-product': typeof AppHiddenLayoutAddProductRoute + '/app/inventory': typeof AppHiddenLayoutInventoryRoute + '/app/view-product': typeof AppHiddenLayoutViewProductRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/login': typeof LoginRoute - '/app/add-product': typeof AppAddProductRoute - '/app/inventory': typeof AppInventoryRoute - '/app/view-product': typeof AppViewProductRoute + '/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren + '/app/_hiddenLayout/add-product': typeof AppHiddenLayoutAddProductRoute + '/app/_hiddenLayout/inventory': typeof AppHiddenLayoutInventoryRoute + '/app/_hiddenLayout/view-product': typeof AppHiddenLayoutViewProductRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' | '/login' + | '/app' | '/app/add-product' | '/app/inventory' | '/app/view-product' @@ -75,6 +88,7 @@ export interface FileRouteTypes { to: | '/' | '/login' + | '/app' | '/app/add-product' | '/app/inventory' | '/app/view-product' @@ -82,17 +96,16 @@ export interface FileRouteTypes { | '__root__' | '/' | '/login' - | '/app/add-product' - | '/app/inventory' - | '/app/view-product' + | '/app/_hiddenLayout' + | '/app/_hiddenLayout/add-product' + | '/app/_hiddenLayout/inventory' + | '/app/_hiddenLayout/view-product' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute LoginRoute: typeof LoginRoute - AppAddProductRoute: typeof AppAddProductRoute - AppInventoryRoute: typeof AppInventoryRoute - AppViewProductRoute: typeof AppViewProductRoute + AppHiddenLayoutRoute: typeof AppHiddenLayoutRouteWithChildren } declare module '@tanstack/react-router' { @@ -111,36 +124,57 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/app/view-product': { - id: '/app/view-product' - path: '/app/view-product' + '/app/_hiddenLayout': { + id: '/app/_hiddenLayout' + path: '/app' + fullPath: '/app' + preLoaderRoute: typeof AppHiddenLayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/app/_hiddenLayout/view-product': { + id: '/app/_hiddenLayout/view-product' + path: '/view-product' fullPath: '/app/view-product' - preLoaderRoute: typeof AppViewProductRouteImport - parentRoute: typeof rootRouteImport + preLoaderRoute: typeof AppHiddenLayoutViewProductRouteImport + parentRoute: typeof AppHiddenLayoutRoute } - '/app/inventory': { - id: '/app/inventory' - path: '/app/inventory' + '/app/_hiddenLayout/inventory': { + id: '/app/_hiddenLayout/inventory' + path: '/inventory' fullPath: '/app/inventory' - preLoaderRoute: typeof AppInventoryRouteImport - parentRoute: typeof rootRouteImport + preLoaderRoute: typeof AppHiddenLayoutInventoryRouteImport + parentRoute: typeof AppHiddenLayoutRoute } - '/app/add-product': { - id: '/app/add-product' - path: '/app/add-product' + '/app/_hiddenLayout/add-product': { + id: '/app/_hiddenLayout/add-product' + path: '/add-product' fullPath: '/app/add-product' - preLoaderRoute: typeof AppAddProductRouteImport - parentRoute: typeof rootRouteImport + preLoaderRoute: typeof AppHiddenLayoutAddProductRouteImport + parentRoute: typeof AppHiddenLayoutRoute } } } +interface AppHiddenLayoutRouteChildren { + AppHiddenLayoutAddProductRoute: typeof AppHiddenLayoutAddProductRoute + AppHiddenLayoutInventoryRoute: typeof AppHiddenLayoutInventoryRoute + AppHiddenLayoutViewProductRoute: typeof AppHiddenLayoutViewProductRoute +} + +const AppHiddenLayoutRouteChildren: AppHiddenLayoutRouteChildren = { + AppHiddenLayoutAddProductRoute: AppHiddenLayoutAddProductRoute, + AppHiddenLayoutInventoryRoute: AppHiddenLayoutInventoryRoute, + AppHiddenLayoutViewProductRoute: AppHiddenLayoutViewProductRoute, +} + +const AppHiddenLayoutRouteWithChildren = AppHiddenLayoutRoute._addFileChildren( + AppHiddenLayoutRouteChildren, +) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LoginRoute: LoginRoute, - AppAddProductRoute: AppAddProductRoute, - AppInventoryRoute: AppInventoryRoute, - AppViewProductRoute: AppViewProductRoute, + AppHiddenLayoutRoute: AppHiddenLayoutRouteWithChildren, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/frontend/src/routes/app/_hiddenLayout.tsx b/frontend/src/routes/app/_hiddenLayout.tsx new file mode 100644 index 0000000..636a903 --- /dev/null +++ b/frontend/src/routes/app/_hiddenLayout.tsx @@ -0,0 +1,15 @@ +// routes/app/_layout.tsx (oder app.tsx als Parent) +import { Outlet, createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/app/_hiddenLayout")({ + component: AppLayout, +}); + +function AppLayout() { + return ( +
+

Layout

+ +
+ ); +} diff --git a/frontend/src/routes/app/_hiddenLayout/add-product.tsx b/frontend/src/routes/app/_hiddenLayout/add-product.tsx new file mode 100644 index 0000000..f2307b9 --- /dev/null +++ b/frontend/src/routes/app/_hiddenLayout/add-product.tsx @@ -0,0 +1,17 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { isAuthenticated } from "../../../utils/auth"; + +export const Route = createFileRoute("/app/_hiddenLayout/add-product")({ + beforeLoad: async () => { + if (!(await isAuthenticated())) { + throw redirect({ + to: "/login", + }); + } + }, + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/app/add-product"!
; +} diff --git a/frontend/src/routes/app/_hiddenLayout/inventory.tsx b/frontend/src/routes/app/_hiddenLayout/inventory.tsx new file mode 100644 index 0000000..afeab4b --- /dev/null +++ b/frontend/src/routes/app/_hiddenLayout/inventory.tsx @@ -0,0 +1,21 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { isAuthenticated } from "../../../utils/auth"; + +export const Route = createFileRoute("/app/_hiddenLayout/inventory")({ + beforeLoad: async () => { + if (!(await isAuthenticated())) { + throw redirect({ + to: "/login", + }); + } + }, + component: RouteComponent, +}); + +function RouteComponent() { + return ( + <> +

Inventar

+ + ); +} diff --git a/frontend/src/routes/app/_hiddenLayout/view-product.tsx b/frontend/src/routes/app/_hiddenLayout/view-product.tsx new file mode 100644 index 0000000..2cbef25 --- /dev/null +++ b/frontend/src/routes/app/_hiddenLayout/view-product.tsx @@ -0,0 +1,17 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { isAuthenticated } from "../../../utils/auth"; + +export const Route = createFileRoute("/app/_hiddenLayout/view-product")({ + beforeLoad: async () => { + if (!(await isAuthenticated())) { + throw redirect({ + to: "/login", + }); + } + }, + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/app/view-product"!
; +} diff --git a/frontend/src/routes/app/add-product.tsx b/frontend/src/routes/app/add-product.tsx deleted file mode 100644 index c218e62..0000000 --- a/frontend/src/routes/app/add-product.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/app/add-product')({ - component: RouteComponent, -}) - -function RouteComponent() { - return
Hello "/app/add-product"!
-} diff --git a/frontend/src/routes/app/inventory.tsx b/frontend/src/routes/app/inventory.tsx deleted file mode 100644 index da772c9..0000000 --- a/frontend/src/routes/app/inventory.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createFileRoute, redirect } from "@tanstack/react-router"; -import { isAuthenticated } from "../../utils/auth"; - -export const Route = createFileRoute("/app/inventory")({ - beforeLoad: () => { - if (!isAuthenticated()) { - throw redirect({ - to: "/login", - }); - } - }, - component: RouteComponent, -}); - -function RouteComponent() { - return
Hello "/app/inventory"!
; -} diff --git a/frontend/src/routes/app/view-product.tsx b/frontend/src/routes/app/view-product.tsx deleted file mode 100644 index 1bc3a33..0000000 --- a/frontend/src/routes/app/view-product.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/app/view-product')({ - component: RouteComponent, -}) - -function RouteComponent() { - return
Hello "/app/view-product"!
-} diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index f7b477e..9940f41 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -1,9 +1,14 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute } from "@tanstack/react-router"; +import { LoginCard } from "../components/LoginCard"; -export const Route = createFileRoute('/login')({ +export const Route = createFileRoute("/login")({ component: RouteComponent, -}) +}); function RouteComponent() { - return
Hello "/login"!
+ return ( + <> + + + ); } diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts index b329cd1..6ed64c5 100644 --- a/frontend/src/utils/auth.ts +++ b/frontend/src/utils/auth.ts @@ -1,29 +1,33 @@ import { API_BASE } from "../config/api.config"; import Cookies from "js-cookie"; -import { useTranslation } from "react-i18next"; +import type { TFunction } from "i18next"; import { toast } from "react-toastify"; -import { redirect } from "@tanstack/react-router"; - -const { t } = useTranslation(); export async function isAuthenticated() { - const result = await fetch(`${API_BASE}/users/verify-token`, { - method: "POST", - headers: { - Authorization: `Bearer ${Cookies.get("token") || ""}`, - "Content-Type": "application/json", - Accept: "application/json", - }, - }); + if (Cookies.get("token")) { + const result = await fetch(`${API_BASE}/users/verify-token`, { + method: "POST", + headers: { + Authorization: `Bearer ${Cookies.get("token") || ""}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); - if (result.status === 200) { - return true; + if (result.status === 200) { + return true; + } } + Cookies.remove("token"); return false; } -export async function signInUser(username: string, password: string) { +export async function signInUser( + username: string, + password: string, + t: TFunction, +) { const result = await fetch(`${API_BASE}/users/login`, { method: "POST", headers: { @@ -35,21 +39,19 @@ export async function signInUser(username: string, password: string) { }); const response = await result.json(); + console.log(response); if (result.status === 202) { - Cookies.set("token", response.token); - return true; + Cookies.set("token", response.data.token); + return { ok: true as const }; } - if (result.status !== 202) { - Cookies.remove("token"); - toast.error(t(response.code)); - } + Cookies.remove("token"); + toast.error(t(response.code)); + return { ok: false as const }; } export function signOutUser() { Cookies.remove("token"); - throw redirect({ - to: "/login", - }); + return { ok: true as const }; } diff --git a/frontend/src/utils/i18n/locales/de/de.json b/frontend/src/utils/i18n/locales/de/de.json index e69de29..6876475 100644 --- a/frontend/src/utils/i18n/locales/de/de.json +++ b/frontend/src/utils/i18n/locales/de/de.json @@ -0,0 +1,6 @@ +{ + "username": "Benutzername", + "password": "Passwort", + "eu001": "Falscher Benutzername oder Passwort!", + "login": "Login" +} \ No newline at end of file diff --git a/frontend/src/utils/i18n/locales/en/en.json b/frontend/src/utils/i18n/locales/en/en.json index e69de29..4ea442b 100644 --- a/frontend/src/utils/i18n/locales/en/en.json +++ b/frontend/src/utils/i18n/locales/en/en.json @@ -0,0 +1,6 @@ +{ + "username": "Username", + "password": "Password", + "eu001": "Wrong username or password!", + "login": "Login" +} \ No newline at end of file