import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { useLocation, Link } from "wouter"; import { zodResolver } from "@hookform/resolvers/zod"; import { insertUserSchema, InsertUser, loginSchema, registerSchema, LoginUser } from "@shared/schema"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { useTranslation } from "react-i18next"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Checkbox } from "@/components/ui/checkbox"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { BrainCircuit } from "lucide-react"; export default function AuthPage() { const { t } = useTranslation(); const { toast } = useToast(); const [, setLocation] = useLocation(); const queryClient = useQueryClient(); const [activeTab, setActiveTab] = useState("login"); // 2FA State const [is2FARequired, setIs2FARequired] = useState(false); const [twoFAUserId, setTwoFAUserId] = useState(null); const [twoFAEmail, setTwoFAEmail] = useState(null); const [otpCode, setOtpCode] = useState(""); const { data: settings } = useQuery<{ registration_enabled: boolean }>({ queryKey: ["/api/settings/public"], queryFn: async () => { const res = await fetch("/api/settings/public"); if (!res.ok) { throw new Error("Failed to fetch settings"); } return res.json(); }, }); const loginMutation = useMutation({ mutationFn: async (data: LoginUser) => { const res = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); // Handle 200 OK could be user or 2fa_required const json = await res.json(); if (!res.ok) { throw new Error(json.message || "Invalid username or password"); } return json; }, onSuccess: (data) => { if (data.message === "2fa_required") { setIs2FARequired(true); setTwoFAUserId(data.userId); setTwoFAEmail(data.email); toast({ title: t('auth.2faCodeSent'), description: t('auth.checkEmail') }); } else { queryClient.setQueryData(["/api/user"], data); queryClient.invalidateQueries({ queryKey: ["/api/user"] }); toast({ title: "Welcome back!" }); setLocation('/'); } }, onError: (error: Error) => { toast({ title: "Login failed", description: error.message, variant: "destructive", }); }, }); const verify2FAMutation = useMutation({ mutationFn: async (data: { userId: string, code: string }) => { const res = await fetch("/api/auth/verify-2fa", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error(await res.text()); return res.json(); }, onSuccess: (user) => { queryClient.setQueryData(["/api/user"], user); queryClient.invalidateQueries({ queryKey: ["/api/user"] }); toast({ title: "Verification Successful", description: "Welcome back!" }); setLocation('/'); }, onError: (error: Error) => { toast({ title: "Verification Failed", description: error.message, variant: "destructive", }); } }); const handleVerify2FA = (e: React.FormEvent) => { e.preventDefault(); if (twoFAUserId && otpCode) { verify2FAMutation.mutate({ userId: twoFAUserId, code: otpCode }); } }; const registerMutation = useMutation({ mutationFn: async (data: InsertUser) => { const res = await fetch("/api/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) { const text = await res.text(); throw new Error(text || "Registration failed"); } return res.json(); }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ["/api/user"] }); queryClient.setQueryData(["/api/user"], data); toast({ title: "Account created!" }); setLocation('/'); }, // Error handling is done in the form submission handler to set field errors }); return (
Logo

{t('auth.heroTitle')}

{t('auth.heroSubtitle')}

{is2FARequired ? (
Logo
{t('auth.2faVerification')} {t('auth.enterCodeSentTo')} {twoFAUserId && twoFAEmail ? twoFAEmail : 'your email'}
setOtpCode(e.target.value.replace(/\D/g, ''))} />

{t('auth.codeExpiresIn10')}

) : (
Logo
{t('auth.welcomeBack')} {settings?.registration_enabled ? t('auth.signInDesc') : t('auth.signInDescNoReg')}
{settings?.registration_enabled && ( )}
{activeTab === "login" ? ( loginMutation.mutate(data)} isLoading={loginMutation.isPending} /> ) : ( settings?.registration_enabled ? ( { registerMutation.mutate(data as InsertUser, { onError: (error) => { // Handled in form } }) }} isLoading={registerMutation.isPending} registerMutation={registerMutation} /> ) : (
{t('auth.registrationDisabled')}
) )}
)}
); } function AuthForm({ mode, onSubmit, isLoading, registerMutation, }: { mode: "login" | "register"; onSubmit: (data: InsertUser | LoginUser) => void; isLoading: boolean; registerMutation?: any; // Type accurately if possible, but 'any' for quick fix avoids generic complexities }) { const { t } = useTranslation(); const { toast } = useToast(); const form = useForm({ resolver: zodResolver(mode === "login" ? loginSchema : registerSchema), defaultValues: { username: "", email: "", password: "", }, }); const handleSubmit = (data: InsertUser | LoginUser) => { if (mode === 'register' && registerMutation) { registerMutation.mutate(data, { onError: (error: Error) => { const msg = error.message.toLowerCase(); if (msg.includes("username")) { form.setError("username", { type: "manual", message: "Username already exists" }); } else if (msg.includes("email")) { form.setError("email", { type: "manual", message: "Email already exists" }); } else { toast({ title: "Registration failed", description: error.message, variant: "destructive", }) } } }); } else { onSubmit(data); } }; return (
( {mode === 'login' ? t('auth.usernameOrEmail') : t('auth.username')} )} /> {mode === "register" && ( ( {t('auth.email')} )} /> )} ( {t('auth.password')} )} /> {mode === "login" && (
( {t("auth.rememberMe")} )} />
)} ); }