option to disable password login

This commit is contained in:
Henry Dollman
2024-07-17 21:50:57 -04:00
parent 9f11c021ce
commit 8fee50d07c
10 changed files with 190 additions and 144 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
linguist-language=Go

30
main.go
View File

@@ -23,7 +23,6 @@ import (
"github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/pocketbase/pocketbase/tools/cron" "github.com/pocketbase/pocketbase/tools/cron"
"github.com/pocketbase/pocketbase/tools/mailer" "github.com/pocketbase/pocketbase/tools/mailer"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
@@ -38,10 +37,31 @@ func main() {
// loosely check if it was executed using "go run" // loosely check if it was executed using "go run"
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir()) isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
// enable auto creation of migration files when making collection changes in the Admin UI // // enable auto creation of migration files when making collection changes in the Admin UI
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{ // migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
// (the isGoRun check is to enable it only during development) // // (the isGoRun check is to enable it only during development)
Automigrate: isGoRun, // Automigrate: isGoRun,
// })
// set auth settings
app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
usersCollection, err := app.Dao().FindCollectionByNameOrId("users")
if err != nil {
return err
}
usersAuthOptions := usersCollection.AuthOptions()
if os.Getenv("DISABLE_PASSWORD_AUTH") == "true" {
usersAuthOptions.AllowEmailAuth = false
usersAuthOptions.AllowUsernameAuth = false
} else {
usersAuthOptions.AllowEmailAuth = true
usersAuthOptions.AllowUsernameAuth = true
}
usersCollection.SetOptions(usersAuthOptions)
if err := app.Dao().SaveCollection(usersCollection); err != nil {
return err
}
return nil
}) })
// serve site // serve site

View File

