mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-12-27 02:22:27 +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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user