feat: restructure routing and authentication for hidden layout and user login

This commit is contained in:
2026-05-26 17:06:31 +02:00
parent d6e29a74af
commit ed9fb0d1ce
16 changed files with 284 additions and 127 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ dotenv.config();
const router = express.Router(); const router = express.Router();
router.post("/verify-token", authenticate, async (req, res) => { router.post("/verify-token", authenticate, async (req, res) => {
res.status(200); res.sendStatus(200);
}); });
router.post("/login", async (req, res) => { router.post("/login", async (req, res) => {
-7
View File
@@ -1,7 +0,0 @@
export const LandingPage = () => {
return (
<>
<p>Landing Page</p>
</>
);
};
+70
View File
@@ -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 (
<>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field name="username">
{(field) => (
<Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
placeholder={t("username")}
/>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<Input
value={field.state.value}
type="password"
onChange={(e) => field.handleChange(e.target.value)}
placeholder={t("password")}
/>
)}
</form.Field>
<Button type="submit" loading={isPending}>
{t("login")}
</Button>
</form>
</>
);
};
+12 -6
View File
@@ -1,10 +1,16 @@
import { StrictMode } from 'react' import { StrictMode } from "react";
import { createRoot } from 'react-dom/client' import { createRoot } from "react-dom/client";
import './index.css' import "./index.css";
import App from './App.tsx' 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(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}>
<App /> <App />
</QueryClientProvider>
</StrictMode>, </StrictMode>,
) );
+83 -49
View File
@@ -11,9 +11,10 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login' import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AppViewProductRouteImport } from './routes/app/view-product' import { Route as AppHiddenLayoutRouteImport } from './routes/app/_hiddenLayout'
import { Route as AppInventoryRouteImport } from './routes/app/inventory' import { Route as AppHiddenLayoutViewProductRouteImport } from './routes/app/_hiddenLayout/view-product'
import { Route as AppAddProductRouteImport } from './routes/app/add-product' import { Route as AppHiddenLayoutInventoryRouteImport } from './routes/app/_hiddenLayout/inventory'
import { Route as AppHiddenLayoutAddProductRouteImport } from './routes/app/_hiddenLayout/add-product'
const LoginRoute = LoginRouteImport.update({ const LoginRoute = LoginRouteImport.update({
id: '/login', id: '/login',
@@ -25,49 +26,61 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AppViewProductRoute = AppViewProductRouteImport.update({ const AppHiddenLayoutRoute = AppHiddenLayoutRouteImport.update({
id: '/app/view-product', id: '/app/_hiddenLayout',
path: '/app/view-product', path: '/app',
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',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } 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 { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/app/add-product': typeof AppAddProductRoute '/app': typeof AppHiddenLayoutRouteWithChildren
'/app/inventory': typeof AppInventoryRoute '/app/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/view-product': typeof AppViewProductRoute '/app/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/app/add-product': typeof AppAddProductRoute '/app': typeof AppHiddenLayoutRouteWithChildren
'/app/inventory': typeof AppInventoryRoute '/app/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/view-product': typeof AppViewProductRoute '/app/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/view-product': typeof AppHiddenLayoutViewProductRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/app/add-product': typeof AppAddProductRoute '/app/_hiddenLayout': typeof AppHiddenLayoutRouteWithChildren
'/app/inventory': typeof AppInventoryRoute '/app/_hiddenLayout/add-product': typeof AppHiddenLayoutAddProductRoute
'/app/view-product': typeof AppViewProductRoute '/app/_hiddenLayout/inventory': typeof AppHiddenLayoutInventoryRoute
'/app/_hiddenLayout/view-product': typeof AppHiddenLayoutViewProductRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '/' | '/'
| '/login' | '/login'
| '/app'
| '/app/add-product' | '/app/add-product'
| '/app/inventory' | '/app/inventory'
| '/app/view-product' | '/app/view-product'
@@ -75,6 +88,7 @@ export interface FileRouteTypes {
to: to:
| '/' | '/'
| '/login' | '/login'
| '/app'
| '/app/add-product' | '/app/add-product'
| '/app/inventory' | '/app/inventory'
| '/app/view-product' | '/app/view-product'
@@ -82,17 +96,16 @@ export interface FileRouteTypes {
| '__root__' | '__root__'
| '/' | '/'
| '/login' | '/login'
| '/app/add-product' | '/app/_hiddenLayout'
| '/app/inventory' | '/app/_hiddenLayout/add-product'
| '/app/view-product' | '/app/_hiddenLayout/inventory'
| '/app/_hiddenLayout/view-product'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
AppAddProductRoute: typeof AppAddProductRoute AppHiddenLayoutRoute: typeof AppHiddenLayoutRouteWithChildren
AppInventoryRoute: typeof AppInventoryRoute
AppViewProductRoute: typeof AppViewProductRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@@ -111,36 +124,57 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/app/view-product': { '/app/_hiddenLayout': {
id: '/app/view-product' id: '/app/_hiddenLayout'
path: '/app/view-product' 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' fullPath: '/app/view-product'
preLoaderRoute: typeof AppViewProductRouteImport preLoaderRoute: typeof AppHiddenLayoutViewProductRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof AppHiddenLayoutRoute
} }
'/app/inventory': { '/app/_hiddenLayout/inventory': {
id: '/app/inventory' id: '/app/_hiddenLayout/inventory'
path: '/app/inventory' path: '/inventory'
fullPath: '/app/inventory' fullPath: '/app/inventory'
preLoaderRoute: typeof AppInventoryRouteImport preLoaderRoute: typeof AppHiddenLayoutInventoryRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof AppHiddenLayoutRoute
} }
'/app/add-product': { '/app/_hiddenLayout/add-product': {
id: '/app/add-product' id: '/app/_hiddenLayout/add-product'
path: '/app/add-product' path: '/add-product'
fullPath: '/app/add-product' fullPath: '/app/add-product'
preLoaderRoute: typeof AppAddProductRouteImport preLoaderRoute: typeof AppHiddenLayoutAddProductRouteImport
parentRoute: typeof rootRouteImport 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 = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
AppAddProductRoute: AppAddProductRoute, AppHiddenLayoutRoute: AppHiddenLayoutRouteWithChildren,
AppInventoryRoute: AppInventoryRoute,
AppViewProductRoute: AppViewProductRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
+15
View File
@@ -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 (
<div>
<h1>Layout</h1>
<Outlet />
</div>
);
}
@@ -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 <div>Hello "/app/add-product"!</div>;
}
@@ -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 (
<>
<p>Inventar</p>
</>
);
}
@@ -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 <div>Hello "/app/view-product"!</div>;
}
-9
View File
@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/add-product')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/app/add-product"!</div>
}
-17
View File
@@ -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 <div>Hello "/app/inventory"!</div>;
}
-9
View File
@@ -1,9 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/view-product')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/app/view-product"!</div>
}
+9 -4
View File
@@ -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, component: RouteComponent,
}) });
function RouteComponent() { function RouteComponent() {
return <div>Hello "/login"!</div> return (
<>
<LoginCard />
</>
);
} }
+14 -12
View File
@@ -1,12 +1,10 @@
import { API_BASE } from "../config/api.config"; import { API_BASE } from "../config/api.config";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { redirect } from "@tanstack/react-router";
const { t } = useTranslation();
export async function isAuthenticated() { export async function isAuthenticated() {
if (Cookies.get("token")) {
const result = await fetch(`${API_BASE}/users/verify-token`, { const result = await fetch(`${API_BASE}/users/verify-token`, {
method: "POST", method: "POST",
headers: { headers: {
@@ -19,11 +17,17 @@ export async function isAuthenticated() {
if (result.status === 200) { if (result.status === 200) {
return true; return true;
} }
}
Cookies.remove("token");
return false; 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`, { const result = await fetch(`${API_BASE}/users/login`, {
method: "POST", method: "POST",
headers: { headers: {
@@ -35,21 +39,19 @@ export async function signInUser(username: string, password: string) {
}); });
const response = await result.json(); const response = await result.json();
console.log(response);
if (result.status === 202) { if (result.status === 202) {
Cookies.set("token", response.token); Cookies.set("token", response.data.token);
return true; return { ok: true as const };
} }
if (result.status !== 202) {
Cookies.remove("token"); Cookies.remove("token");
toast.error(t(response.code)); toast.error(t(response.code));
} return { ok: false as const };
} }
export function signOutUser() { export function signOutUser() {
Cookies.remove("token"); Cookies.remove("token");
throw redirect({ return { ok: true as const };
to: "/login",
});
} }
@@ -0,0 +1,6 @@
{
"username": "Benutzername",
"password": "Passwort",
"eu001": "Falscher Benutzername oder Passwort!",
"login": "Login"
}
@@ -0,0 +1,6 @@
{
"username": "Username",
"password": "Password",
"eu001": "Wrong username or password!",
"login": "Login"
}