mirror of
				https://github.com/khairul169/garage-webui.git
				synced 2025-10-29 22:29:32 +07:00 
			
		
		
		
	Compare commits
	
		
			No commits in common. "8c3458c27f7b16772213a0d7b28639fdff210ecc" and "b53859ae234525f9ca1c22f7f1252ea6672f6993" have entirely different histories.
		
	
	
		
			8c3458c27f
			...
			b53859ae23
		
	
		
| @ -1,7 +1,9 @@ | ||||
| FROM node:20-slim AS frontend | ||||
| WORKDIR /app | ||||
| 
 | ||||
| RUN npm install -g corepack@latest && corepack use pnpm@latest | ||||
| ENV PNPM_HOME="/pnpm" | ||||
| ENV PATH="$PNPM_HOME:$PATH" | ||||
| RUN corepack enable | ||||
| 
 | ||||
| COPY package.json pnpm-lock.yaml ./ | ||||
| RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile | ||||
| @ -9,7 +11,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile | ||||
| COPY . . | ||||
| RUN pnpm run build | ||||
| 
 | ||||
| FROM golang:1.23 AS backend | ||||
| FROM golang:1.22.5 AS backend | ||||
| WORKDIR /app | ||||
| 
 | ||||
| COPY backend/go.mod backend/go.sum ./ | ||||
|  | ||||
							
								
								
									
										21
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
									
									
									
									
								
							| @ -62,7 +62,7 @@ services: | ||||
