This commit is contained in:
Henry Dollman
2024-07-13 23:51:22 -04:00
parent 054a56c316
commit 0af3138ef7
17 changed files with 267 additions and 201 deletions

16
main.go
View File

@@ -41,20 +41,6 @@ func main() {
Automigrate: isGoRun,
})
app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
// update app settings on first run
settings := app.Settings()
if app.Settings().Meta.AppName == "Acme" {
app.Settings().Meta.AppName = "Qoma"
app.Settings().Meta.HideControls = true
err := app.Dao().SaveSettings(settings)
if err != nil {
return err
}
}
return nil
})
// serve site
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
switch isGoRun {
@@ -116,7 +102,7 @@ func main() {
// api route to return public key
e.Router.GET("/api/qoma/getkey", func(c echo.Context) error {
requestData := apis.RequestInfo(c)
if requestData.Admin == nil {
if requestData.AuthRecord == nil {
return apis.NewForbiddenError("Forbidden", nil)
}
key, err := os.ReadFile("./pb_data/id_ed25519.pub")

View File

@@ -12,73 +12,10 @@ import (
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "_pb_users_auth_",
"created": "2024-07-07 15:59:04.262Z",
"updated": "2024-07-09 23:42:40.542Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_name",
"name": "name",
"type": "text",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"presentable": false,
"unique": false,
"options": {
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"maxSelect": 1,
"maxSize": 5242880,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"onlyVerified": false,
"requireEmail": false
}
},
{
"id": "2hz5ncl8tizk5nx",
"created": "2024-07-07 16:08:20.979Z",
"updated": "2024-07-13 23:20:50.678Z",
"updated": "2024-07-14 03:36:23.090Z",
"name": "systems",
"type": "base",
"system": false,
@@ -156,17 +93,17 @@ func init() {
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"listRule": "",
"viewRule": "@request.auth.id != \"\"",
"createRule": "@request.auth.id != \"\" && @request.auth.admin = true",
"updateRule": "",
"deleteRule": "@request.auth.id != \"\" && @request.auth.admin = true",
"options": {}
},
{
"id": "ej9oowivz8b2mht",
"created": "2024-07-07 16:09:09.179Z",
"updated": "2024-07-09 23:42:40.542Z",
"updated": "2024-07-14 03:36:23.089Z",
"name": "system_stats",
"type": "base",
"system": false,
@@ -203,7 +140,7 @@ func init() {
"indexes": [
"CREATE INDEX ` + "`" + `idx_GxIee0j` + "`" + ` ON ` + "`" + `system_stats` + "`" + ` (` + "`" + `system` + "`" + `)"
],
"listRule": null,
"listRule": "@request.auth.id != \"\"",
"viewRule": null,
"createRule": null,
"updateRule": null,
@@ -213,7 +150,7 @@ func init() {
{
"id": "juohu4jipgc13v7",
"created": "2024-07-07 16:09:57.976Z",
"updated": "2024-07-09 23:42:40.542Z",
"updated": "2024-07-14 03:36:23.090Z",
"name": "container_stats",
"type": "base",
"system": false,
@@ -248,12 +185,71 @@ func init() {
}
],
"indexes": [],
"listRule": null,
"listRule": "@request.auth.id != \"\"",
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2024-07-14 03:36:23.076Z",
"updated": "2024-07-14 03:36:23.087Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"presentable": false,
"unique": false,
"options": {
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"maxSelect": 1,
"maxSize": 5242880,
"protected": false
}
},
{
"system": false,
"id": "ebyl7gfs",
"name": "admin",
"type": "bool",
"required": false,
"presentable": false,
"unique": false,
"options": {}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"onlyVerified": false,
"requireEmail": false
}
}
]`

View File

@@ -0,0 +1,19 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db)
settings, _ := dao.FindSettings()
settings.Meta.AppName = "Qoma"
settings.Meta.HideControls = true
return dao.SaveSettings(settings)
}, nil)
}

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.7 81.8"><path fill="#16a34a" d="M76 29v14a33 33 0 0 1-1 7 30 30 0 0 1-1 2 29 29 0 0 1-3 7 27 27 0 0 1 0 1 27 27 0 0 1-6 6 27 27 0 0 1-7 4l14 12H54L42 72H29a32 32 0 0 1-8-1 29 29 0 0 1-3-1q-6-2-10-6a28 28 0 0 1-6-10 30 30 0 0 1-2-9 35 35 0 0 1 0-2V29a32 32 0 0 1 1-8 28 28 0 0 1 1-3 28 28 0 0 1 5-9 27 27 0 0 1 1-1 28 28 0 0 1 10-6 32 32 0 0 1 0 0 30 30 0 0 1 9-2 35 35 0 0 1 2 0h17a32 32 0 0 1 9 1 28 28 0 0 1 3 1 28 28 0 0 1 9 6 27 27 0 0 1 6 9 31 31 0 0 1 0 1 30 30 0 0 1 3 9 35 35 0 0 1 0 2ZM63 43V29a20 20 0 0 0 0-4 17 17 0 0 0-1-3 15 15 0 0 0-3-4 14 14 0 0 0-1-1 15 15 0 0 0-4-3 17 17 0 0 0-1 0 17 17 0 0 0-5-1 21 21 0 0 0-2 0H29a20 20 0 0 0-4 0 17 17 0 0 0-2 1l-6 3a15 15 0 0 0-3 5 17 17 0 0 0 0 0 17 17 0 0 0-1 5 22 22 0 0 0 0 2v14a20 20 0 0 0 0 4 17 17 0 0 0 1 2 15 15 0 0 0 3 5 14 14 0 0 0 0 1 15 15 0 0 0 5 3 17 17 0 0 0 1 0 17 17 0 0 0 4 1 21 21 0 0 0 2 0h17a20 20 0 0 0 4 0 17 17 0 0 0 3-1l5-3a15 15 0 0 0 4-5 17 17 0 0 0 0-1 17 17 0 0 0 1-4 22 22 0 0 0 0-2Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.7 81.8"><path fill="#22c55e" d="M76 29v14a33 33 0 0 1-1 7 30 30 0 0 1-1 2 29 29 0 0 1-3 7 27 27 0 0 1 0 1 27 27 0 0 1-6 6 27 27 0 0 1-7 4l14 12H54L42 72H29a32 32 0 0 1-8-1 29 29 0 0 1-3-1q-6-2-10-6a28 28 0 0 1-6-10 30 30 0 0 1-2-9 35 35 0 0 1 0-2V29a32 32 0 0 1 1-8 28 28 0 0 1 1-3 28 28 0 0 1 5-9 27 27 0 0 1 1-1 28 28 0 0 1 10-6 32 32 0 0 1 0 0 30 30 0 0 1 9-2 35 35 0 0 1 2 0h17a32 32 0 0 1 9 1 28 28 0 0 1 3 1 28 28 0 0 1 9 6 27 27 0 0 1 6 9 31 31 0 0 1 0 1 30 30 0 0 1 3 9 35 35 0 0 1 0 2ZM63 43V29a20 20 0 0 0 0-4 17 17 0 0 0-1-3 15 15 0 0 0-3-4 14 14 0 0 0-1-1 15 15 0 0 0-4-3 17 17 0 0 0-1 0 17 17 0 0 0-5-1 21 21 0 0 0-2 0H29a20 20 0 0 0-4 0 17 17 0 0 0-2 1l-6 3a15 15 0 0 0-3 5 17 17 0 0 0 0 0 17 17 0 0 0-1 5 22 22 0 0 0 0 2v14a20 20 0 0 0 0 4 17 17 0 0 0 1 2 15 15 0 0 0 3 5 14 14 0 0 0 0 1 15 15 0 0 0 5 3 17 17 0 0 0 1 0 17 17 0 0 0 4 1 21 21 0 0 0 2 0h17a20 20 0 0 0 4 0 17 17 0 0 0 3-1l5-3a15 15 0 0 0 4-5 17 17 0 0 0 0-1 17 17 0 0 0 1-4 22 22 0 0 0 0-2Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -79,17 +79,17 @@ export function AddServerButton() {
Add <span className="hidden sm:inline">System</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg">
<DialogHeader>
<DialogTitle>Add New System</DialogTitle>
<DialogTitle className="mb-2">Add New System</DialogTitle>
<DialogDescription>
The agent must be running on the server to connect. Copy the{' '}
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent
below.
</DialogDescription>
</DialogHeader>
<form name="testing" action="/" onSubmit={handleSubmit as any}>
<div className="grid gap-4 py-4">
<form onSubmit={handleSubmit as any}>
<div className="grid gap-4 mt-1 mb-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
@@ -116,7 +116,7 @@ export function AddServerButton() {
/>
</div>
<div className="grid grid-cols-4 items-center gap-4 relative">
<Label htmlFor="pkey" className="text-right">
<Label htmlFor="pkey" className="text-right whitespace-pre">
Public Key
</Label>
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
@@ -144,7 +144,7 @@ export function AddServerButton() {
</TooltipProvider>
</div>
</div>
<DialogFooter>
<DialogFooter className="flex justify-end gap-2">
<Button
type="button"
variant={'ghost'}

View File

@@ -19,7 +19,7 @@ const chartConfig = {
},
} satisfies ChartConfig
export default function ({ chartData }: { chartData: { time: string; cpu: number }[] }) {
export default function CpuChart({ chartData }: { chartData: { time: string; cpu: number }[] }) {
if (!chartData?.length) {
return <Spinner />
}

View File

@@ -20,7 +20,7 @@ const chartConfig = {
},
} satisfies ChartConfig
export default function ({
export default function DiskChart({
chartData,
}: {
chartData: { time: string; disk: number; diskUsed: number }[]

View File

@@ -22,6 +22,7 @@ import {
import { useEffect, useState } from 'react'
import { useStore } from '@nanostores/react'
import { $servers, navigate } from '@/lib/stores'
import { isAdmin } from '@/lib/utils'
export default function CommandPalette() {
const [open, setOpen] = useState(false)
@@ -82,38 +83,42 @@ export default function CommandPalette() {
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Admin">
<CommandItem
onSelect={() => {
window.location.href = '/_/#/collections?collectionId=2hz5ncl8tizk5nx'
}}
>
<Database className="mr-2 h-4 w-4" />
<span>PocketBase</span>
<CommandShortcut>Admin</CommandShortcut>
</CommandItem>
<CommandItem
keywords={['email']}
onSelect={() => {
window.location.href = '/_/#/settings/backups'
}}
>
<DatabaseBackupIcon className="mr-2 h-4 w-4" />
<span>Database backups</span>
<CommandShortcut>Admin</CommandShortcut>
</CommandItem>
<CommandItem
keywords={['email']}
onSelect={() => {
window.location.href = '/_/#/settings/mail'
}}
>
<MailIcon className="mr-2 h-4 w-4" />
<span>SMTP settings</span>
<CommandShortcut>Admin</CommandShortcut>
</CommandItem>
</CommandGroup>
{isAdmin() && (
<>
<CommandSeparator />
<CommandGroup heading="Admin">
<CommandItem
onSelect={() => {
window.location.href = '/_/'
}}
>
<Database className="mr-2 h-4 w-4" />
<span>PocketBase</span>
<CommandShortcut>Admin</CommandShortcut>
</CommandItem>
<CommandItem
keywords={['email']}
onSelect={() => {
window.location.href = '/_/#/settings/backups'
}}
>
<DatabaseBackupIcon className="mr-2 h-4 w-4" />
<span>Database backups</span>
<CommandShortcut>Admin</CommandShortcut>
</CommandItem>
<CommandItem
keywords={['email']}
onSelect={() => {
window.location.href = '/_/#/settings/mail'
}}
>
<MailIcon className="mr-2 h-4 w-4" />
<span>SMTP settings</span>
<CommandShortcut>Admin</CommandShortcut>
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</CommandDialog>
)

View File

@@ -24,7 +24,7 @@ export default function () {
<span className="sr-only">Qoma</span>
</h1>
<p className="text-sm text-muted-foreground">
{isFirstRun ? 'Please create your admin account' : 'Please sign in to your account'}
{isFirstRun ? 'Please create an admin account' : 'Please sign in to your account'}
</p>
</div>
<UserAuthForm isFirstRun={isFirstRun} />

View File

@@ -21,7 +21,7 @@ export function ModeToggle() {
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>

View File

@@ -1,5 +1,4 @@
import { Suspense, lazy, useEffect } from 'react'
// import { DataTable } from '../server-table/data-table'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
const SystemsTable = lazy(() => import('../server-table/systems-table'))

View File

@@ -4,14 +4,18 @@ import { Suspense, lazy, useEffect, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
import { useStore } from '@nanostores/react'
import Spinner from '../spinner'
import CpuChart from '../charts/cpu-chart'
import MemChart from '../charts/mem-chart'
import DiskChart from '../charts/disk-chart'
import ContainerCpuChart from '../charts/container-cpu-chart'
// import CpuChart from '../charts/cpu-chart'
// import MemChart from '../charts/mem-chart'
// import DiskChart from '../charts/disk-chart'
// import ContainerCpuChart from '../charts/container-cpu-chart'
// import ContainerMemChart from '../charts/container-mem-chart'
import { CpuIcon, MemoryStickIcon } from 'lucide-react'
import ContainerMemChart from '../charts/container-mem-chart'
// const CpuChart = lazy(() => import('../cpu-chart'))
const CpuChart = lazy(() => import('../charts/cpu-chart'))
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
const MemChart = lazy(() => import('../charts/mem-chart'))
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
const DiskChart = lazy(() => import('../charts/disk-chart'))
function timestampToBrowserTime(timestamp: string) {
const date = new Date(timestamp)
@@ -138,6 +142,9 @@ export default function ServerDetail({ name }: { name: string }) {
return (
<>
<div className="grid gap-6 mb-10">
<div className="p-6">
<h1 className="text-3xl font-semibold">{name}</h1>
</div>
<Card className="pb-2">
<CardHeader>
<CardTitle className="flex gap-2 justify-between">
@@ -203,9 +210,9 @@ export default function ServerDetail({ name }: { name: string }) {
<CardDescription>Precise usage at the recorded time</CardDescription>
</CardHeader>
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
{/* <Suspense fallback={<Spinner />}> */}
<DiskChart chartData={diskChartData} />
{/* </Suspense> */}
<Suspense fallback={<Spinner />}>
<DiskChart chartData={diskChartData} />
</Suspense>
</CardContent>
</Card>
</div>

View File

@@ -61,7 +61,7 @@ import { useMemo, useState } from 'react'
import { $servers, pb, navigate } from '@/lib/stores'
import { useStore } from '@nanostores/react'
import { AddServerButton } from '../add-server'
import { cn, copyToClipboard } from '@/lib/utils'
import { cn, copyToClipboard, isAdmin } from '@/lib/utils'
import {
Dialog,
DialogContent,
@@ -75,7 +75,7 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
const val = info.getValue() as number
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="grow min-w-10 block bg-muted h-4 relative rounded-sm overflow-hidden">
<span className="grow min-w-10 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span
className={cn(
'absolute inset-0 w-full h-full origin-left',
@@ -288,9 +288,11 @@ export default function SystemsTable() {
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<div className="ml-auto flex gap-2">
<AddServerButton />
</div>
{isAdmin() && (
<div className="ml-auto flex gap-2">
<AddServerButton />
</div>
)}
</div>
<div className="rounded-md border overflow-hidden">
<Table>

View File

@@ -3,8 +3,8 @@ 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, LogInIcon } from 'lucide-react'
import { pb } from '@/lib/stores'
import { Github, 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 {
@@ -24,13 +24,21 @@ const passwordSchema = v.pipe(
)
const LoginSchema = v.looseObject({
username: honeypot,
name: honeypot,
email: emailSchema,
password: passwordSchema,
})
const RegisterSchema = v.looseObject({
username: honeypot,
name: honeypot,
username: v.pipe(
v.string(),
v.regex(
/^(?=.*[a-zA-Z])[a-zA-Z0-9_-]+$/,
'Invalid username. You may use alphanumeric characters, underscores, and hyphens.'
),
v.minLength(3, 'Username must be at least 3 characters long.')
),
email: emailSchema,
password: passwordSchema,
passwordConfirm: passwordSchema,
@@ -68,7 +76,7 @@ export function UserAuthForm({
setErrors(errors)
return
}
const { email, password, passwordConfirm } = result.output
const { email, password, passwordConfirm, username } = result.output
if (isFirstRun) {
// check that passwords match
if (password !== passwordConfirm) {
@@ -82,9 +90,19 @@ export function UserAuthForm({
passwordConfirm: password,
})
await pb.admins.authWithPassword(email, password)
await pb.collection('users').create({
username,
email,
password,
passwordConfirm: password,
admin: true,
verified: true,
})
await pb.collection('users').authWithPassword(email, password)
} else {
await pb.admins.authWithPassword(email, password)
await pb.collection('users').authWithPassword(email, password)
}
$authenticated.set(true)
} catch (e) {
return toast({
title: 'Login attempt failed',
@@ -100,30 +118,49 @@ export function UserAuthForm({
<div className={cn('grid gap-6', className)} {...props}>
<form onSubmit={handleSubmit} onChange={() => setErrors({})}>
<div className="grid gap-2.5">
<div className="sr-only">
{/* honeypot */}
<label htmlFor="username"></label>
<input id="username" type="text" name="username" tabIndex={-1} />
</div>
<div className="grid gap-1">
{isFirstRun && (
<div className="grid gap-1 relative">
<UserIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Label className="sr-only" htmlFor="username">
Username
</Label>
<Input
autoFocus={true}
id="username"
name="username"
required
placeholder="username"
type="username"
autoCapitalize="none"
autoComplete="username"
autoCorrect="off"
disabled={isLoading || isGitHubLoading}
className="pl-9"
/>
{errors?.username && <p className="px-1 text-xs text-red-600">{errors.username}</p>}
</div>
)}
<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
autoFocus={true}
id="email"
name="email"
required
placeholder="name@example.com"
placeholder={isFirstRun ? 'email' : 'name@example.com'}
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading || isGitHubLoading}
className="pl-9"
/>
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email}</p>}
</div>
<div className="grid gap-1">
<div className="grid gap-1 relative">
<LockIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Label className="sr-only" htmlFor="pass">
Password
</Label>
@@ -135,11 +172,13 @@ export function UserAuthForm({
type="password"
autoComplete="current-password"
disabled={isLoading || isGitHubLoading}
className="pl-9"
/>
{errors?.password && <p className="px-1 text-xs text-red-600">{errors.password}</p>}
</div>
{isFirstRun && (
<div className="grid gap-1">
<div className="grid gap-1 relative">
<LockIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Label className="sr-only" htmlFor="pass2">
Confirm password
</Label>
@@ -151,12 +190,18 @@ export function UserAuthForm({
type="password"
autoComplete="current-password"
disabled={isLoading || isGitHubLoading}
className="pl-9"
/>
{errors?.passwordConfirm && (
<p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>
)}
</div>
)}
<div className="sr-only">
{/* honeypot */}
<label htmlFor="name"></label>
<input id="name" type="text" name="name" tabIndex={-1} />
</div>
<button className={cn(buttonVariants())} disabled={isLoading}>
{isLoading ? (
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />

View File

@@ -4,8 +4,6 @@ import { SystemRecord } from '@/types'
import { createRouter } from '@nanostores/router'
export const pb = new PocketBase('/')
// @ts-ignore
pb.authStore.storageKey = 'pb_admin_auth'
export const $router = createRouter(
{
@@ -20,9 +18,6 @@ export const navigate = (urlString: string) => {
}
export const $authenticated = atom(pb.authStore.isValid)
pb.authStore.onChange(() => {
$authenticated.set(pb.authStore.isValid)
})
export const $servers = atom([] as SystemRecord[])

View File

@@ -54,3 +54,5 @@ export const formatShortDate = (timestamp: string) => shortDateFormatter.format(
export const updateFavicon = (newIconUrl: string) =>
((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl)
export const isAdmin = () => pb.authStore.model?.admin

View File

@@ -5,7 +5,7 @@ import Home from './components/routes/home.tsx'
import { ThemeProvider } from './components/theme-provider.tsx'
import { $authenticated, $router, $servers, navigate, pb } from './lib/stores.ts'
import { ModeToggle } from './components/mode-toggle.tsx'
import { cn, updateFavicon, updateServerList } from './lib/utils.ts'
import { cn, isAdmin, updateFavicon, updateServerList } from './lib/utils.ts'
import { buttonVariants } from './components/ui/button.tsx'
import { DatabaseBackupIcon, Github, LogOutIcon, LogsIcon, MailIcon, UserIcon } from 'lucide-react'
import { useStore } from '@nanostores/react'
@@ -35,8 +35,14 @@ const App = () => {
const authenticated = useStore($authenticated)
const servers = useStore($servers)
// get servers
useEffect(updateServerList, [])
useEffect(() => {
// get servers
updateServerList()
// change auth store on auth change
pb.authStore.onChange(() => {
$authenticated.set(pb.authStore.isValid)
})
}, [])
useEffect(() => {
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
@@ -128,36 +134,7 @@ const Layout = () => {
</a>
<div className={'flex ml-auto'}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<a
aria-label="User Actions"
href={'https://github.com/henrygd'}
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
>
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
</a>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onSelect={() => pb.authStore.clear()}>
<LogOutIcon className="mr-2.5 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
<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>
<ModeToggle />
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
@@ -175,7 +152,40 @@ const Layout = () => {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<ModeToggle />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<a
aria-label="User Actions"
href={'https://github.com/henrygd'}
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
>
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
</a>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<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>
</div>
</div>