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, UserIcon } 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, useState } from "react" import { AuthMethodsList, OAuth2AuthConfig } from "pocketbase" import { Link } from "../router" import { useTranslation } from "react-i18next" 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({ name: honeypot, email: emailSchema, password: passwordSchema, }) const RegisterSchema = v.looseObject({ 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, }) const showLoginFaliedToast = () => { toast({ title: "Login attempt failed", description: "Please check your credentials and try again", variant: "destructive", }) } export function UserAuthForm({ className, isFirstRun, authMethods, ...props }: { className?: string isFirstRun: boolean authMethods: AuthMethodsList }) { const { t } = useTranslation() 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) 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 { email, password, passwordConfirm, username } = 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, passwordConfirm: password, }) await pb.admins.authWithPassword(email, password) await pb.collection("users").create({ username, email, password, passwordConfirm: password, role: "admin", verified: true, }) await pb.collection("users").authWithPassword(email, password) } else { await pb.collection("users").authWithPassword(email, password) } $authenticated.set(true) } catch (e) { showLoginFaliedToast() } finally { setIsLoading(false) } }, [isFirstRun] ) if (!authMethods) { return null } return (
{authMethods.emailPassword && ( <>
setErrors({})}>
{isFirstRun && (
{errors?.username &&

{errors.username}

}
)}
{errors?.email &&

{errors.email}

}
{errors?.password &&

{errors.password}

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

{errors.passwordConfirm}

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

{t("auth.openid_des")}

{t("please_view_the")}{" "} GitHub README {" "} {t("for_instructions")}

)} {authMethods.emailPassword && !isFirstRun && ( {t("auth.forgot_password")} )}
) }