| Get the latest binary from the [release page](https://github.com/khairul169/garage-webui/releases/latest) according to your OS architecture. For example: | ||||
| 
 | ||||
| ```sh | ||||
| $ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.7/garage-webui-v1.0.7-linux-amd64 | ||||
| $ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.6/garage-webui-v1.0.6-linux-amd64 | ||||
| $ chmod +x garage-webui | ||||
| $ sudo cp garage-webui /usr/local/bin | ||||
| ``` | ||||
| @ -144,25 +144,6 @@ However, if it fails to load, you can set these environment variables instead: | ||||
| - `S3_REGION`: S3 Region. | ||||
| - `S3_ENDPOINT_URL`: S3 Endpoint url. | ||||
| 
 | ||||
| ### Authentication | ||||
| 
 | ||||
| Enable authentication by setting `AUTH_USER_PASS` environment variable. Generate the username and password hash using the following command: | ||||
| 
 | ||||
| ```bash | ||||
| htpasswd -nbBC 10 "YOUR_USERNAME" "YOUR_PASSWORD" | ||||
| ``` | ||||
| 
 | ||||
| > If command 'htpasswd' is not found, install `apache2-utils` using your package manager. | ||||
| 
 | ||||
| Then update your `docker-compose.yml`: | ||||
| 
 | ||||
| ```yml | ||||
| webui: | ||||
|   .... | ||||
|   environment: | ||||
|     AUTH_USER_PASS: "username:$2y$10$DSTi9o..." | ||||
| ``` | ||||
| 
 | ||||
| ### Running | ||||
| 
 | ||||
| Once your instance of Garage Web UI is started, you can open the web UI at http://your-ip:3909. You can place it behind a reverse proxy to secure it with SSL. | ||||
|  | ||||
| @ -1,8 +1,6 @@ | ||||
| module khairul169/garage-webui | ||||
| 
 | ||||
| go 1.23.0 | ||||
| 
 | ||||
| toolchain go1.24.0 | ||||
| go 1.22.5 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/aws/aws-sdk-go-v2 v1.30.4 | ||||
| @ -15,7 +13,6 @@ require ( | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/alexedwards/scs/v2 v2.8.0 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect | ||||
| @ -24,5 +21,4 @@ require ( | ||||
| 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect | ||||
| 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect | ||||
| 	golang.org/x/crypto v0.35.0 | ||||
| ) | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= | ||||
| github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= | ||||
| github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8= | ||||
| github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= | ||||
| github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= | ||||
| @ -44,8 +42,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO | ||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||
| github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||||
| github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= | ||||
| golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
|  | ||||
| @ -12,18 +12,15 @@ import ( | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
| 	// Initialize app | ||||
| 	godotenv.Load() | ||||
| 	utils.InitCacheManager() | ||||
| 	sessionMgr := utils.InitSessionManager() | ||||
| 
 | ||||
| 	if err := utils.Garage.LoadConfig(); err != nil { | ||||
| 		log.Println("Cannot load garage config!", err) | ||||
| 	} | ||||
| 
 | ||||
| 	mux := http.NewServeMux() | ||||
| 	mux.Handle("/api/", http.StripPrefix("/api", router.HandleApiRouter())) | ||||
| 	ui.ServeUI(mux) | ||||
| 	http.Handle("/api/", http.StripPrefix("/api", router.HandleApiRouter())) | ||||
| 	ui.ServeUI() | ||||
| 
 | ||||
| 	host := utils.GetEnv("HOST", "0.0.0.0") | ||||
| 	port := utils.GetEnv("PORT", "3909") | ||||
| @ -31,7 +28,7 @@ func main() { | ||||
| 	addr := fmt.Sprintf("%s:%s", host, port) | ||||
| 	log.Printf("Starting server on http://%s", addr) | ||||
| 
 | ||||
| 	if err := http.ListenAndServe(addr, sessionMgr.LoadAndSave(mux)); err != nil { | ||||
| 	if err := http.ListenAndServe(addr, nil); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -1,27 +0,0 @@ | ||||
| package middleware | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"khairul169/garage-webui/utils" | ||||
| 	"net/http" | ||||
| ) | ||||
| 
 | ||||
| func AuthMiddleware(next http.Handler) http.Handler { | ||||
| 	authData := utils.GetEnv("AUTH_USER_PASS", "") | ||||
| 
 | ||||
| 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		auth := utils.Session.Get(r, "authenticated") | ||||
| 
 | ||||
| 		if authData == "" { | ||||
| 			next.ServeHTTP(w, r) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if auth == nil || !auth.(bool) { | ||||
| 			utils.ResponseErrorStatus(w, errors.New("unauthorized"), http.StatusUnauthorized) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		next.ServeHTTP(w, r) | ||||
| 	}) | ||||
| } | ||||
| @ -1,64 +0,0 @@ | ||||
| package router | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"khairul169/garage-webui/utils" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
| 
 | ||||
| type Auth struct{} | ||||
| 
 | ||||
| func (c *Auth) Login(w http.ResponseWriter, r *http.Request) { | ||||
| 	var body struct { | ||||
| 		Username string `json:"username"` | ||||
| 		Password string `json:"password"` | ||||
| 	} | ||||
| 	if err := json.NewDecoder(r.Body).Decode(&body); err != nil { | ||||
| 		utils.ResponseError(w, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	userPass := strings.Split(utils.GetEnv("AUTH_USER_PASS", ""), ":") | ||||
| 	if len(userPass) < 2 { | ||||
| 		utils.ResponseErrorStatus(w, errors.New("AUTH_USER_PASS not set"), 500) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if strings.TrimSpace(body.Username) != userPass[0] || bcrypt.CompareHashAndPassword([]byte(userPass[1]), []byte(body.Password)) != nil { | ||||
| 		utils.ResponseErrorStatus(w, errors.New("invalid username or password"), 401) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	utils.Session.Set(r, "authenticated", true) | ||||
| 	utils.ResponseSuccess(w, map[string]bool{ | ||||
| 		"authenticated": true, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (c *Auth) Logout(w http.ResponseWriter, r *http.Request) { | ||||
| 	utils.Session.Clear(r) | ||||
| 	utils.ResponseSuccess(w, true) | ||||
| } | ||||
| 
 | ||||
| func (c *Auth) GetStatus(w http.ResponseWriter, r *http.Request) { | ||||
| 	isAuthenticated := true | ||||
| 	authSession := utils.Session.Get(r, "authenticated") | ||||
| 	enabled := false | ||||
| 
 | ||||
| 	if utils.GetEnv("AUTH_USER_PASS", "") != "" { | ||||
| 		enabled = true | ||||
| 	} | ||||
| 
 | ||||
| 	if authSession != nil && authSession.(bool) { | ||||
| 		isAuthenticated = true | ||||
| 	} | ||||
| 
 | ||||
| 	utils.ResponseSuccess(w, map[string]bool{ | ||||
| 		"enabled":       enabled, | ||||
| 		"authenticated": isAuthenticated, | ||||
| 	}) | ||||
| } | ||||
| @ -1,19 +1,9 @@ | ||||
| package router | ||||
| 
 | ||||
| import ( | ||||
| 	"khairul169/garage-webui/middleware" | ||||
| 	"net/http" | ||||
| ) | ||||
| import "net/http" | ||||
| 
 | ||||
| func HandleApiRouter() *http.ServeMux { | ||||
| 	mux := http.NewServeMux() | ||||
| 
 | ||||
| 	auth := &Auth{} | ||||
| 	mux.HandleFunc("POST /auth/login", auth.Login) | ||||
| 
 | ||||
| 	router := http.NewServeMux() | ||||
| 	router.HandleFunc("POST /auth/logout", auth.Logout) | ||||
| 	router.HandleFunc("GET /auth/status", auth.GetStatus) | ||||
| 
 | ||||
| 	config := &Config{} | ||||
| 	router.HandleFunc("GET /config", config.GetAll) | ||||
| @ -27,9 +17,7 @@ func HandleApiRouter() *http.ServeMux { | ||||
| 	router.HandleFunc("PUT /browse/{bucket}/{key...}", browse.PutObject) | ||||
| 	router.HandleFunc("DELETE /browse/{bucket}/{key...}", browse.DeleteObject) | ||||
| 
 | ||||
| 	// Proxy request to garage api endpoint | ||||
| 	router.HandleFunc("/", ProxyHandler) | ||||
| 
 | ||||
| 	mux.Handle("/", middleware.AuthMiddleware(router)) | ||||
| 	return mux | ||||
| 	return router | ||||
| } | ||||
|  | ||||
| @ -3,6 +3,4 @@ | ||||
| 
 | ||||
| package ui | ||||
| 
 | ||||
| import "net/http" | ||||
| 
 | ||||
| func ServeUI(mux *http.ServeMux) {} | ||||
| func ServeUI() {} | ||||
|  | ||||
| @ -13,11 +13,11 @@ import ( | ||||
| //go:embed dist | ||||
| var embeddedFs embed.FS | ||||
| 
 | ||||
| func ServeUI(mux *http.ServeMux) { | ||||
| func ServeUI() { | ||||
| 	distFs, _ := fs.Sub(embeddedFs, "dist") | ||||
| 	fileServer := http.FileServer(http.FS(distFs)) | ||||
| 
 | ||||
| 	mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 	http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		_path := path.Clean(r.URL.Path)[1:] | ||||
| 
 | ||||
| 		// Rewrite non-existing paths to index.html | ||||
|  | ||||
| @ -1,33 +0,0 @@ | ||||
| package utils | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/alexedwards/scs/v2" | ||||
| ) | ||||
| 
 | ||||
| type SessionManager struct { | ||||
| 	mgr *scs.SessionManager | ||||
| } | ||||
| 
 | ||||
| var Session *SessionManager | ||||
| 
 | ||||
| func InitSessionManager() *scs.SessionManager { | ||||
| 	sessMgr := scs.New() | ||||
| 	sessMgr.Lifetime = 24 * time.Hour | ||||
| 	Session = &SessionManager{mgr: sessMgr} | ||||
| 	return sessMgr | ||||
| } | ||||
| 
 | ||||
| func (s *SessionManager) Get(r *http.Request, key string) interface{} { | ||||
| 	return s.mgr.Get(r.Context(), key) | ||||
| } | ||||
| 
 | ||||
| func (s *SessionManager) Set(r *http.Request, key string, value interface{}) { | ||||
| 	s.mgr.Put(r.Context(), key, value) | ||||
| } | ||||
| 
 | ||||
| func (s *SessionManager) Clear(r *http.Request) error { | ||||
| 	return s.mgr.Clear(r.Context()) | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "garage-webui", | ||||
|   "private": true, | ||||
|   "version": "1.0.7", | ||||
|   "version": "1.0.6", | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "dev:client": "vite", | ||||
| @ -47,11 +47,5 @@ | ||||
|     "typescript": "^5.5.3", | ||||
|     "typescript-eslint": "^8.0.0", | ||||
|     "vite": "^5.4.0" | ||||
|   }, | ||||
|   "pnpm": { | ||||
|     "onlyBuiltDependencies": [ | ||||
|       "@swc/core", | ||||
|       "esbuild" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| import { createBrowserRouter, RouterProvider } from "react-router-dom"; | ||||
| import { lazy, Suspense } from "react"; | ||||
| import { lazy } 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")); | ||||
| @ -14,12 +13,6 @@ const router = createBrowserRouter([ | ||||
|   { | ||||
|     path: "/auth", | ||||
|     Component: AuthLayout, | ||||
|     children: [ | ||||
|       { | ||||
|         path: "login", | ||||
|         Component: LoginPage, | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     path: "/", | ||||
| @ -49,11 +42,7 @@ const router = createBrowserRouter([ | ||||
| ]); | ||||
| 
 | ||||
| const Router = () => { | ||||
|   return ( | ||||
|     <Suspense> | ||||
|       <RouterProvider router={router} /> | ||||
|     </Suspense> | ||||
|   ); | ||||
|   return <RouterProvider router={router} />; | ||||
| }; | ||||
| 
 | ||||
| export default Router; | ||||
|  | ||||
| @ -4,7 +4,6 @@ import { | ||||
|   HardDrive, | ||||
|   KeySquare, | ||||
|   LayoutDashboard, | ||||
|   LogOut, | ||||
|   Palette, | ||||
| } from "lucide-react"; | ||||
| import { Dropdown, Menu } from "react-daisyui"; | ||||
| @ -13,10 +12,6 @@ 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 }, | ||||
| @ -27,7 +22,6 @@ 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"> | ||||
| @ -63,50 +57,23 @@ const Sidebar = () => { | ||||
|         })} | ||||
|       </Menu> | ||||
| 
 | ||||
|       <div className="py-2 px-4 flex items-center gap-2"> | ||||
|         <Dropdown vertical="top"> | ||||
|       <Dropdown className="my-2 mx-4" vertical="top"> | ||||
|         <Dropdown.Toggle button={false}> | ||||
|           <Button icon={Palette} color="ghost"> | ||||
|               {!auth.isEnabled ? "Theme" : null} | ||||
|             Theme | ||||
|           </Button> | ||||
|         </Dropdown.Toggle> | ||||
| 
 | ||||
|           <Dropdown.Menu className="max-h-[500px] overflow-y-auto"> | ||||
|         <Dropdown.Menu className="max-h-[200px] overflow-y-auto"> | ||||
|           {themes.map((theme) => ( | ||||
|               <Dropdown.Item | ||||
|                 key={theme} | ||||
|                 onClick={() => appStore.setTheme(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,22 +1,5 @@ | ||||
| import { useAuth } from "@/hooks/useAuth"; | ||||
| import { Navigate, Outlet } from "react-router-dom"; | ||||
| 
 | ||||
| const AuthLayout = () => { | ||||
|   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> | ||||
|   ); | ||||
|   return <div>AuthLayout</div>; | ||||
| }; | ||||
| 
 | ||||
| export default AuthLayout; | ||||
|  | ||||
| @ -1,17 +1,15 @@ | ||||
| import { PageContext } from "@/context/page-context"; | ||||
| import { Suspense, useContext, useEffect } from "react"; | ||||
| import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; | ||||
| import { 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) { | ||||
| @ -19,14 +17,6 @@ const MainLayout = () => { | ||||
|     } | ||||
|   }, [pathname]); | ||||
| 
 | ||||
|   if (auth.isLoading) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   if (!auth.isAuthenticated) { | ||||
|     return <Navigate to="/auth/login" />; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Drawer | ||||
|       open={sidebar.isOpen} | ||||
|  | ||||
| @ -4,7 +4,6 @@ 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; | ||||
|  | ||||
| @ -1,20 +0,0 @@ | ||||
| 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,7 +27,6 @@ const api = { | ||||
| 
 | ||||
|     const res = await fetch(_url, { | ||||
|       ...options, | ||||
|       credentials: "include", | ||||
|       headers: { ...headers, ...(options?.headers || {}) }, | ||||
|     }); | ||||
| 
 | ||||
|  | ||||
| @ -1,21 +0,0 @@ | ||||
| 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"); | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
| @ -1,54 +0,0 @@ | ||||
| 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> | ||||
|   ); | ||||
| } | ||||
| @ -1,6 +0,0 @@ | ||||
| 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"), | ||||
| }); | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user