mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 09:49:28 +08:00
user creation and other updates
This commit is contained in:
30
main.go
30
main.go
@@ -41,6 +41,20 @@ func main() {
|
|||||||
Automigrate: isGoRun,
|
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
|
// serve site
|
||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
switch isGoRun {
|
switch isGoRun {
|
||||||
@@ -95,11 +109,12 @@ func main() {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ssh key setup
|
||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
// create ssh key if it doesn't exist
|
// create ssh key if it doesn't exist
|
||||||
getSSHKey()
|
getSSHKey()
|
||||||
// api route to return public key
|
// 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)
|
requestData := apis.RequestInfo(c)
|
||||||
if requestData.Admin == nil {
|
if requestData.Admin == nil {
|
||||||
return apis.NewForbiddenError("Forbidden", nil)
|
return apis.NewForbiddenError("Forbidden", nil)
|
||||||
@@ -113,6 +128,19 @@ func main() {
|
|||||||
return nil
|
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
|
// start ticker for server updates
|
||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
go serverUpdateTicker()
|
go serverUpdateTicker()
|
||||||
|
@@ -78,7 +78,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "2hz5ncl8tizk5nx",
|
"id": "2hz5ncl8tizk5nx",
|
||||||
"created": "2024-07-07 16:08:20.979Z",
|
"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",
|
"name": "systems",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -155,9 +155,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [],
|
||||||
"CREATE UNIQUE INDEX ` + "`" + `idx_eggNgAn` + "`" + ` ON ` + "`" + `systems` + "`" + ` (` + "`" + `name` + "`" + `)"
|
|
||||||
],
|
|
||||||
"listRule": null,
|
"listRule": null,
|
||||||
"viewRule": null,
|
"viewRule": null,
|
||||||
"createRule": null,
|
"createRule": null,
|
||||||
|
@@ -43,7 +43,7 @@ export function AddServerButton() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// get public key
|
// get public key
|
||||||
pb.send('/getkey', {}).then(({ key }) => {
|
pb.send('/api/qoma/getkey', {}).then(({ key }) => {
|
||||||
$publicKey.set(key)
|
$publicKey.set(key)
|
||||||
})
|
})
|
||||||
}, [open])
|
}, [open])
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Database, Github, Home, Server } from 'lucide-react'
|
import {
|
||||||
|
Database,
|
||||||
|
DatabaseBackupIcon,
|
||||||
|
Github,
|
||||||
|
LayoutDashboard,
|
||||||
|
MailIcon,
|
||||||
|
Server,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
@@ -16,7 +23,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $servers, navigate } from '@/lib/stores'
|
import { $servers, navigate } from '@/lib/stores'
|
||||||
|
|
||||||
export default function () {
|
export default function CommandPalette() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const servers = useStore($servers)
|
const servers = useStore($servers)
|
||||||
|
|
||||||
@@ -39,24 +46,16 @@ export default function () {
|
|||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
<CommandGroup heading="Suggestions">
|
<CommandGroup heading="Suggestions">
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
keywords={['home']}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate('/')
|
navigate('/')
|
||||||
setOpen((open) => !open)
|
setOpen((open) => !open)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Home className="mr-2 h-4 w-4" />
|
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||||||
<span>Home</span>
|
<span>Dashboard</span>
|
||||||
<CommandShortcut>Page</CommandShortcut>
|
<CommandShortcut>Page</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem
|
|
||||||
onSelect={() => {
|
|
||||||
window.location.href = '/_/#/collections?collectionId=2hz5ncl8tizk5nx'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Database className="mr-2 h-4 w-4" />
|
|
||||||
<span>Admin UI</span>
|
|
||||||
<CommandShortcut>PocketBase</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
window.location.href = 'https://github.com/henrygd'
|
window.location.href = 'https://github.com/henrygd'
|
||||||
@@ -79,9 +78,42 @@ export default function () {
|
|||||||
>
|
>
|
||||||
<Server className="mr-2 h-4 w-4" />
|
<Server className="mr-2 h-4 w-4" />
|
||||||
<span>{server.name}</span>
|
<span>{server.name}</span>
|
||||||
|
<CommandShortcut>{server.host}</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</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>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
)
|
)
|
||||||
|
@@ -1,11 +1,19 @@
|
|||||||
import { UserAuthForm } from '@/components/user-auth-form'
|
import { UserAuthForm } from '@/components/user-auth-form'
|
||||||
import { Logo } from './logo'
|
import { Logo } from './logo'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { pb } from '@/lib/stores'
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
|
const [isFirstRun, setFirstRun] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Login / Qoma'
|
document.title = 'Login / Qoma'
|
||||||
|
|
||||||
|
pb.send('/api/qoma/first-run', {}).then(({ firstRun }) => {
|
||||||
|
setFirstRun(firstRun)
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen grid lg:max-w-none lg:px-0">
|
<div className="relative h-screen grid lg:max-w-none lg:px-0">
|
||||||
<div className="grid items-center py-12">
|
<div className="grid items-center py-12">
|
||||||
@@ -15,9 +23,11 @@ export default function () {
|
|||||||
<Logo className="h-7 fill-foreground mx-auto" />
|
<Logo className="h-7 fill-foreground mx-auto" />
|
||||||
<span className="sr-only">Qoma</span>
|
<span className="sr-only">Qoma</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">Please sign in to your account</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{isFirstRun ? 'Please create your admin account' : 'Please sign in to your account'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<UserAuthForm />
|
<UserAuthForm isFirstRun={isFirstRun} />
|
||||||
<p className="text-center text-sm opacity-70 hover:opacity-100 transition-opacity">
|
<p className="text-center text-sm opacity-70 hover:opacity-100 transition-opacity">
|
||||||
{/* todo: add forgot password section to readme and link to section
|
{/* todo: add forgot password section to readme and link to section
|
||||||
reset w/ command or link to pb reset */}
|
reset w/ command or link to pb reset */}
|
||||||
|
@@ -2,7 +2,7 @@ import { Suspense, lazy, useEffect } from 'react'
|
|||||||
// import { DataTable } from '../server-table/data-table'
|
// import { DataTable } from '../server-table/data-table'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
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 () {
|
export default function () {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -24,7 +24,7 @@ export default function () {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<DataTable />
|
<SystemsTable />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@@ -103,7 +103,7 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Ico
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function SystemsTable() {
|
||||||
const data = useStore($servers)
|
const data = useStore($servers)
|
||||||
// const [deleteServer, setDeleteServer] = useState({} as SystemRecord)
|
// const [deleteServer, setDeleteServer] = useState({} as SystemRecord)
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
@@ -135,7 +135,7 @@ export default function () {
|
|||||||
onClick={() => copyToClipboard(info.getValue() as string)}
|
onClick={() => copyToClipboard(info.getValue() as string)}
|
||||||
>
|
>
|
||||||
{info.getValue() as string}
|
{info.getValue() as string}
|
||||||
<CopyIcon className="h-3 w-3" />
|
<CopyIcon className="h-2.5 w-2.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react'
|
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 { Command as CommandPrimitive } from 'cmdk'
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react'
|
||||||
|
|
||||||
@@ -27,6 +27,9 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||||
|
<div className="sr-only">
|
||||||
|
<DialogTitle>Command</DialogTitle>
|
||||||
|
</div>
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
|
@@ -1,17 +1,8 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import * as React from 'react'
|
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 { cn } from '@/lib/utils'
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
// import { toast } from '@/components/ui/use-toast'
|
|
||||||
import { Github, LoaderCircle, LogInIcon } from 'lucide-react'
|
import { Github, LoaderCircle, LogInIcon } from 'lucide-react'
|
||||||
import { pb } from '@/lib/stores'
|
import { pb } from '@/lib/stores'
|
||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
@@ -25,14 +16,34 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
const LoginSchema = v.object({
|
const honeypot = v.literal('')
|
||||||
email: v.pipe(v.string(), v.email('Invalid email address.')),
|
const emailSchema = 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 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<typeof LoginSchema> // { 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<boolean>(false)
|
const [isLoading, setIsLoading] = React.useState<boolean>(false)
|
||||||
const [isGitHubLoading, setIsGitHubLoading] = React.useState<boolean>(false)
|
const [isGitHubLoading, setIsGitHubLoading] = React.useState<boolean>(false)
|
||||||
const [errors, setErrors] = React.useState<Record<string, string | undefined>>({})
|
const [errors, setErrors] = React.useState<Record<string, string | undefined>>({})
|
||||||
@@ -45,8 +56,10 @@ export function UserAuthForm({ className, ...props }: { className?: string }) {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData(e.target as HTMLFormElement)
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
const data = Object.fromEntries(formData) as Record<string, any>
|
const data = Object.fromEntries(formData) as Record<string, any>
|
||||||
const result = v.safeParse(LoginSchema, data)
|
const Schema = isFirstRun ? RegisterSchema : LoginSchema
|
||||||
|
const result = v.safeParse(Schema, data)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
console.log(result)
|
||||||
let errors = {}
|
let errors = {}
|
||||||
for (const issue of result.issues) {
|
for (const issue of result.issues) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -55,9 +68,14 @@ export function UserAuthForm({ className, ...props }: { className?: string }) {
|
|||||||
setErrors(errors)
|
setErrors(errors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { email, password } = result.output
|
const { email, password, passwordConfirm } = result.output
|
||||||
let firstRun = true
|
if (isFirstRun) {
|
||||||
if (firstRun) {
|
// check that passwords match
|
||||||
|
if (password !== passwordConfirm) {
|
||||||
|
let msg = 'Passwords do not match'
|
||||||
|
setErrors({ passwordConfirm: msg })
|
||||||
|
return
|
||||||
|
}
|
||||||
await pb.admins.create({
|
await pb.admins.create({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
@@ -80,13 +98,19 @@ export function UserAuthForm({ className, ...props }: { className?: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('grid gap-6', className)} {...props}>
|
<div className={cn('grid gap-6', className)} {...props}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit} onChange={() => setErrors({})}>
|
||||||
<div className="grid gap-2.5">
|
<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">
|
<div className="grid gap-1">
|
||||||
<Label className="sr-only" htmlFor="email">
|
<Label className="sr-only" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
autoFocus={true}
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
required
|
required
|
||||||
@@ -100,8 +124,8 @@ export function UserAuthForm({ className, ...props }: { className?: string }) {
|
|||||||
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email}</p>}
|
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label className="sr-only" htmlFor="email">
|
<Label className="sr-only" htmlFor="pass">
|
||||||
Email
|
Password
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="pass"
|
id="pass"
|
||||||
@@ -114,13 +138,32 @@ export function UserAuthForm({ className, ...props }: { className?: string }) {
|
|||||||
/>
|
/>
|
||||||
{errors?.password && <p className="px-1 text-xs text-red-600">{errors.password}</p>}
|
{errors?.password && <p className="px-1 text-xs text-red-600">{errors.password}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
{isFirstRun && (
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="sr-only" htmlFor="pass2">
|
||||||
|
Confirm password
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="pass2"
|
||||||
|
name="passwordConfirm"
|
||||||
|
placeholder="confirm password"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
disabled={isLoading || isGitHubLoading}
|
||||||
|
/>
|
||||||
|
{errors?.passwordConfirm && (
|
||||||
|
<p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<button className={cn(buttonVariants())} disabled={isLoading}>
|
<button className={cn(buttonVariants())} disabled={isLoading}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<LogInIcon className="mr-2 h-4 w-4" />
|
<LogInIcon className="mr-2 h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
Sign In
|
{isFirstRun ? 'Create account' : 'Sign in'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -152,11 +195,20 @@ export function UserAuthForm({ className, ...props }: { className?: string }) {
|
|||||||
Github
|
Github
|
||||||
</button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent style={{ maxWidth: 440, width: '90%' }}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>OAuth support coming soon</DialogTitle>
|
<DialogTitle className="mb-2">OAuth 2 / OIDC support</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription className="grid gap-3">
|
||||||
OAuth / OpenID with all major providers should be available at 1.0.0.
|
<p>
|
||||||
|
Support for OAuth / OIDC (all major providers) will be available in the future. As
|
||||||
|
well as an option to disable password auth.
|
||||||
|
</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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
@@ -7,7 +7,7 @@ import { $authenticated, $router, $servers, navigate, pb } from './lib/stores.ts
|
|||||||
import { ModeToggle } from './components/mode-toggle.tsx'
|
import { ModeToggle } from './components/mode-toggle.tsx'
|
||||||
import { cn, updateFavicon, updateServerList } from './lib/utils.ts'
|
import { cn, updateFavicon, updateServerList } from './lib/utils.ts'
|
||||||
import { buttonVariants } from './components/ui/button.tsx'
|
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 { useStore } from '@nanostores/react'
|
||||||
import { Toaster } from './components/ui/toaster.tsx'
|
import { Toaster } from './components/ui/toaster.tsx'
|
||||||
import { Logo } from './components/logo.tsx'
|
import { Logo } from './components/logo.tsx'
|
||||||
|
Reference in New Issue
Block a user