@@ -266,13 +266,13 @@ func init() {
"options": { "options": {
"allowEmailAuth": true, "allowEmailAuth": true,
"allowOAuth2Auth": true, "allowOAuth2Auth": true,
"allowUsernameAuth": true, "allowUsernameAuth": false,
"exceptEmailDomains": null, "exceptEmailDomains": null,
"manageRule": null, "manageRule": null,
"minPasswordLength": 8, "minPasswordLength": 8,
"onlyEmailDomains": null, "onlyEmailDomains": null,
"onlyVerified": false, "onlyVerified": true,
"requireEmail": false "requireEmail": true
} }
}, },
{ {

View File

@@ -29,6 +29,12 @@ The hub and agent are distributed as single binary files, as well as docker imag
### Binary ### Binary
## Environment Variables
| Name | Default | Description |
| ----------------------- | ------- | ------------------------------------------------- |
| `DISABLE_PASSWORD_AUTH` | unset | Disables password authentication if set to `true` |
## OAuth / OIDC integration ## OAuth / OIDC integration
Beszel supports OpenID Connect and many OAuth2 authentication providers (see list below). To enable this, you will need to: Beszel supports OpenID Connect and many OAuth2 authentication providers (see list below). To enable this, you will need to:
@@ -64,7 +70,7 @@ Beszel supports OpenID Connect and many OAuth2 authentication providers (see lis
- Yandex - Yandex
</details> </details>
## API ## REST API
Because Beszel is built on top of PocketBase, you can use the normal PocketBase API to read or update your data in your own applications. Because Beszel is built on top of PocketBase, you can use the normal PocketBase API to read or update your data in your own applications.

View File

@@ -1 +1 @@
<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> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 70"><path fill="#22c55e" d="M70 52.2v2.4a15.6 15.6 0 0 1-.3 2.8 20.5 20.5 0 0 1-.5 2.3q-.8 2.7-2.7 5a14 14 0 0 1-3.2 2.9 17.2 17.2 0 0 1-1.5.9q-3 1.5-7.2 1.5H6.4a6.7 6.7 0 0 1-2-.2 6.1 6.1 0 0 1-.5-.3 6.2 6.2 0 0 1-2-1.3 6.2 6.2 0 0 1-1.4-2A6.4 6.4 0 0 1 0 64a7.5 7.5 0 0 1 0-.4V6.4a6.4 6.4 0 0 1 .5-2.5Q1 2.7 1.8 2a6 6 0 0 1 2-1.4A6.4 6.4 0 0 1 6.2 0a7.5 7.5 0 0 1 .3 0h42.5a15.2 15.2 0 0 1 2.7.3 20 20 0 0 1 2.3.5q2.7.9 5 2.7a14 14 0 0 1 3 3.4 17 17 0 0 1 .9 1.4q1.5 2.9 1.5 7.1v2.4a23.2 23.2 0 0 1-.3 4 30.7 30.7 0 0 1-.8 3.3 23.9 23.9 0 0 1-3.5 7.1 26.8 26.8 0 0 1-.1.2 22.4 22.4 0 0 1 4 3.2 20.1 20.1 0 0 1 3 3.8 22.6 22.6 0 0 1 .3.5 21.5 21.5 0 0 1 1.7 3.6 26 26 0 0 1 .5 2 23.8 23.8 0 0 1 .7 3.9 30 30 0 0 1 .2 2.8Zm-12.7 2.3v-2.3a13.6 13.6 0 0 0-.2-2.5 10.7 10.7 0 0 0-.6-2 10 10 0 0 0-1.8-2.9 9.4 9.4 0 0 0-.4-.5 9.4 9.4 0 0 0-3.1-2 11 11 0 0 0-.3-.1 11.6 11.6 0 0 0-2.7-.7 14.7 14.7 0 0 0-1.8 0H17.8V28.5h22.9a14.1 14.1 0 0 0 2.5-.2 11.2 11.2 0 0 0 2-.5 9.7 9.7 0 0 0 2.7-1.6 9 9 0 0 0 .7-.6 9.5 9.5 0 0 0 2.1-3.3 11 11 0 0 0 0-.1 11.3 11.3 0 0 0 .7-2.6 14.6 14.6 0 0 0 .1-1.9v-2.4q0-2.7-2.6-2.7H12.7v44.6h41.9a6 6 0 0 0 .3 0h.5a2 2 0 0 0 .7-.2 2 2 0 0 0 .2-.1 1.5 1.5 0 0 0 .3-.3l.4-.5.3-1a6 6 0 0 0 0-.7Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.7 81.8"><path fill="#dc2626" 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 70 70"><path fill="#dc2626" d="M70 52.2v2.4a15.6 15.6 0 0 1-.3 2.8 20.5 20.5 0 0 1-.5 2.3q-.8 2.7-2.7 5a14 14 0 0 1-3.2 2.9 17.2 17.2 0 0 1-1.5.9q-3 1.5-7.2 1.5H6.4a6.7 6.7 0 0 1-2-.2 6.1 6.1 0 0 1-.5-.3 6.2 6.2 0 0 1-2-1.3 6.2 6.2 0 0 1-1.4-2A6.4 6.4 0 0 1 0 64a7.5 7.5 0 0 1 0-.4V6.4a6.4 6.4 0 0 1 .5-2.5Q1 2.7 1.8 2a6 6 0 0 1 2-1.4A6.4 6.4 0 0 1 6.2 0a7.5 7.5 0 0 1 .3 0h42.5a15.2 15.2 0 0 1 2.7.3 20 20 0 0 1 2.3.5q2.7.9 5 2.7a14 14 0 0 1 3 3.4 17 17 0 0 1 .9 1.4q1.5 2.9 1.5 7.1v2.4a23.2 23.2 0 0 1-.3 4 30.7 30.7 0 0 1-.8 3.3 23.9 23.9 0 0 1-3.5 7.1 26.8 26.8 0 0 1-.1.2 22.4 22.4 0 0 1 4 3.2 20.1 20.1 0 0 1 3 3.8 22.6 22.6 0 0 1 .3.5 21.5 21.5 0 0 1 1.7 3.6 26 26 0 0 1 .5 2 23.8 23.8 0 0 1 .7 3.9 30 30 0 0 1 .2 2.8Zm-12.7 2.3v-2.3a13.6 13.6 0 0 0-.2-2.5 10.7 10.7 0 0 0-.6-2 10 10 0 0 0-1.8-2.9 9.4 9.4 0 0 0-.4-.5 9.4 9.4 0 0 0-3.1-2 11 11 0 0 0-.3-.1 11.6 11.6 0 0 0-2.7-.7 14.7 14.7 0 0 0-1.8 0H17.8V28.5h22.9a14.1 14.1 0 0 0 2.5-.2 11.2 11.2 0 0 0 2-.5 9.7 9.7 0 0 0 2.7-1.6 9 9 0 0 0 .7-.6 9.5 9.5 0 0 0 2.1-3.3 11 11 0 0 0 0-.1 11.3 11.3 0 0 0 .7-2.6 14.6 14.6 0 0 0 .1-1.9v-2.4q0-2.7-2.6-2.7H12.7v44.6h41.9a6 6 0 0 0 .3 0h.5a2 2 0 0 0 .7-.2 2 2 0 0 0 .2-.1 1.5 1.5 0 0 0 .3-.3l.4-.5.3-1a6 6 0 0 0 0-.7Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.7 81.8"><path fill="#888" 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 70 70"><path fill="#888" d="M70 52.2v2.4a15.6 15.6 0 0 1-.3 2.8 20.5 20.5 0 0 1-.5 2.3q-.8 2.7-2.7 5a14 14 0 0 1-3.2 2.9 17.2 17.2 0 0 1-1.5.9q-3 1.5-7.2 1.5H6.4a6.7 6.7 0 0 1-2-.2 6.1 6.1 0 0 1-.5-.3 6.2 6.2 0 0 1-2-1.3 6.2 6.2 0 0 1-1.4-2A6.4 6.4 0 0 1 0 64a7.5 7.5 0 0 1 0-.4V6.4a6.4 6.4 0 0 1 .5-2.5Q1 2.7 1.8 2a6 6 0 0 1 2-1.4A6.4 6.4 0 0 1 6.2 0a7.5 7.5 0 0 1 .3 0h42.5a15.2 15.2 0 0 1 2.7.3 20 20 0 0 1 2.3.5q2.7.9 5 2.7a14 14 0 0 1 3 3.4 17 17 0 0 1 .9 1.4q1.5 2.9 1.5 7.1v2.4a23.2 23.2 0 0 1-.3 4 30.7 30.7 0 0 1-.8 3.3 23.9 23.9 0 0 1-3.5 7.1 26.8 26.8 0 0 1-.1.2 22.4 22.4 0 0 1 4 3.2 20.1 20.1 0 0 1 3 3.8 22.6 22.6 0 0 1 .3.5 21.5 21.5 0 0 1 1.7 3.6 26 26 0 0 1 .5 2 23.8 23.8 0 0 1 .7 3.9 30 30 0 0 1 .2 2.8Zm-12.7 2.3v-2.3a13.6 13.6 0 0 0-.2-2.5 10.7 10.7 0 0 0-.6-2 10 10 0 0 0-1.8-2.9 9.4 9.4 0 0 0-.4-.5 9.4 9.4 0 0 0-3.1-2 11 11 0 0 0-.3-.1 11.6 11.6 0 0 0-2.7-.7 14.7 14.7 0 0 0-1.8 0H17.8V28.5h22.9a14.1 14.1 0 0 0 2.5-.2 11.2 11.2 0 0 0 2-.5 9.7 9.7 0 0 0 2.7-1.6 9 9 0 0 0 .7-.6 9.5 9.5 0 0 0 2.1-3.3 11 11 0 0 0 0-.1 11.3 11.3 0 0 0 .7-2.6 14.6 14.6 0 0 0 .1-1.9v-2.4q0-2.7-2.6-2.7H12.7v44.6h41.9a6 6 0 0 0 .3 0h.5a2 2 0 0 0 .7-.2 2 2 0 0 0 .2-.1 1.5 1.5 0 0 0 .3-.3l.4-.5.3-1a6 6 0 0 0 0-.7Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -10,12 +10,11 @@ import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogTrigger, DialogTrigger,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { useEffect, useState } from 'react' import { useState } from 'react'
import { AuthProviderInfo } from 'pocketbase' import { AuthMethodsList } from 'pocketbase'
import { Link } from '../router' import { Link } from '../router'
const honeypot = v.literal('') const honeypot = v.literal('')
@@ -57,26 +56,16 @@ const showLoginFaliedToast = () => {
export function UserAuthForm({ export function UserAuthForm({
className, className,
isFirstRun, isFirstRun,
authMethods,
...props ...props
}: { }: {
className?: string className?: string
isFirstRun: boolean isFirstRun: boolean
authMethods: AuthMethodsList
}) { }) {
const [isLoading, setIsLoading] = useState<boolean>(false) const [isLoading, setIsLoading] = useState<boolean>(false)
const [isGitHubLoading, setIsOauthLoading] = useState<boolean>(false) const [isGitHubLoading, setIsOauthLoading] = useState<boolean>(false)
const [errors, setErrors] = useState<Record<string, string | undefined>>({}) const [errors, setErrors] = useState<Record<string, string | undefined>>({})
const [authProviders, setAuthProviders] = useState<AuthProviderInfo[]>([])
useEffect(() => {
pb.collection('users')
.listAuthMethods()
.then((methods) => {
console.log('methods', methods)
console.log('password active', methods.emailPassword)
setAuthProviders(methods.authProviders)
console.log('auth providers', authProviders)
})
}, [])
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault() e.preventDefault()
@@ -130,120 +119,133 @@ export function UserAuthForm({
} }
} }
if (!authMethods) {
return null
}
return ( return (
<div className={cn('grid gap-6', className)} {...props}> <div className={cn('grid gap-6', className)} {...props}>
<form onSubmit={handleSubmit} onChange={() => setErrors({})}> {authMethods.emailPassword && (
<div className="grid gap-2.5"> <>
{isFirstRun && ( <form onSubmit={handleSubmit} onChange={() => setErrors({})}>
<div className="grid gap-1 relative"> <div className="grid gap-2.5">
<UserIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> {isFirstRun && (
<Label className="sr-only" htmlFor="username"> <div className="grid gap-1 relative">
Username <UserIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
</Label> <Label className="sr-only" htmlFor="username">
<Input Username
autoFocus={true} </Label>
id="username" <Input
name="username" autoFocus={true}
required id="username"
placeholder="username" name="username"
type="username" required
autoCapitalize="none" placeholder="username"
autoComplete="username" type="username"
autoCorrect="off" autoCapitalize="none"
disabled={isLoading || isGitHubLoading} autoComplete="username"
className="pl-9" autoCorrect="off"
/> disabled={isLoading || isGitHubLoading}
{errors?.username && <p className="px-1 text-xs text-red-600">{errors.username}</p>} className="pl-9"
</div> />
)} {errors?.username && (
<div className="grid gap-1 relative"> <p className="px-1 text-xs text-red-600">{errors.username}</p>
<MailIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> )}
<Label className="sr-only" htmlFor="email"> </div>
Email
</Label>
<Input
id="email"
name="email"
required
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 relative">
<LockIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Label className="sr-only" htmlFor="pass">
Password
</Label>
<Input
id="pass"
name="password"
placeholder="password"
required
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 relative">
<LockIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<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}
className="pl-9"
/>
{errors?.passwordConfirm && (
<p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>
)} )}
<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
id="email"
name="email"
required
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 relative">
<LockIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Label className="sr-only" htmlFor="pass">
Password
</Label>
<Input
id="pass"
name="password"
placeholder="password"
required
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 relative">
<LockIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<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}
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" />
) : (
<LogInIcon className="mr-2 h-4 w-4" />
)}
{isFirstRun ? 'Create account' : 'Sign in'}
</button>
</div>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
</div> </div>
)}
<div className="sr-only">
{/* honeypot */}
<label htmlFor="name"></label>
<input id="name" type="text" name="name" tabIndex={-1} />
</div> </div>
<button className={cn(buttonVariants())} disabled={isLoading}> </>
{isLoading ? ( )}
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
) : (
<LogInIcon className="mr-2 h-4 w-4" />
)}
{isFirstRun ? 'Create account' : 'Sign in'}
</button>
</div>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
</div>
</div>
{authProviders.length > 0 && ( {authMethods.authProviders.length > 0 && (
<div className="grid gap-2"> <div className="grid gap-2 -mt-1">
{authProviders.map((provider) => ( {authMethods.authProviders.map((provider) => (
<button <button
key={provider.name} key={provider.name}
type="button" type="button"
className={cn(buttonVariants({ variant: 'outline' }))} className={cn(buttonVariants({ variant: 'outline' }), {
'justify-self-center': !authMethods.emailPassword,
'px-5': !authMethods.emailPassword,
})}
onClick={async () => { onClick={async () => {
setIsOauthLoading(true) setIsOauthLoading(true)
try { try {
@@ -275,7 +277,7 @@ export function UserAuthForm({
</div> </div>
)} )}
{!authProviders.length && ( {!authMethods.authProviders.length && (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<button type="button" className={cn(buttonVariants({ variant: 'outline' }))}> <button type="button" className={cn(buttonVariants({ variant: 'outline' }))}>
@@ -303,12 +305,15 @@ export function UserAuthForm({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
<Link
href="/forgot-password" {authMethods.emailPassword && (
className="text-sm mx-auto mt-2 hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity" <Link
> href="/forgot-password"
Forgot password? className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
</Link> >
Forgot password?
</Link>
)}
</div> </div>
) )
} }

View File

@@ -1,4 +1,4 @@
import { LoaderCircle, MailIcon, SendIcon } from 'lucide-react' import { LoaderCircle, MailIcon, SendHorizonalIcon } from 'lucide-react'
import { Input } from '../ui/input' import { Input } from '../ui/input'
import { Label } from '../ui/label' import { Label } from '../ui/label'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
@@ -70,7 +70,7 @@ export default function ForgotPassword() {
{isLoading ? ( {isLoading ? (
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" /> <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
) : ( ) : (
<SendIcon className="mr-2 h-4 w-4" /> <SendHorizonalIcon className="mr-2 h-4 w-4" />
)} )}
Reset password Reset password
</button> </button>
@@ -78,7 +78,7 @@ export default function ForgotPassword() {
</form> </form>
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<button className="text-sm mx-auto mt-2 hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"> <button className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity">
Command line instructions Command line instructions
</button> </button>
</DialogTrigger> </DialogTrigger>

View File

@@ -5,10 +5,12 @@ import { pb } from '@/lib/stores'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import ForgotPassword from './forgot-pass-form' import ForgotPassword from './forgot-pass-form'
import { $router } from '../router' import { $router } from '../router'
import { AuthMethodsList } from 'pocketbase'
export default function () { export default function () {
const page = useStore($router) const page = useStore($router)
const [isFirstRun, setFirstRun] = useState(false) const [isFirstRun, setFirstRun] = useState(false)
const [authMethods, setAuthMethods] = useState<AuthMethodsList>()
useEffect(() => { useEffect(() => {
document.title = 'Login / Beszel' document.title = 'Login / Beszel'
@@ -18,6 +20,14 @@ export default function () {
}) })
}, []) }, [])
useEffect(() => {
pb.collection('users')
.listAuthMethods()
.then((methods) => {
setAuthMethods(methods)
})
}, [])
const subtitle = useMemo(() => { const subtitle = useMemo(() => {
if (isFirstRun) { if (isFirstRun) {
return 'Please create an admin account' return 'Please create an admin account'
@@ -28,9 +38,13 @@ export default function () {
} }
}, [isFirstRun, page]) }, [isFirstRun, page])
if (!authMethods) {
return null
}
return ( return (
<div className="min-h-screen grid items-center py-12"> <div className="min-h-screen grid items-center py-12">
<div className="grid gap-5 w-full px-4 max-w-[22em] mx-auto"> <div className="grid gap-5 w-full px-4 mx-auto" style={{ maxWidth: '22em' }}>
<div className="text-center"> <div className="text-center">
<h1 className="mb-3"> <h1 className="mb-3">
<Logo className="h-7 fill-foreground mx-auto" /> <Logo className="h-7 fill-foreground mx-auto" />
@@ -41,7 +55,7 @@ export default function () {
{page?.path === '/forgot-password' ? ( {page?.path === '/forgot-password' ? (
<ForgotPassword /> <ForgotPassword />
) : ( ) : (
<UserAuthForm isFirstRun={isFirstRun} /> <UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} />
)} )}
</div> </div>
</div> </div>