diff --git a/main.go b/main.go index 4228e84..a2eb004 100644 --- a/main.go +++ b/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") diff --git a/migrations/1720568457_collections_snapshot.go b/migrations/1720568457_collections_snapshot.go index 993c8ec..ecb2433 100644 --- a/migrations/1720568457_collections_snapshot.go +++ b/migrations/1720568457_collections_snapshot.go @@ -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 + } } ]` diff --git a/migrations/initial-settings.go b/migrations/initial-settings.go new file mode 100644 index 0000000..dcb0a37 --- /dev/null +++ b/migrations/initial-settings.go @@ -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) +} diff --git a/site/public/favicon-green.svg b/site/public/favicon-green.svg index 5efca21..cf1bbee 100644 --- a/site/public/favicon-green.svg +++ b/site/public/favicon-green.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/site/src/components/add-server.tsx b/site/src/components/add-server.tsx index 51d38d4..bf4dbb7 100644 --- a/site/src/components/add-server.tsx +++ b/site/src/components/add-server.tsx @@ -79,17 +79,17 @@ export function AddServerButton() { Add System - + - Add New System + Add New System The agent must be running on the server to connect. Copy the{' '} docker-compose.yml for the agent below. - - + + Name @@ -116,7 +116,7 @@ export function AddServerButton() { /> - + Public Key @@ -144,7 +144,7 @@ export function AddServerButton() { - + } diff --git a/site/src/components/charts/disk-chart.tsx b/site/src/components/charts/disk-chart.tsx index 61cc404..e84341e 100644 --- a/site/src/components/charts/disk-chart.tsx +++ b/site/src/components/charts/disk-chart.tsx @@ -20,7 +20,7 @@ const chartConfig = { }, } satisfies ChartConfig -export default function ({ +export default function DiskChart({ chartData, }: { chartData: { time: string; disk: number; diskUsed: number }[] diff --git a/site/src/components/command-palette.tsx b/site/src/components/command-palette.tsx index fedaecb..b756609 100644 --- a/site/src/components/command-palette.tsx +++ b/site/src/components/command-palette.tsx @@ -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() { ))} - - - { - window.location.href = '/_/#/collections?collectionId=2hz5ncl8tizk5nx' - }} - > - - PocketBase - Admin - - { - window.location.href = '/_/#/settings/backups' - }} - > - - Database backups - Admin - - { - window.location.href = '/_/#/settings/mail' - }} - > - - SMTP settings - Admin - - + {isAdmin() && ( + <> + + + { + window.location.href = '/_/' + }} + > + + PocketBase + Admin + + { + window.location.href = '/_/#/settings/backups' + }} + > + + Database backups + Admin + + { + window.location.href = '/_/#/settings/mail' + }} + > + + SMTP settings + Admin + + + > + )} ) diff --git a/site/src/components/login.tsx b/site/src/components/login.tsx index d1d8e5e..4370e9d 100644 --- a/site/src/components/login.tsx +++ b/site/src/components/login.tsx @@ -24,7 +24,7 @@ export default function () { Qoma - {isFirstRun ? 'Please create your admin account' : 'Please sign in to your account'} + {isFirstRun ? 'Please create an admin account' : 'Please sign in to your account'} diff --git a/site/src/components/mode-toggle.tsx b/site/src/components/mode-toggle.tsx index 8ce1f1b..c0b404f 100644 --- a/site/src/components/mode-toggle.tsx +++ b/site/src/components/mode-toggle.tsx @@ -21,7 +21,7 @@ export function ModeToggle() { Toggle theme - + setTheme('light')}>Light setTheme('dark')}>Dark setTheme('system')}>System diff --git a/site/src/components/routes/home.tsx b/site/src/components/routes/home.tsx index d6eb676..21325e6 100644 --- a/site/src/components/routes/home.tsx +++ b/site/src/components/routes/home.tsx @@ -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')) diff --git a/site/src/components/routes/server.tsx b/site/src/components/routes/server.tsx index bd6dc8a..7345bcd 100644 --- a/site/src/components/routes/server.tsx +++ b/site/src/components/routes/server.tsx @@ -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 ( <> + + {name} + @@ -203,9 +210,9 @@ export default function ServerDetail({ name }: { name: string }) { Precise usage at the recorded time - {/* }> */} - - {/* */} + }> + + diff --git a/site/src/components/server-table/systems-table.tsx b/site/src/components/server-table/systems-table.tsx index 33dea94..7bd04de 100644 --- a/site/src/components/server-table/systems-table.tsx +++ b/site/src/components/server-table/systems-table.tsx @@ -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) { const val = info.getValue() as number return ( - + table.getColumn('name')?.setFilterValue(event.target.value)} className="max-w-sm" /> - - - + {isAdmin() && ( + + + + )} diff --git a/site/src/components/user-auth-form.tsx b/site/src/components/user-auth-form.tsx index 0847dac..6efc8ed 100644 --- a/site/src/components/user-auth-form.tsx +++ b/site/src/components/user-auth-form.tsx @@ -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({ setErrors({})}> - - {/* honeypot */} - - - - + {isFirstRun && ( + + + + Username + + + {errors?.username && {errors.username}} + + )} + + Email {errors?.email && {errors.email}} - + + Password @@ -135,11 +172,13 @@ export function UserAuthForm({ type="password" autoComplete="current-password" disabled={isLoading || isGitHubLoading} + className="pl-9" /> {errors?.password && {errors.password}} {isFirstRun && ( - + + Confirm password @@ -151,12 +190,18 @@ export function UserAuthForm({ type="password" autoComplete="current-password" disabled={isLoading || isGitHubLoading} + className="pl-9" /> {errors?.passwordConfirm && ( {errors.passwordConfirm} )} )} + + {/* honeypot */} + + + {isLoading ? ( diff --git a/site/src/lib/stores.ts b/site/src/lib/stores.ts index b979508..1f9c877 100644 --- a/site/src/lib/stores.ts +++ b/site/src/lib/stores.ts @@ -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[]) diff --git a/site/src/lib/utils.ts b/site/src/lib/utils.ts index cf11fda..bc5e085 100644 --- a/site/src/lib/utils.ts +++ b/site/src/lib/utils.ts @@ -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 diff --git a/site/src/main.tsx b/site/src/main.tsx index 338a93d..112ceb7 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -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('systems').subscribe('*', (e) => { @@ -128,36 +134,7 @@ const Layout = () => { - - - - - - - - pb.authStore.clear()}> - - Log out - - - - - - Logs - - - - - - Backups - - - - + @@ -175,7 +152,40 @@ const Layout = () => { - + + + + + + + + pb.authStore.clear()}> + + Log out + + {isAdmin() && ( + <> + + + + + Logs + + + + + + Backups + + + > + )} + +
docker-compose.yml
- {isFirstRun ? 'Please create your admin account' : 'Please sign in to your account'} + {isFirstRun ? 'Please create an admin account' : 'Please sign in to your account'}
{errors.username}
{errors.email}
{errors.password}
{errors.passwordConfirm}