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')} )}
) }