From 9f11c021cef428e4f07cc925d6c2e876d97d5e97 Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Wed, 17 Jul 2024 18:52:29 -0400 Subject: [PATCH] oauth integration / reset password --- main.go | 10 +- migrations/initial-settings.go | 2 +- readme.md | 77 +++++++++ site/index.html | 2 +- site/public/icons/apple.svg | 1 + site/public/icons/bitbucket.svg | 1 + site/public/icons/discord.svg | 1 + site/public/icons/facebook.svg | 1 + site/public/icons/gitea.svg | 1 + site/public/icons/gitee.svg | 1 + site/public/icons/github.svg | 1 + site/public/icons/gitlab.svg | 1 + site/public/icons/google.svg | 1 + site/public/icons/instagram.svg | 1 + site/public/icons/lock.svg | 1 + site/public/icons/oidc.svg | 1 + site/public/icons/patreon.svg | 1 + site/public/icons/spotify.svg | 1 + site/public/icons/strava.svg | 1 + site/public/icons/twitch.svg | 1 + site/public/icons/twitter.svg | 1 + site/src/components/add-server.tsx | 6 +- site/src/components/command-palette.tsx | 3 +- site/src/components/login.tsx | 51 ------ .../auth-form.tsx} | 147 ++++++++++++------ .../src/components/login/forgot-pass-form.tsx | 100 ++++++++++++ site/src/components/login/login.tsx | 49 ++++++ site/src/components/router.tsx | 24 +++ site/src/components/routes/home.tsx | 2 +- .../components/server-table/systems-table.tsx | 3 +- site/src/lib/stores.ts | 14 -- site/src/main.tsx | 90 +++++++---- 32 files changed, 440 insertions(+), 157 deletions(-) create mode 100644 readme.md create mode 100644 site/public/icons/apple.svg create mode 100644 site/public/icons/bitbucket.svg create mode 100644 site/public/icons/discord.svg create mode 100644 site/public/icons/facebook.svg create mode 100644 site/public/icons/gitea.svg create mode 100644 site/public/icons/gitee.svg create mode 100644 site/public/icons/github.svg create mode 100644 site/public/icons/gitlab.svg create mode 100644 site/public/icons/google.svg create mode 100644 site/public/icons/instagram.svg create mode 100644 site/public/icons/lock.svg create mode 100644 site/public/icons/oidc.svg create mode 100644 site/public/icons/patreon.svg create mode 100644 site/public/icons/spotify.svg create mode 100644 site/public/icons/strava.svg create mode 100644 site/public/icons/twitch.svg create mode 100644 site/public/icons/twitter.svg delete mode 100644 site/src/components/login.tsx rename site/src/components/{user-auth-form.tsx => login/auth-form.tsx} (65%) create mode 100644 site/src/components/login/forgot-pass-form.tsx create mode 100644 site/src/components/login/login.tsx create mode 100644 site/src/components/router.tsx diff --git a/main.go b/main.go index c707962..7a27002 100644 --- a/main.go +++ b/main.go @@ -52,10 +52,12 @@ func main() { Scheme: "http", Host: "localhost:5173", }) + e.Router.GET("/icons/*", apis.StaticDirectoryHandler(os.DirFS("./site/public/icons"), false)) e.Router.Any("/*", echo.WrapHandler(proxy)) e.Router.Any("/", echo.WrapHandler(proxy)) default: - e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./site/dist"), true)) + e.Router.GET("/icons/*", apis.StaticDirectoryHandler(os.DirFS("./site/dist/icons"), false)) + e.Router.Any("/*", apis.StaticDirectoryHandler(os.DirFS("./site/dist"), true)) } return nil }) @@ -103,7 +105,7 @@ func main() { // create ssh key if it doesn't exist getSSHKey() // api route to return public key - e.Router.GET("/api/qoma/getkey", func(c echo.Context) error { + e.Router.GET("/api/beszel/getkey", func(c echo.Context) error { requestData := apis.RequestInfo(c) if requestData.AuthRecord == nil { return apis.NewForbiddenError("Forbidden", nil) @@ -120,7 +122,7 @@ func main() { // other api routes app.OnBeforeServe().Add(func(e *core.ServeEvent) error { // check if first time setup on login page - e.Router.GET("/api/qoma/first-run", func(c echo.Context) error { + e.Router.GET("/api/beszel/first-run", func(c echo.Context) error { adminNum, err := app.Dao().TotalAdmins() if err != nil { return err @@ -405,7 +407,7 @@ func handleStatusAlerts(newStatus string, oldRecord *models.Record) error { sendAlert(EmailData{ to: user.Get("email").(string), subj: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji), - body: fmt.Sprintf("Connection to %s is %s %v\n\n- Qoma", systemName, alertStatus, emoji), + body: fmt.Sprintf("Connection to %s is %s\n\n- Beszel", systemName, alertStatus), }) } return nil diff --git a/migrations/initial-settings.go b/migrations/initial-settings.go index dcb0a37..c528245 100644 --- a/migrations/initial-settings.go +++ b/migrations/initial-settings.go @@ -11,7 +11,7 @@ func init() { dao := daos.New(db) settings, _ := dao.FindSettings() - settings.Meta.AppName = "Qoma" + settings.Meta.AppName = "Beszel" settings.Meta.HideControls = true return dao.SaveSettings(settings) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..cec739f --- /dev/null +++ b/readme.md @@ -0,0 +1,77 @@ +# Beszel + +A lightweight resource monitoring hub with historical data, docker stats, and alerts. + + + + + + + + +
example of turso.tech/pricing link which is missing an og:image as of may 11 2024example of turso.tech/pricing link using an image generated by the server as it's og:image
+ +## Introduction + +Beszel has two components: the hub and the agent. + +The hub is a web application built on top of [PocketBase](https://pocketbase.io/) that provides a dashboard to view and manage your connected systems. + +The agent runs on each system you want to monitor. It provides a minimal SSH server through which it communicates system information to the hub. + +## Installation + +The hub and agent are distributed as single binary files, as well as docker images. + +> **Note**: The docker version does not support disk I/O stats, so use the binary version if that's important to you. + +### Docker + +### Binary + +## OAuth / OIDC integration + +Beszel supports OpenID Connect and many OAuth2 authentication providers (see list below). To enable this, you will need to: + +1. Create an OAuth2 application using your provider of choice. The redirect / callback URL should be `/api/oauth2-redirect`. +2. When you have the client ID and secret, go to the "Auth providers" page and enable your provider. + +
+ Supported provider list + +- Apple +- Bitbucket +- Discord +- Facebook +- Gitea +- Gitee +- GitHub +- GitLab +- Google +- Instagram +- Kakao +- LiveChat +- mailcow +- Microsoft +- OpenID Connect +- Patreon (v2) +- Planning Center +- Spotify +- Strava +- Twitch +- Twitter +- VK +- Yandex +
+ +## API + +Because Beszel is built on top of PocketBase, you can use the normal PocketBase API to read or update your data in your own applications. + +## Security + +The hub and agent communicate over SSH, so they do not need to be exposed to the internet. + +When the hub is started for the first time, it generates an ED25519 key pair. + +The agent's SSH server is configured to only accept connections using this key. It also does not provide a pty or accept any input, so it is not possible to execute commands on the agent. diff --git a/site/index.html b/site/index.html index e3e71c6..6002b70 100644 --- a/site/index.html +++ b/site/index.html @@ -4,7 +4,7 @@ - Qoma + Beszel \ No newline at end of file diff --git a/site/public/icons/bitbucket.svg b/site/public/icons/bitbucket.svg new file mode 100644 index 0000000..75a844a --- /dev/null +++ b/site/public/icons/bitbucket.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/discord.svg b/site/public/icons/discord.svg new file mode 100644 index 0000000..ebde05f --- /dev/null +++ b/site/public/icons/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/facebook.svg b/site/public/icons/facebook.svg new file mode 100644 index 0000000..6e28668 --- /dev/null +++ b/site/public/icons/facebook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/gitea.svg b/site/public/icons/gitea.svg new file mode 100644 index 0000000..8915778 --- /dev/null +++ b/site/public/icons/gitea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/gitee.svg b/site/public/icons/gitee.svg new file mode 100644 index 0000000..294cfba --- /dev/null +++ b/site/public/icons/gitee.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/github.svg b/site/public/icons/github.svg new file mode 100644 index 0000000..c979f5e --- /dev/null +++ b/site/public/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/gitlab.svg b/site/public/icons/gitlab.svg new file mode 100644 index 0000000..4902ba1 --- /dev/null +++ b/site/public/icons/gitlab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/google.svg b/site/public/icons/google.svg new file mode 100644 index 0000000..9f85fc9 --- /dev/null +++ b/site/public/icons/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/instagram.svg b/site/public/icons/instagram.svg new file mode 100644 index 0000000..ee03d71 --- /dev/null +++ b/site/public/icons/instagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/lock.svg b/site/public/icons/lock.svg new file mode 100644 index 0000000..dad77ab --- /dev/null +++ b/site/public/icons/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/oidc.svg b/site/public/icons/oidc.svg new file mode 100644 index 0000000..fe4c1cb --- /dev/null +++ b/site/public/icons/oidc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/patreon.svg b/site/public/icons/patreon.svg new file mode 100644 index 0000000..4d6de14 --- /dev/null +++ b/site/public/icons/patreon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/spotify.svg b/site/public/icons/spotify.svg new file mode 100644 index 0000000..885702b --- /dev/null +++ b/site/public/icons/spotify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/strava.svg b/site/public/icons/strava.svg new file mode 100644 index 0000000..8a63a00 --- /dev/null +++ b/site/public/icons/strava.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/twitch.svg b/site/public/icons/twitch.svg new file mode 100644 index 0000000..f1c7f61 --- /dev/null +++ b/site/public/icons/twitch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/public/icons/twitter.svg b/site/public/icons/twitter.svg new file mode 100644 index 0000000..47967f1 --- /dev/null +++ b/site/public/icons/twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/src/components/add-server.tsx b/site/src/components/add-server.tsx index a57cc3c..2b5bf78 100644 --- a/site/src/components/add-server.tsx +++ b/site/src/components/add-server.tsx @@ -27,8 +27,8 @@ export function AddServerButton() { function copyDockerCompose(port: string) { copyToClipboard(`services: agent: - image: 'henrygd/qoma-agent' - container_name: 'qoma-agent' + image: 'henrygd/beszel-agent' + container_name: 'beszel-agent' restart: unless-stopped ports: - '${port}:45876' @@ -43,7 +43,7 @@ export function AddServerButton() { return } // get public key - pb.send('/api/qoma/getkey', {}).then(({ key }) => { + pb.send('/api/beszel/getkey', {}).then(({ key }) => { $publicKey.set(key) }) }, [open]) diff --git a/site/src/components/command-palette.tsx b/site/src/components/command-palette.tsx index 3b6432d..16eba21 100644 --- a/site/src/components/command-palette.tsx +++ b/site/src/components/command-palette.tsx @@ -21,8 +21,9 @@ import { } from '@/components/ui/command' import { useEffect, useState } from 'react' import { useStore } from '@nanostores/react' -import { $systems, navigate } from '@/lib/stores' +import { $systems } from '@/lib/stores' import { isAdmin } from '@/lib/utils' +import { navigate } from './router' export default function CommandPalette() { const [open, setOpen] = useState(false) diff --git a/site/src/components/login.tsx b/site/src/components/login.tsx deleted file mode 100644 index 4370e9d..0000000 --- a/site/src/components/login.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { UserAuthForm } from '@/components/user-auth-form' -import { Logo } from './logo' -import { useEffect, useState } from 'react' -import { pb } from '@/lib/stores' - -export default function () { - const [isFirstRun, setFirstRun] = useState(false) - - useEffect(() => { - document.title = 'Login / Qoma' - - pb.send('/api/qoma/first-run', {}).then(({ firstRun }) => { - setFirstRun(firstRun) - }) - }, []) - - return ( -
-
-
-
-

- - Qoma -

-

- {isFirstRun ? 'Please create an admin account' : 'Please sign in to your account'} -

-
- -

- {/* todo: add forgot password section to readme and link to section - reset w/ command or link to pb reset */} - - Forgot password? - -

