mirror of
https://github.com/fankes/beszel.git
synced 2025-10-20 10:19:27 +08:00
oauth integration / reset password
This commit is contained in:
@@ -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])
|
||||
|
@@ -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)
|
||||
|
@@ -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 (
|
||||
<div className="relative h-screen grid lg:max-w-none lg:px-0">
|
||||
<div className="grid items-center py-12">
|
||||
<div className="grid gap-5 w-full px-4 max-w-[22em] mx-auto">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-3">
|
||||
<Logo className="h-7 fill-foreground mx-auto" />
|
||||
<span className="sr-only">Qoma</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isFirstRun ? 'Please create an admin account' : 'Please sign in to your account'}
|
||||
</p>
|
||||
</div>
|
||||
<UserAuthForm isFirstRun={isFirstRun} />
|
||||
<p className="text-center text-sm opacity-70 hover:opacity-100 transition-opacity">
|
||||
{/* todo: add forgot password section to readme and link to section
|
||||
reset w/ command or link to pb reset */}
|
||||
<a
|
||||
href="https://github.com/henrygd/qoma"
|
||||
className="hover:text-brand underline underline-offset-4"
|
||||
>
|
||||
Forgot password?
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="relative hidden h-full bg-primary lg:block">
|
||||
<img
|
||||
className="absolute inset-0 h-full w-full object-cover bg-primary"
|
||||
src="/penguin-and-egg.avif"
|
||||
></img>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -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<boolean>(false)
|
||||
const [isGitHubLoading, setIsGitHubLoading] = React.useState<boolean>(false)
|
||||
const [errors, setErrors] = React.useState<Record<string, string | undefined>>({})
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [isGitHubLoading, setIsOauthLoading] = useState<boolean>(false)
|
||||
const [errors, setErrors] = useState<Record<string, string | undefined>>({})
|
||||
const [authProviders, setAuthProviders] = useState<AuthProviderInfo[]>([])
|
||||
|
||||
// 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<HTMLFormElement>) {
|
||||
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({
|
||||
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(buttonVariants({ variant: 'outline' }))}
|
||||
// onClick={async () => {
|
||||
// setIsGitHubLoading(true)
|
||||
// do stuff
|
||||
// setIsGitHubLoading(false)
|
||||
// }}
|
||||
disabled={isLoading || isGitHubLoading}
|
||||
>
|
||||
{isGitHubLoading ? (
|
||||
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
)}{' '}
|
||||
Github
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent style={{ maxWidth: 440, width: '90%' }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-2">OAuth 2 / OIDC support</DialogTitle>
|
||||
<DialogDescription className="grid gap-3">
|
||||
|
||||
{authProviders.length > 0 && (
|
||||
<div className="grid gap-2">
|
||||
{authProviders.map((provider) => (
|
||||
<button
|
||||
key={provider.name}
|
||||
type="button"
|
||||
className={cn(buttonVariants({ variant: 'outline' }))}
|
||||
onClick={async () => {
|
||||
setIsOauthLoading(true)
|
||||
try {
|
||||
await pb.collection('users').authWithOAuth2({ provider: provider.name })
|
||||
$authenticated.set(pb.authStore.isValid)
|
||||
} catch (e) {
|
||||
showLoginFaliedToast()
|
||||
} finally {
|
||||
setIsOauthLoading(false)
|
||||
}
|
||||
}}
|
||||
disabled={isLoading || isGitHubLoading}
|
||||
>
|
||||
{isGitHubLoading ? (
|
||||
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<img
|
||||
className="mr-2 h-4 w-4 dark:invert"
|
||||
src={`/icons/${provider.name}.svg`}
|
||||
alt=""
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = '/icons/lock.svg'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="translate-y-[1px]">{provider.displayName}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!authProviders.length && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<button type="button" className={cn(buttonVariants({ variant: 'outline' }))}>
|
||||
<img className="mr-2 h-4 w-4 dark:invert" src="/icons/github.svg" alt="" />
|
||||
<span className="translate-y-[1px]">GitHub</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent style={{ maxWidth: 440, width: '90%' }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>OAuth 2 / OIDC support</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="text-primary/70 text-[0.95em] contents">
|
||||
<p>Beszel supports OpenID Connect and many OAuth2 authentication providers.</p>
|
||||
<p>
|
||||
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{' '}
|
||||
<a
|
||||
href="https://github.com/henrygd/beszel/blob/main/readme.md#oauth--oidc-integration"
|
||||
className={cn(buttonVariants({ variant: 'link' }), 'p-0 h-auto')}
|
||||
>
|
||||
GitHub README
|
||||
</a>{' '}
|
||||
for instructions.
|
||||
</p>
|
||||
<p>First I need to decide what to do with additional users.</p>
|
||||
<p>
|
||||
Should systems be shared across all accounts? Or should they be private by default
|
||||
with team-based sharing?
|
||||
</p>
|
||||
<p>Let me know if you have strong opinions either way.</p>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-sm mx-auto mt-2 hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
100
site/src/components/login/forgot-pass-form.tsx
Normal file
100
site/src/components/login/forgot-pass-form.tsx
Normal file
@@ -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<boolean>(false)
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-3">
|
||||
<div className="grid gap-1 relative">
|
||||
<MailIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Label className="sr-only" htmlFor="email">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<button className={cn(buttonVariants())} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<SendIcon className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Reset password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<button className="text-sm mx-auto mt-2 hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity">
|
||||
Command line instructions
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[33em]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Command line instructions</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-primary/70 text-[0.95em]">
|
||||
If you don't have an SMTP server configured, you can use the following command to reset
|
||||
your password:
|
||||
</p>
|
||||
<code className="bg-muted rounded-sm py-0.5 px-2.5 mr-auto text-sm">
|
||||
beszel admin update youremail@example.com newpassword
|
||||
</code>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
49
site/src/components/login/login.tsx
Normal file
49
site/src/components/login/login.tsx
Normal file
@@ -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 (
|
||||
<div className="min-h-screen grid items-center py-12">
|
||||
<div className="grid gap-5 w-full px-4 max-w-[22em] mx-auto">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-3">
|
||||
<Logo className="h-7 fill-foreground mx-auto" />
|
||||
<span className="sr-only">Beszel</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
||||
</div>
|
||||
{page?.path === '/forgot-password' ? (
|
||||
<ForgotPassword />
|
||||
) : (
|
||||
<UserAuthForm isFirstRun={isFirstRun} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
24
site/src/components/router.tsx
Normal file
24
site/src/components/router.tsx
Normal file
@@ -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<HTMLAnchorElement, MouseEvent>) {
|
||||
e.preventDefault()
|
||||
$router.open(new URL((e.target as HTMLAnchorElement).href).pathname)
|
||||
}
|
||||
|
||||
export const Link = (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
return <a onClick={onClick} {...props}></a>
|
||||
}
|
@@ -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 (
|
||||
|
@@ -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<SystemRecord, unknown>) {
|
||||
const val = info.getValue() as number
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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 = () => {
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="flex items-center h-16 bg-card px-6 border bt-0 rounded-md my-5">
|
||||
<a
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Home"
|
||||
className={'p-2 pl-0'}
|
||||
@@ -132,7 +136,7 @@ const Layout = () => {
|
||||
}}
|
||||
>
|
||||
<Logo className="h-[1.2em] fill-foreground" />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<div className={'flex ml-auto'}>
|
||||
<ModeToggle />
|
||||
@@ -145,7 +149,7 @@ const Layout = () => {
|
||||
href={'https://github.com/henrygd'}
|
||||
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
||||
>
|
||||
<Github className="h-[1.2rem] w-[1.2rem]" />
|
||||
<GithubIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -163,28 +167,50 @@ const Layout = () => {
|
||||
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
</a>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent align="end" className="min-w-44">
|
||||
<DropdownMenuLabel>{pb.authStore.model?.email}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{isAdmin() && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/">
|
||||
<UsersIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Users</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/collections?collectionId=2hz5ncl8tizk5nx">
|
||||
<ServerIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Systems</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/logs">
|
||||
<LogsIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Logs</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/settings/backups">
|
||||
<DatabaseBackupIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Backups</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/settings/auth-providers">
|
||||
<LockKeyholeIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Auth providers</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={() => pb.authStore.clear()}>
|
||||
<LogOutIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Log out</span>
|
||||
</DropdownMenuItem>
|
||||
{isAdmin() && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/logs">
|
||||
<LogsIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Logs</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/settings/backups">
|
||||
<DatabaseBackupIcon className="mr-2.5 h-4 w-4" />
|
||||
<span>Backups</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user