From 054a56c31686815241e72754f8e2622239c0f80d Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Sat, 13 Jul 2024 19:43:14 -0400 Subject: [PATCH] user creation and other updates --- main.go | 30 ++++- migrations/1720568457_collections_snapshot.go | 6 +- site/src/components/add-server.tsx | 2 +- site/src/components/command-palette.tsx | 58 +++++++--- site/src/components/login.tsx | 16 ++- site/src/components/routes/home.tsx | 4 +- .../{data-table.tsx => systems-table.tsx} | 4 +- site/src/components/ui/command.tsx | 5 +- site/src/components/user-auth-form.tsx | 104 +++++++++++++----- site/src/main.tsx | 2 +- 10 files changed, 177 insertions(+), 54 deletions(-) rename site/src/components/server-table/{data-table.tsx => systems-table.tsx} (99%) diff --git a/main.go b/main.go index ec11478..4228e84 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,20 @@ 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 { @@ -95,11 +109,12 @@ func main() { return nil }) + // ssh key setup app.OnBeforeServe().Add(func(e *core.ServeEvent) error { // create ssh key if it doesn't exist getSSHKey() // api route to return public key - e.Router.GET("/getkey", func(c echo.Context) error { + e.Router.GET("/api/qoma/getkey", func(c echo.Context) error { requestData := apis.RequestInfo(c) if requestData.Admin == nil { return apis.NewForbiddenError("Forbidden", nil) @@ -113,6 +128,19 @@ func main() { return nil }) + // other api routes + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + // check if first time setup on login page + e.Router.GET("/api/qoma/first-run", func(c echo.Context) error { + adminNum, err := app.Dao().TotalAdmins() + if err != nil { + return err + } + return c.JSON(http.StatusOK, map[string]bool{"firstRun": adminNum == 0}) + }) + return nil + }) + // start ticker for server updates app.OnBeforeServe().Add(func(e *core.ServeEvent) error { go serverUpdateTicker() diff --git a/migrations/1720568457_collections_snapshot.go b/migrations/1720568457_collections_snapshot.go index fdb2198..993c8ec 100644 --- a/migrations/1720568457_collections_snapshot.go +++ b/migrations/1720568457_collections_snapshot.go @@ -78,7 +78,7 @@ func init() { { "id": "2hz5ncl8tizk5nx", "created": "2024-07-07 16:08:20.979Z", - "updated": "2024-07-13 01:18:43.529Z", + "updated": "2024-07-13 23:20:50.678Z", "name": "systems", "type": "base", "system": false, @@ -155,9 +155,7 @@ func init() { } } ], - "indexes": [ - "CREATE UNIQUE INDEX ` + "`" + `idx_eggNgAn` + "`" + ` ON ` + "`" + `systems` + "`" + ` (` + "`" + `name` + "`" + `)" - ], + "indexes": [], "listRule": null, "viewRule": null, "createRule": null, diff --git a/site/src/components/add-server.tsx b/site/src/components/add-server.tsx index 0d15b06..51d38d4 100644 --- a/site/src/components/add-server.tsx +++ b/site/src/components/add-server.tsx @@ -43,7 +43,7 @@ export function AddServerButton() { return } // get public key - pb.send('/getkey', {}).then(({ key }) => { + pb.send('/api/qoma/getkey', {}).then(({ key }) => { $publicKey.set(key) }) }, [open]) diff --git a/site/src/components/command-palette.tsx b/site/src/components/command-palette.tsx index c1b6254..fedaecb 100644 --- a/site/src/components/command-palette.tsx +++ b/site/src/components/command-palette.tsx @@ -1,6 +1,13 @@ 'use client' -import { Database, Github, Home, Server } from 'lucide-react' +import { + Database, + DatabaseBackupIcon, + Github, + LayoutDashboard, + MailIcon, + Server, +} from 'lucide-react' import { CommandDialog, @@ -16,7 +23,7 @@ import { useEffect, useState } from 'react' import { useStore } from '@nanostores/react' import { $servers, navigate } from '@/lib/stores' -export default function () { +export default function CommandPalette() { const [open, setOpen] = useState(false) const servers = useStore($servers) @@ -39,24 +46,16 @@ export default function () { No results found. { navigate('/') setOpen((open) => !open) }} > - - Home + + Dashboard Page - { - window.location.href = '/_/#/collections?collectionId=2hz5ncl8tizk5nx' - }} - > - - Admin UI - PocketBase - { window.location.href = 'https://github.com/henrygd' @@ -79,9 +78,42 @@ export default function () { > {server.name} + {server.host} ))} + + + { + window.location.href = '/_/#/collections?collectionId=2hz5ncl8tizk5nx' + }} + > + + 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 62af7ff..d1d8e5e 100644 --- a/site/src/components/login.tsx +++ b/site/src/components/login.tsx @@ -1,11 +1,19 @@ import { UserAuthForm } from '@/components/user-auth-form' import { Logo } from './logo' -import { useEffect } from 'react' +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 (
@@ -15,9 +23,11 @@ export default function () { Qoma -

Please sign in to your account

+

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

- +

{/* todo: add forgot password section to readme and link to section reset w/ command or link to pb reset */} diff --git a/site/src/components/routes/home.tsx b/site/src/components/routes/home.tsx index ea7524c..d6eb676 100644 --- a/site/src/components/routes/home.tsx +++ b/site/src/components/routes/home.tsx @@ -2,7 +2,7 @@ import { Suspense, lazy, useEffect } from 'react' // import { DataTable } from '../server-table/data-table' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card' -const DataTable = lazy(() => import('../server-table/data-table')) +const SystemsTable = lazy(() => import('../server-table/systems-table')) export default function () { useEffect(() => { @@ -24,7 +24,7 @@ export default function () { - + diff --git a/site/src/components/server-table/data-table.tsx b/site/src/components/server-table/systems-table.tsx similarity index 99% rename from site/src/components/server-table/data-table.tsx rename to site/src/components/server-table/systems-table.tsx index 909af02..33dea94 100644 --- a/site/src/components/server-table/data-table.tsx +++ b/site/src/components/server-table/systems-table.tsx @@ -103,7 +103,7 @@ function sortableHeader(column: Column, name: string, Ico ) } -export default function () { +export default function SystemsTable() { const data = useStore($servers) // const [deleteServer, setDeleteServer] = useState({} as SystemRecord) const [sorting, setSorting] = useState([]) @@ -135,7 +135,7 @@ export default function () { onClick={() => copyToClipboard(info.getValue() as string)} > {info.getValue() as string} - + ) diff --git a/site/src/components/ui/command.tsx b/site/src/components/ui/command.tsx index 97db253..1cd97be 100644 --- a/site/src/components/ui/command.tsx +++ b/site/src/components/ui/command.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { type DialogProps } from '@radix-ui/react-dialog' +import { DialogTitle, type DialogProps } from '@radix-ui/react-dialog' import { Command as CommandPrimitive } from 'cmdk' import { Search } from 'lucide-react' @@ -27,6 +27,9 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return (

+
+ Command +
{children} diff --git a/site/src/components/user-auth-form.tsx b/site/src/components/user-auth-form.tsx index 8d20bc5..0847dac 100644 --- a/site/src/components/user-auth-form.tsx +++ b/site/src/components/user-auth-form.tsx @@ -1,17 +1,8 @@ -'use client' - import * as React from 'react' -// import { useSearchParams } from 'next/navigation' -// import { zodResolver } from '@hookform/resolvers/zod' -// import { signIn } from 'next-auth/react' -// import { useForm } from 'react-hook-form' -// import * as z from 'zod' - import { cn } from '@/lib/utils' import { buttonVariants } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -// import { toast } from '@/components/ui/use-toast' import { Github, LoaderCircle, LogInIcon } from 'lucide-react' import { pb } from '@/lib/stores' import * as v from 'valibot' @@ -25,14 +16,34 @@ import { DialogTitle, } from '@/components/ui/dialog' -const LoginSchema = v.object({ - email: v.pipe(v.string(), v.email('Invalid email address.')), - password: v.pipe(v.string(), v.minLength(10, 'Password must be at least 10 characters long.')), +const honeypot = v.literal('') +const emailSchema = v.pipe(v.string(), v.email('Invalid email address.')) +const passwordSchema = v.pipe( + v.string(), + v.minLength(10, 'Password must be at least 10 characters.') +) + +const LoginSchema = v.looseObject({ + username: honeypot, + email: emailSchema, + password: passwordSchema, }) -// type LoginData = v.InferOutput // { email: string; password: string } +const RegisterSchema = v.looseObject({ + username: honeypot, + email: emailSchema, + password: passwordSchema, + passwordConfirm: passwordSchema, +}) -export function UserAuthForm({ className, ...props }: { className?: string }) { +export function UserAuthForm({ + className, + isFirstRun, + ...props +}: { + className?: string + isFirstRun: boolean +}) { const [isLoading, setIsLoading] = React.useState(false) const [isGitHubLoading, setIsGitHubLoading] = React.useState(false) const [errors, setErrors] = React.useState>({}) @@ -45,8 +56,10 @@ export function UserAuthForm({ className, ...props }: { className?: string }) { try { const formData = new FormData(e.target as HTMLFormElement) const data = Object.fromEntries(formData) as Record - const result = v.safeParse(LoginSchema, data) + const Schema = isFirstRun ? RegisterSchema : LoginSchema + const result = v.safeParse(Schema, data) if (!result.success) { + console.log(result) let errors = {} for (const issue of result.issues) { // @ts-ignore @@ -55,9 +68,14 @@ export function UserAuthForm({ className, ...props }: { className?: string }) { setErrors(errors) return } - const { email, password } = result.output - let firstRun = true - if (firstRun) { + const { email, password, passwordConfirm } = result.output + if (isFirstRun) { + // check that passwords match + if (password !== passwordConfirm) { + let msg = 'Passwords do not match' + setErrors({ passwordConfirm: msg }) + return + } await pb.admins.create({ email, password, @@ -80,13 +98,19 @@ export function UserAuthForm({ className, ...props }: { className?: string }) { return (
-
+ setErrors({})}>
+
+ {/* honeypot */} + + +
{errors.email}

}
-
+ {isFirstRun && ( +
+ + + {errors?.passwordConfirm && ( +

{errors.passwordConfirm}

+ )} +
+ )}
@@ -152,11 +195,20 @@ export function UserAuthForm({ className, ...props }: { className?: string }) { Github - + - OAuth support coming soon - - OAuth / OpenID with all major providers should be available at 1.0.0. + OAuth 2 / OIDC support + +

+ Support for OAuth / OIDC (all major providers) will be available in the future. As + well as an option to disable password auth. +

+

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

+

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

+

Let me know if you have strong opinions either way.

diff --git a/site/src/main.tsx b/site/src/main.tsx index 99ce9b7..338a93d 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -7,7 +7,7 @@ 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 { buttonVariants } from './components/ui/button.tsx' -import { DatabaseBackupIcon, Github, LogOutIcon, LogsIcon, UserIcon } from 'lucide-react' +import { DatabaseBackupIcon, Github, LogOutIcon, LogsIcon, MailIcon, UserIcon } from 'lucide-react' import { useStore } from '@nanostores/react' import { Toaster } from './components/ui/toaster.tsx' import { Logo } from './components/logo.tsx'