import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react" import { $authenticated, pb } from "@/lib/stores" import * as v from "valibot" import { toast } from "../ui/use-toast" import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { useCallback, useEffect, useState } from "react" import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase" import { $router, Link, prependBasePath } from "../router" import { Trans, t } from "@lingui/macro" import { getPagePath } from "@nanostores/router" const honeypot = v.literal("") const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`)) const passwordSchema = v.pipe( v.string(), v.minLength(8, t`Password must be at least 8 characters.`), v.maxBytes(72, t`Password must be less than 72 bytes.`) ) const LoginSchema = v.looseObject({ name: honeypot, email: emailSchema, password: passwordSchema, }) const RegisterSchema = v.looseObject({ name: honeypot, email: emailSchema, password: passwordSchema, passwordConfirm: passwordSchema, }) const showLoginFaliedToast = () => { toast({ title: t`Login attempt failed`, description: t`Please check your credentials and try again`, variant: "destructive", }) } export function UserAuthForm({ className, isFirstRun, authMethods, ...props }: { className?: string isFirstRun: boolean authMethods: AuthMethodsList }) { const [isLoading, setIsLoading] = useState(false) const [isOauthLoading, setIsOauthLoading] = useState(false) const [errors, setErrors] = useState>({}) const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault() setIsLoading(true) // store email for later use if mfa is enabled let email = "" try { const formData = new FormData(e.target as HTMLFormElement) const data = Object.fromEntries(formData) as Record 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 errors[issue.path[0].key] = issue.message } setErrors(errors) return } const { password, passwordConfirm } = result.output email = result.output.email if (isFirstRun) { // check that passwords match if (password !== passwordConfirm) { let msg = "Passwords do not match" setErrors({ passwordConfirm: msg }) return } await pb.send("/api/beszel/create-user", { method: "POST", body: JSON.stringify({ email, password }), }) await pb.collection("users").authWithPassword(email, password) } else { await pb.collection("users").authWithPassword(email, password) } $authenticated.set(true) } catch (err: any) { showLoginFaliedToast() // todo: implement MFA // const mfaId = err.response?.mfaId // if (!mfaId) { // showLoginFaliedToast() // throw err // } // the user needs to authenticate again with another auth method, for example OTP // const result = await pb.collection("users").requestOTP(email) // ... show a modal for users to check their email and to enter the received code ... // await pb.collection("users").authWithOTP(result.otpId, "EMAIL_CODE", { mfaId: mfaId }) } finally { setIsLoading(false) } }, [isFirstRun] ) if (!authMethods) { return null } const authProviders = authMethods.oauth2.providers ?? [] const oauthEnabled = authMethods.oauth2.enabled && authProviders.length > 0 const passwordEnabled = authMethods.password.enabled function loginWithOauth(provider: AuthProviderInfo, forcePopup = false) { setIsOauthLoading(true) const oAuthOpts: OAuth2AuthConfig = { provider: provider.name, } // https://github.com/pocketbase/pocketbase/discussions/2429#discussioncomment-5943061 if (forcePopup || navigator.userAgent.match(/iPhone|iPad|iPod/i)) { const authWindow = window.open() if (!authWindow) { setIsOauthLoading(false) toast({ title: t`Error`, description: t`Please enable pop-ups for this site`, variant: "destructive", }) return } oAuthOpts.urlCallback = (url) => { authWindow.location.href = url } } pb.collection("users") .authWithOAuth2(oAuthOpts) .then(() => { $authenticated.set(pb.authStore.isValid) }) .catch(showLoginFaliedToast) .finally(() => { setIsOauthLoading(false) }) } useEffect(() => { // auto login if password disabled and only one auth provider if (!passwordEnabled && authProviders.length === 1) { loginWithOauth(authProviders[0], true) } }, []) return (
{passwordEnabled && ( <>
setErrors({})}>
{errors?.email &&

{errors.email}

}
{errors?.password &&

{errors.password}

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

{errors.passwordConfirm}

}
)}
{/* honeypot */}
{(isFirstRun || oauthEnabled) && ( // only show 'continue with' during onboarding or if we have auth providers
Or continue with
)} )} {oauthEnabled && (
{authMethods.oauth2.providers.map((provider) => ( ))}
)} {!oauthEnabled && isFirstRun && ( // only show GitHub button / dialog during onboarding OAuth 2 / OIDC support

Beszel supports OpenID Connect and many OAuth2 authentication providers.

Please see{" "} the documentation {" "} for instructions.

)} {passwordEnabled && !isFirstRun && ( Forgot password? )}
) }