mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-09-15 00:43:21 +07:00
feat: add authentication
This commit is contained in:
+13
-2
@@ -1,8 +1,9 @@
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import { lazy } from "react";
|
||||
import { lazy, Suspense } from "react";
|
||||
import AuthLayout from "@/components/layouts/auth-layout";
|
||||
import MainLayout from "@/components/layouts/main-layout";
|
||||
|
||||
const LoginPage = lazy(() => import("@/pages/auth/login"));
|
||||
const ClusterPage = lazy(() => import("@/pages/cluster/page"));
|
||||
const HomePage = lazy(() => import("@/pages/home/page"));
|
||||
const BucketsPage = lazy(() => import("@/pages/buckets/page"));
|
||||
@@ -13,6 +14,12 @@ const router = createBrowserRouter([
|
||||
{
|
||||
path: "/auth",
|
||||
Component: AuthLayout,
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
Component: LoginPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
@@ -42,7 +49,11 @@ const router = createBrowserRouter([
|
||||
]);
|
||||
|
||||
const Router = () => {
|
||||
return <RouterProvider router={router} />;
|
||||
return (
|
||||
<Suspense>
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default Router;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
HardDrive,
|
||||
KeySquare,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Palette,
|
||||
} from "lucide-react";
|
||||
import { Dropdown, Menu } from "react-daisyui";
|
||||
@@ -12,6 +13,10 @@ import Button from "../ui/button";
|
||||
import { themes } from "@/app/themes";
|
||||
import appStore from "@/stores/app-store";
|
||||
import garageLogo from "@/assets/garage-logo.svg";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import api from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
const pages = [
|
||||
{ icon: LayoutDashboard, title: "Dashboard", path: "/", exact: true },
|
||||
@@ -22,6 +27,7 @@ const pages = [
|
||||
|
||||
const Sidebar = () => {
|
||||
const { pathname } = useLocation();
|
||||
const auth = useAuth();
|
||||
|
||||
return (
|
||||
<aside className="bg-base-100 border-r border-base-300/30 w-[80%] md:w-[250px] flex flex-col items-stretch overflow-hidden h-full">
|
||||
@@ -57,23 +63,50 @@ const Sidebar = () => {
|
||||
})}
|
||||
</Menu>
|
||||
|
||||
<Dropdown className="my-2 mx-4" vertical="top">
|
||||
<Dropdown.Toggle button={false}>
|
||||
<Button icon={Palette} color="ghost">
|
||||
Theme
|
||||
</Button>
|
||||
</Dropdown.Toggle>
|
||||
<div className="py-2 px-4 flex items-center gap-2">
|
||||
<Dropdown vertical="top">
|
||||
<Dropdown.Toggle button={false}>
|
||||
<Button icon={Palette} color="ghost">
|
||||
{!auth.isEnabled ? "Theme" : null}
|
||||
</Button>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu className="max-h-[200px] overflow-y-auto">
|
||||
{themes.map((theme) => (
|
||||
<Dropdown.Item key={theme} onClick={() => appStore.setTheme(theme)}>
|
||||
{ucfirst(theme)}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<Dropdown.Menu className="max-h-[500px] overflow-y-auto">
|
||||
{themes.map((theme) => (
|
||||
<Dropdown.Item
|
||||
key={theme}
|
||||
onClick={() => appStore.setTheme(theme)}
|
||||
>
|
||||
{ucfirst(theme)}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
{auth.isEnabled ? <LogoutButton /> : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
const LogoutButton = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const logout = useMutation({
|
||||
mutationFn: () => api.post("/auth/logout"),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err?.message || "Unknown error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Button className="flex-1" icon={LogOut} onClick={() => logout.mutate()}>
|
||||
Logout
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
|
||||
const AuthLayout = () => {
|
||||
return <div>AuthLayout</div>;
|
||||
const auth = useAuth();
|
||||
|
||||
if (auth.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (auth.isAuthenticated) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-svh flex items-center justify-center">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthLayout;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { PageContext } from "@/context/page-context";
|
||||
import { Suspense, useContext, useEffect } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import Sidebar from "../containers/sidebar";
|
||||
import { ArrowLeft, MenuIcon } from "lucide-react";
|
||||
import Button from "../ui/button";
|
||||
import { useDisclosure } from "@/hooks/useDisclosure";
|
||||
import { Drawer } from "react-daisyui";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
const MainLayout = () => {
|
||||
const sidebar = useDisclosure();
|
||||
const { pathname } = useLocation();
|
||||
const auth = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebar.isOpen) {
|
||||
@@ -17,6 +19,14 @@ const MainLayout = () => {
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
if (auth.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!auth.isAuthenticated) {
|
||||
return <Navigate to="/auth/login" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={sidebar.isOpen}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button as BaseButton } from "react-daisyui";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type ButtonProps = ComponentPropsWithoutRef<typeof BaseButton> & {
|
||||
type?: HTMLButtonElement["type"];
|
||||
href?: string;
|
||||
target?: "_blank" | "_self" | "_parent" | "_top";
|
||||
icon?: LucideIcon;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import api from "@/lib/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
type AuthResponse = {
|
||||
enabled: boolean;
|
||||
authenticated: boolean;
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["auth"],
|
||||
queryFn: () => api.get<AuthResponse>("/auth/status"),
|
||||
retry: false,
|
||||
});
|
||||
return {
|
||||
isLoading,
|
||||
isEnabled: data?.enabled,
|
||||
isAuthenticated: data?.authenticated,
|
||||
};
|
||||
};
|
||||
@@ -27,6 +27,7 @@ const api = {
|
||||
|
||||
const res = await fetch(_url, {
|
||||
...options,
|
||||
credentials: "include",
|
||||
headers: { ...headers, ...(options?.headers || {}) },
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
import { loginSchema } from "./schema";
|
||||
import api from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const useLogin = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (body: z.infer<typeof loginSchema>) => {
|
||||
return api.post("/auth/login", { body });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["auth"] });
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err?.message || "Unknown error");
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import Button from "@/components/ui/button";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Card } from "react-daisyui";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { loginSchema } from "./schema";
|
||||
import { InputField } from "@/components/ui/input";
|
||||
import { useLogin } from "./hooks";
|
||||
|
||||
export default function LoginPage() {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: { username: "", password: "" },
|
||||
});
|
||||
const login = useLogin();
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit((v) => login.mutate(v))}>
|
||||
<Card className="w-full max-w-md" bordered>
|
||||
<Card.Body>
|
||||
<Card.Title tag="h2">Login</Card.Title>
|
||||
<p className="text-base-content/60">
|
||||
Enter username and password below to log in to your account
|
||||
</p>
|
||||
|
||||
<InputField
|
||||
form={form}
|
||||
name="username"
|
||||
title="Username"
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
|
||||
<InputField
|
||||
form={form}
|
||||
name="password"
|
||||
title="Password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
|
||||
<Card.Actions className="mt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
className="w-full md:w-auto min-w-[100px]"
|
||||
loading={login.isPending}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const loginSchema = z.object({
|
||||
username: z.string().min(1, "Username is required"),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
});
|
||||
Reference in New Issue
Block a user