-
-
- {/*
- -
*/} -
- ) -} diff --git a/site/src/components/user-auth-form.tsx b/site/src/components/login/auth-form.tsx similarity index 65% rename from site/src/components/user-auth-form.tsx rename to site/src/components/login/auth-form.tsx index 695047b..a8e2c04 100644 --- a/site/src/components/user-auth-form.tsx +++ b/site/src/components/login/auth-form.tsx @@ -1,12 +1,11 @@ -import * as React from 'react' import { cn } from '@/lib/utils' import { buttonVariants } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { Github, LoaderCircle, LockIcon, LogInIcon, MailIcon, UserIcon } from 'lucide-react' +import { LoaderCircle, LockIcon, LogInIcon, MailIcon, UserIcon } from 'lucide-react' import { $authenticated, pb } from '@/lib/stores' import * as v from 'valibot' -import { toast } from './ui/use-toast' +import { toast } from '../ui/use-toast' import { Dialog, DialogContent, @@ -15,6 +14,9 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { useEffect, useState } from 'react' +import { AuthProviderInfo } from 'pocketbase' +import { Link } from '../router' const honeypot = v.literal('') const emailSchema = v.pipe(v.string(), v.email('Invalid email address.')) @@ -44,6 +46,14 @@ const RegisterSchema = v.looseObject({ passwordConfirm: passwordSchema, }) +const showLoginFaliedToast = () => { + toast({ + title: 'Login attempt failed', + description: 'Please check your credentials and try again', + variant: 'destructive', + }) +} + export function UserAuthForm({ className, isFirstRun, @@ -52,11 +62,21 @@ export function UserAuthForm({ className?: string isFirstRun: boolean }) { - const [isLoading, setIsLoading] = React.useState(false) - const [isGitHubLoading, setIsGitHubLoading] = React.useState(false) - const [errors, setErrors] = React.useState>({}) + const [isLoading, setIsLoading] = useState(false) + const [isGitHubLoading, setIsOauthLoading] = useState(false) + const [errors, setErrors] = useState>({}) + const [authProviders, setAuthProviders] = useState([]) - // const searchParams = useSearchParams() + useEffect(() => { + pb.collection('users') + .listAuthMethods() + .then((methods) => { + console.log('methods', methods) + console.log('password active', methods.emailPassword) + setAuthProviders(methods.authProviders) + console.log('auth providers', authProviders) + }) + }, []) async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -104,11 +124,7 @@ export function UserAuthForm({ } $authenticated.set(true) } catch (e) { - return toast({ - title: 'Login attempt failed', - description: 'Please check your credentials and try again', - variant: 'destructive', - }) + showLoginFaliedToast() } finally { setIsLoading(false) } @@ -220,44 +236,79 @@ export function UserAuthForm({ Or continue with - - - - - - - OAuth 2 / OIDC support - + + {authProviders.length > 0 && ( +
+ {authProviders.map((provider) => ( + + ))} +
+ )} + + {!authProviders.length && ( + + + + + + + OAuth 2 / OIDC support + +
+

Beszel supports OpenID Connect and many OAuth2 authentication providers.

- Support for OAuth / OIDC (all major providers) will be available in the future. As - well as an option to disable password auth. + Please view the{' '} + + GitHub README + {' '} + for instructions.

-

First I need to decide what to do with additional users.

-

- Should systems be shared across all accounts? Or should they be private by default - with team-based sharing? -

-

Let me know if you have strong opinions either way.

- - - -
+ +
+
+ )} + + Forgot password? + ) } diff --git a/site/src/components/login/forgot-pass-form.tsx b/site/src/components/login/forgot-pass-form.tsx new file mode 100644 index 0000000..dc00fea --- /dev/null +++ b/site/src/components/login/forgot-pass-form.tsx @@ -0,0 +1,100 @@ +import { LoaderCircle, MailIcon, SendIcon } from 'lucide-react' +import { Input } from '../ui/input' +import { Label } from '../ui/label' +import { useCallback, useState } from 'react' +import { toast } from '../ui/use-toast' +import { buttonVariants } from '../ui/button' +import { cn } from '@/lib/utils' +import { pb } from '@/lib/stores' +import { Dialog, DialogHeader } from '../ui/dialog' +import { DialogContent, DialogTrigger, DialogTitle } from '../ui/dialog' + +const showLoginFaliedToast = () => { + toast({ + title: 'Login attempt failed', + description: 'Please check your credentials and try again', + variant: 'destructive', + }) +} + +export default function ForgotPassword() { + const [isLoading, setIsLoading] = useState(false) + const [email, setEmail] = useState('') + + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + try { + // console.log(email) + await pb.collection('users').requestPasswordReset(email) + toast({ + title: 'Password reset request received', + description: `Check ${email} for a reset link.`, + }) + } catch (e) { + showLoginFaliedToast() + } finally { + setIsLoading(false) + setEmail('') + } + }, + [email] + ) + + return ( + <> +
+
+
+ + + setEmail(e.target.value)} + id="email" + name="email" + required + placeholder="name@example.com" + type="email" + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" + disabled={isLoading} + className="pl-9" + /> +
+ +
+
+ + + + + + + Command line instructions + +

+ If you don't have an SMTP server configured, you can use the following command to reset + your password: +

+ + beszel admin update youremail@example.com newpassword + +
+
+ + ) +} diff --git a/site/src/components/login/login.tsx b/site/src/components/login/login.tsx new file mode 100644 index 0000000..e0001fa --- /dev/null +++ b/site/src/components/login/login.tsx @@ -0,0 +1,49 @@ +import { UserAuthForm } from '@/components/login/auth-form' +import { Logo } from '../logo' +import { useEffect, useMemo, useState } from 'react' +import { pb } from '@/lib/stores' +import { useStore } from '@nanostores/react' +import ForgotPassword from './forgot-pass-form' +import { $router } from '../router' + +export default function () { + const page = useStore($router) + const [isFirstRun, setFirstRun] = useState(false) + + useEffect(() => { + document.title = 'Login / Beszel' + + pb.send('/api/beszel/first-run', {}).then(({ firstRun }) => { + setFirstRun(firstRun) + }) + }, []) + + const subtitle = useMemo(() => { + if (isFirstRun) { + return 'Please create an admin account' + } else if (page?.path === '/forgot-password') { + return 'Enter email address to reset password' + } else { + return 'Please sign in to your account' + } + }, [isFirstRun, page]) + + return ( +
+
+
+

+ + Beszel +

+

{subtitle}

+
+ {page?.path === '/forgot-password' ? ( + + ) : ( + + )} +
+
+ ) +} diff --git a/site/src/components/router.tsx b/site/src/components/router.tsx new file mode 100644 index 0000000..0bf76f2 --- /dev/null +++ b/site/src/components/router.tsx @@ -0,0 +1,24 @@ +import { createRouter } from '@nanostores/router' + +export const $router = createRouter( + { + home: '/', + server: '/server/:name', + 'forgot-password': '/forgot-password', + }, + { links: false } +) + +/** Navigate to url using router */ +export const navigate = (urlString: string) => { + $router.open(urlString) +} + +function onClick(e: React.MouseEvent) { + e.preventDefault() + $router.open(new URL((e.target as HTMLAnchorElement).href).pathname) +} + +export const Link = (props: React.AnchorHTMLAttributes) => { + return +} diff --git a/site/src/components/routes/home.tsx b/site/src/components/routes/home.tsx index 21325e6..d326d9b 100644 --- a/site/src/components/routes/home.tsx +++ b/site/src/components/routes/home.tsx @@ -5,7 +5,7 @@ const SystemsTable = lazy(() => import('../server-table/systems-table')) export default function () { useEffect(() => { - document.title = 'Dashboard / Qoma' + document.title = 'Dashboard / Beszel' }, []) return ( diff --git a/site/src/components/server-table/systems-table.tsx b/site/src/components/server-table/systems-table.tsx index 7d6ec2c..8db820b 100644 --- a/site/src/components/server-table/systems-table.tsx +++ b/site/src/components/server-table/systems-table.tsx @@ -57,11 +57,12 @@ import { Trash2Icon, } from 'lucide-react' import { useMemo, useState } from 'react' -import { $systems, pb, navigate } from '@/lib/stores' +import { $systems, pb } from '@/lib/stores' import { useStore } from '@nanostores/react' import { AddServerButton } from '../add-server' import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils' import AlertsButton from '../table-alerts' +import { navigate } from '../router' function CellFormatter(info: CellContext) { const val = info.getValue() as number diff --git a/site/src/lib/stores.ts b/site/src/lib/stores.ts index a40ca9f..19c2ccb 100644 --- a/site/src/lib/stores.ts +++ b/site/src/lib/stores.ts @@ -1,24 +1,10 @@ import PocketBase from 'pocketbase' import { atom, WritableAtom } from 'nanostores' import { AlertRecord, ChartTimes, SystemRecord } from '@/types' -import { createRouter } from '@nanostores/router' /** PocketBase JS Client */ export const pb = new PocketBase('/') -export const $router = createRouter( - { - home: '/', - server: '/server/:name', - }, - { links: false } -) - -/** Navigate to url using router */ -export const navigate = (urlString: string) => { - $router.open(urlString) -} - /** Store if user is authenticated */ export const $authenticated = atom(pb.authStore.isValid) diff --git a/site/src/main.tsx b/site/src/main.tsx index 609e387..2545647 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -3,15 +3,7 @@ import React, { Suspense, lazy, useEffect } from 'react' import ReactDOM from 'react-dom/client' import Home from './components/routes/home.tsx' import { ThemeProvider } from './components/theme-provider.tsx' -import { - $alerts, - $authenticated, - $updatedSystem, - $router, - $systems, - navigate, - pb, -} from './lib/stores.ts' +import { $alerts, $authenticated, $updatedSystem, $systems, pb } from './lib/stores.ts' import { ModeToggle } from './components/mode-toggle.tsx' import { cn, @@ -22,7 +14,16 @@ import { updateServerList, } from './lib/utils.ts' import { buttonVariants } from './components/ui/button.tsx' -import { DatabaseBackupIcon, Github, LogOutIcon, LogsIcon, UserIcon } from 'lucide-react' +import { + DatabaseBackupIcon, + GithubIcon, + LockKeyholeIcon, + LogOutIcon, + LogsIcon, + ServerIcon, + UserIcon, + UsersIcon, +} from 'lucide-react' import { useStore } from '@nanostores/react' import { Toaster } from './components/ui/toaster.tsx' import { Logo } from './components/logo.tsx' @@ -35,15 +36,18 @@ import { import { DropdownMenu, DropdownMenuContent, + DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, + DropdownMenuLabel, } from './components/ui/dropdown-menu.tsx' import { AlertRecord, SystemRecord } from './types' +import { $router, Link, navigate } from './components/router.tsx' const ServerDetail = lazy(() => import('./components/routes/server.tsx')) const CommandPalette = lazy(() => import('./components/command-palette.tsx')) -const LoginPage = lazy(() => import('./components/login.tsx')) +const LoginPage = lazy(() => import('./components/login/login.tsx')) const App = () => { const page = useStore($router) @@ -122,7 +126,7 @@ const Layout = () => { <>
- { }} > - +
@@ -145,7 +149,7 @@ const Layout = () => { href={'https://github.com/henrygd'} className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))} > - + @@ -163,28 +167,50 @@ const Layout = () => { - + + {pb.authStore.model?.email} + + + {isAdmin() && ( + <> + + + + Users + + + + + + Systems + + + + + + Logs + + + + + + Backups + + + + + + Auth providers + + + + + )} + pb.authStore.clear()}> Log out - {isAdmin() && ( - <> - - - - - Logs - - - - - - Backups - - - - )}