mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	feat: multiple oauth providers (#355)
* feat: add flag decoder (candidate) * refactor: finalize flags decoder * feat: add env decoder * feat: add oauth config parsing logic * feat: implement backend logic for multiple oauth providers * feat: implement multiple oauth providers in the frontend * feat: add some default icons * chore: add credits for parser * feat: style oauth auto redirect screen * fix: bot suggestions * refactor: rework decoders using simpler and more efficient pattern * refactor: rework oauth name database migration
This commit is contained in:
		@@ -70,7 +70,7 @@ export const ContinuePage = () => {
 | 
			
		||||
    const reveal = setTimeout(() => {
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
      setShowRedirectButton(true);
 | 
			
		||||
    }, 1000);
 | 
			
		||||
    }, 5000);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      clearTimeout(auto);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,18 @@
 | 
			
		||||
import { LoginForm } from "@/components/auth/login-form";
 | 
			
		||||
import { GenericIcon } from "@/components/icons/generic";
 | 
			
		||||
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";
 | 
			
		||||
@@ -17,28 +22,40 @@ import { useIsMounted } from "@/lib/hooks/use-is-mounted";
 | 
			
		||||
import { LoginSchema } from "@/schemas/login-schema";
 | 
			
		||||
import { useMutation } from "@tanstack/react-query";
 | 
			
		||||
import axios, { AxiosError } from "axios";
 | 
			
		||||
import { useEffect, useRef } from "react";
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { Navigate, useLocation } from "react-router";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
 | 
			
		||||
