import { LoginForm } from "@/components/auth/login-form"; import { GithubIcon } from "@/components/icons/github"; import { GoogleIcon } from "@/components/icons/google"; import { MicrosoftIcon } from "@/components/icons/microsoft"; import { OAuthIcon } from "@/components/icons/oauth"; import { PocketIDIcon } from "@/components/icons/pocket-id"; import { TailscaleIcon } from "@/components/icons/tailscale"; import { Button } from "@/components/ui/button"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, } from "@/components/ui/card"; import { OAuthButton } from "@/components/ui/oauth-button"; import { SeperatorWithChildren } from "@/components/ui/separator"; import { useAppContext } from "@/context/app-context"; import { useUserContext } from "@/context/user-context"; import { useOIDCParams } from "@/lib/hooks/oidc"; import { LoginSchema } from "@/schemas/login-schema"; import { useMutation } from "@tanstack/react-query"; import axios, { AxiosError } from "axios"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router"; import { toast } from "sonner"; const iconMap: Record = { google: , github: , tailscale: , microsoft: , pocketid: , }; export const LoginPage = () => { const { isLoggedIn } = useUserContext(); const { providers, title, oauthAutoRedirect } = useAppContext(); const { search } = useLocation(); const { t } = useTranslation(); const [showRedirectButton, setShowRedirectButton] = useState(false); const hasAutoRedirectedRef = useRef(false); const redirectTimer = useRef(null); const redirectButtonTimer = useRef(null); const searchParams = new URLSearchParams(search); const { values: props, isOidc, compiled: compiledOIDCParams, } = useOIDCParams(searchParams); const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState( providers.find((provider) => provider.id === oauthAutoRedirect) !== undefined && props.redirect_uri, ); const oauthProviders = providers.filter( (provider) => provider.id !== "local" && provider.id !== "ldap", ); const userAuthConfigured = providers.find( (provider) => provider.id === "local" || provider.id === "ldap", ) !== undefined; const { mutate: oauthMutate, data: oauthData, isPending: oauthIsPending, variables: oauthVariables, } = useMutation({ mutationFn: (provider: string) => axios.get( `/api/oauth/url/${provider}${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, ), mutationKey: ["oauth"], onSuccess: (data) => { toast.info(t("loginOauthSuccessTitle"), { description: t("loginOauthSuccessSubtitle"), }); redirectTimer.current = window.setTimeout(() => { window.location.replace(data.data.url); }, 500); if (isOauthAutoRedirect) { redirectButtonTimer.current = window.setTimeout(() => { setShowRedirectButton(true); }, 5000); } }, onError: () => { setIsOauthAutoRedirect(false); toast.error(t("loginOauthFailTitle"), { description: t("loginOauthFailSubtitle"), }); }, }); const { mutate: loginMutate, isPending: loginIsPending } = useMutation({ mutationFn: (values: LoginSchema) => axios.post("/api/user/login", values), mutationKey: ["login"], onSuccess: (data) => { if (data.data.totpPending) { window.location.replace( `/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, ); return; } toast.success(t("loginSuccessTitle"), { description: t("loginSuccessSubtitle"), }); redirectTimer.current = window.setTimeout(() => { if (isOidc) { window.location.replace(`/authorize?${compiledOIDCParams}`); return; } window.location.replace( `/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, ); }, 500); }, onError: (error: AxiosError) => { toast.error(t("loginFailTitle"), { description: error.response?.status === 429 ? t("loginFailRateLimit") : t("loginFailSubtitle"), }); }, }); useEffect(() => { if ( !isLoggedIn && isOauthAutoRedirect && !hasAutoRedirectedRef.current && props.redirect_uri ) { hasAutoRedirectedRef.current = true; oauthMutate(oauthAutoRedirect); } }, [ isLoggedIn, oauthMutate, hasAutoRedirectedRef, oauthAutoRedirect, isOauthAutoRedirect, props.redirect_uri, ]); useEffect(() => { return () => { if (redirectTimer.current) { clearTimeout(redirectTimer.current); } if (redirectButtonTimer.current) { clearTimeout(redirectButtonTimer.current); } }; }, [redirectTimer, redirectButtonTimer]); if (isLoggedIn && isOidc) { return ; } if (isLoggedIn && props.redirect_uri !== "") { return ( ); } if (isLoggedIn) { return ; } if (isOauthAutoRedirect) { return ( {t("loginOauthAutoRedirectTitle")} {t("loginOauthAutoRedirectSubtitle")} {showRedirectButton && ( )} ); } return ( {title} {providers.length > 0 && ( {oauthProviders.length !== 0 ? t("loginTitle") : t("loginTitleSimple")} )} {oauthProviders.length !== 0 && (
{oauthProviders.map((provider) => ( } className="w-full" onClick={() => oauthMutate(provider.id)} loading={oauthIsPending && oauthVariables === provider.id} disabled={oauthIsPending || loginIsPending} /> ))}
)} {userAuthConfigured && oauthProviders.length !== 0 && ( {t("loginDivider")} )} {userAuthConfigured && ( loginMutate(values)} loading={loginIsPending || oauthIsPending} /> )} {providers.length == 0 && (

{t("failedToFetchProvidersTitle")}

)}
); };