mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 01:39:34 +08:00
updates
This commit is contained in:
16
main.go
16
main.go
@@ -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")
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
|
19
migrations/initial-settings.go
Normal file
19
migrations/initial-settings.go
Normal 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)
|
||||
}
|
@@ -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 |
@@ -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'}
|
||||
|
@@ -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 />
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ const chartConfig = {
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function ({
|
||||
export default function DiskChart({
|
||||
chartData,
|
||||
}: {
|
||||
chartData: { time: string; disk: number; diskUsed: number }[]
|
||||
|
@@ -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>
|
||||
)
|
||||
|
@@ -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} />
|
||||
|
@@ -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>
|
||||
|
@@ -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'))
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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" />
|
||||
|
@@ -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[])
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user