const iconMap: Record<string, React.ReactNode> = {
 | 
			
		||||
  google: <GoogleIcon />,
 | 
			
		||||
  github: <GithubIcon />,
 | 
			
		||||
  tailscale: <TailscaleIcon />,
 | 
			
		||||
  microsoft: <MicrosoftIcon />,
 | 
			
		||||
  pocketid: <PocketIDIcon />,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const LoginPage = () => {
 | 
			
		||||
  const { isLoggedIn } = useUserContext();
 | 
			
		||||
  const { configuredProviders, title, oauthAutoRedirect, genericName } =
 | 
			
		||||
    useAppContext();
 | 
			
		||||
  const { providers, title, oauthAutoRedirect } = useAppContext();
 | 
			
		||||
  const { search } = useLocation();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const isMounted = useIsMounted();
 | 
			
		||||
  const [oauthAutoRedirectHandover, setOauthAutoRedirectHandover] =
 | 
			
		||||
    useState(false);
 | 
			
		||||
  const [showRedirectButton, setShowRedirectButton] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const redirectTimer = useRef<number | null>(null);
 | 
			
		||||
  const redirectButtonTimer = useRef<number | null>(null);
 | 
			
		||||
 | 
			
		||||
  const searchParams = new URLSearchParams(search);
 | 
			
		||||
  const redirectUri = searchParams.get("redirect_uri");
 | 
			
		||||
 | 
			
		||||
  const oauthConfigured =
 | 
			
		||||
    configuredProviders.filter((provider) => provider !== "username").length >
 | 
			
		||||
    0;
 | 
			
		||||
  const userAuthConfigured = configuredProviders.includes("username");
 | 
			
		||||
  const oauthProviders = providers.filter(
 | 
			
		||||
    (provider) => provider.id !== "username",
 | 
			
		||||
  );
 | 
			
		||||
  const userAuthConfigured =
 | 
			
		||||
    providers.find((provider) => provider.id === "username") !== undefined;
 | 
			
		||||
 | 
			
		||||
  const oauthMutation = useMutation({
 | 
			
		||||
    mutationFn: (provider: string) =>
 | 
			
		||||
@@ -56,6 +73,7 @@ export const LoginPage = () => {
 | 
			
		||||
      }, 500);
 | 
			
		||||
    },
 | 
			
		||||
    onError: () => {
 | 
			
		||||
      setOauthAutoRedirectHandover(false);
 | 
			
		||||
      toast.error(t("loginOauthFailTitle"), {
 | 
			
		||||
        description: t("loginOauthFailSubtitle"),
 | 
			
		||||
      });
 | 
			
		||||
@@ -96,12 +114,16 @@ export const LoginPage = () => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isMounted()) {
 | 
			
		||||
      if (
 | 
			
		||||
        oauthConfigured &&
 | 
			
		||||
        configuredProviders.includes(oauthAutoRedirect) &&
 | 
			
		||||
        oauthProviders.length !== 0 &&
 | 
			
		||||
        providers.find((provider) => provider.id === oauthAutoRedirect) &&
 | 
			
		||||
        !isLoggedIn &&
 | 
			
		||||
        redirectUri
 | 
			
		||||
      ) {
 | 
			
		||||
        setOauthAutoRedirectHandover(true);
 | 
			
		||||
        oauthMutation.mutate(oauthAutoRedirect);
 | 
			
		||||
        redirectButtonTimer.current = window.setTimeout(() => {
 | 
			
		||||
          setShowRedirectButton(true);
 | 
			
		||||
        }, 5000);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
@@ -109,6 +131,8 @@ export const LoginPage = () => {
 | 
			
		||||
  useEffect(
 | 
			
		||||
    () => () => {
 | 
			
		||||
      if (redirectTimer.current) clearTimeout(redirectTimer.current);
 | 
			
		||||
      if (redirectButtonTimer.current)
 | 
			
		||||
        clearTimeout(redirectButtonTimer.current);
 | 
			
		||||
    },
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
@@ -126,61 +150,63 @@ export const LoginPage = () => {
 | 
			
		||||
    return <Navigate to="/logout" replace />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (oauthAutoRedirectHandover) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Card className="min-w-xs sm:min-w-sm">
 | 
			
		||||
        <CardHeader>
 | 
			
		||||
          <CardTitle className="text-3xl">
 | 
			
		||||
            {t("loginOauthAutoRedirectTitle")}
 | 
			
		||||
          </CardTitle>
 | 
			
		||||
          <CardDescription>
 | 
			
		||||
            {t("loginOauthAutoRedirectSubtitle")}
 | 
			
		||||
          </CardDescription>
 | 
			
		||||
        </CardHeader>
 | 
			
		||||
        {showRedirectButton && (
 | 
			
		||||
          <CardFooter className="flex flex-col items-stretch">
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                window.location.replace(oauthMutation.data?.data.url);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {t("loginOauthAutoRedirectButton")}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </CardFooter>
 | 
			
		||||
        )}
 | 
			
		||||
      </Card>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Card className="min-w-xs sm:min-w-sm">
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <CardTitle className="text-center text-3xl">{title}</CardTitle>
 | 
			
		||||
        {configuredProviders.length > 0 && (
 | 
			
		||||
        {providers.length > 0 && (
 | 
			
		||||
          <CardDescription className="text-center">
 | 
			
		||||
            {oauthConfigured ? t("loginTitle") : t("loginTitleSimple")}
 | 
			
		||||
            {oauthProviders.length !== 0
 | 
			
		||||
              ? t("loginTitle")
 | 
			
		||||
              : t("loginTitleSimple")}
 | 
			
		||||
          </CardDescription>
 | 
			
		||||
        )}
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardContent className="flex flex-col gap-4">
 | 
			
		||||
        {oauthConfigured && (
 | 
			
		||||
        {oauthProviders.length !== 0 && (
 | 
			
		||||
          <div className="flex flex-col gap-2 items-center justify-center">
 | 
			
		||||
            {configuredProviders.includes("google") && (
 | 
			
		||||
            {oauthProviders.map((provider) => (
 | 
			
		||||
              <OAuthButton
 | 
			
		||||
                title="Google"
 | 
			
		||||
                icon={<GoogleIcon />}
 | 
			
		||||
                key={provider.id}
 | 
			
		||||
                title={provider.name}
 | 
			
		||||
                icon={iconMap[provider.id] ?? <OAuthIcon />}
 | 
			
		||||
                className="w-full"
 | 
			
		||||
                onClick={() => oauthMutation.mutate("google")}
 | 
			
		||||
                onClick={() => oauthMutation.mutate(provider.id)}
 | 
			
		||||
                loading={
 | 
			
		||||
                  oauthMutation.isPending &&
 | 
			
		||||
                  oauthMutation.variables === "google"
 | 
			
		||||
                  oauthMutation.variables === provider.id
 | 
			
		||||
                }
 | 
			
		||||
                disabled={oauthMutation.isPending || loginMutation.isPending}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            {configuredProviders.includes("github") && (
 | 
			
		||||
              <OAuthButton
 | 
			
		||||
                title="Github"
 | 
			
		||||
                icon={<GithubIcon />}
 | 
			
		||||
                className="w-full"
 | 
			
		||||
                onClick={() => oauthMutation.mutate("github")}
 | 
			
		||||
                loading={
 | 
			
		||||
                  oauthMutation.isPending &&
 | 
			
		||||
                  oauthMutation.variables === "github"
 | 
			
		||||
                }
 | 
			
		||||
                disabled={oauthMutation.isPending || loginMutation.isPending}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            {configuredProviders.includes("generic") && (
 | 
			
		||||
              <OAuthButton
 | 
			
		||||
                title={genericName}
 | 
			
		||||
                icon={<GenericIcon />}
 | 
			
		||||
                className="w-full"
 | 
			
		||||
                onClick={() => oauthMutation.mutate("generic")}
 | 
			
		||||
                loading={
 | 
			
		||||
                  oauthMutation.isPending &&
 | 
			
		||||
                  oauthMutation.variables === "generic"
 | 
			
		||||
                }
 | 
			
		||||
                disabled={oauthMutation.isPending || loginMutation.isPending}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {userAuthConfigured && oauthConfigured && (
 | 
			
		||||
        {userAuthConfigured && oauthProviders.length !== 0 && (
 | 
			
		||||
          <SeperatorWithChildren>{t("loginDivider")}</SeperatorWithChildren>
 | 
			
		||||
        )}
 | 
			
		||||
        {userAuthConfigured && (
 | 
			
		||||
@@ -189,7 +215,7 @@ export const LoginPage = () => {
 | 
			
		||||
            loading={loginMutation.isPending || oauthMutation.isPending}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {configuredProviders.length == 0 && (
 | 
			
		||||
        {providers.length == 0 && (
 | 
			
		||||
          <p className="text-center text-red-600 max-w-sm">
 | 
			
		||||
            {t("failedToFetchProvidersTitle")}
 | 
			
		||||
          </p>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,7 @@ import {
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
} from "@/components/ui/card";
 | 
			
		||||
import { useAppContext } from "@/context/app-context";
 | 
			
		||||
import { useUserContext } from "@/context/user-context";
 | 
			
		||||
import { capitalize } from "@/lib/utils";
 | 
			
		||||
import { useMutation } from "@tanstack/react-query";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { useEffect, useRef } from "react";
 | 
			
		||||
@@ -17,8 +15,7 @@ import { Navigate } from "react-router";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
 | 
			
		||||
export const LogoutPage = () => {
 | 
			
		||||
  const { provider, username, isLoggedIn, email } = useUserContext();
 | 
			
		||||
  const { genericName } = useAppContext();
 | 
			
		||||
  const { provider, username, isLoggedIn, email, oauthName } = useUserContext();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const redirectTimer = useRef<number | null>(null);
 | 
			
		||||
@@ -67,8 +64,7 @@ export const LogoutPage = () => {
 | 
			
		||||
              }}
 | 
			
		||||
              values={{
 | 
			
		||||
                username: email,
 | 
			
		||||
                provider:
 | 
			
		||||
                  provider === "generic" ? genericName : capitalize(provider),
 | 
			
		||||
                provider: oauthName,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user