feat: implement identity provider settings

This commit is contained in:
Steven
2024-08-11 23:30:58 +08:00
parent 61dd989df4
commit 768af5b096
16 changed files with 791 additions and 181 deletions

View File

@ -0,0 +1,81 @@
import { last } from "lodash-es";
import { ClientError } from "nice-grpc-web";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import Icon from "@/components/Icon";
import { authServiceClient } from "@/grpcweb";
import { absolutifyLink } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo";
import { useUserStore } from "@/stores";
interface State {
loading: boolean;
errorMessage: string;
}
const AuthCallback = () => {
const navigateTo = useNavigateTo();
const [searchParams] = useSearchParams();
const userStore = useUserStore();
const [state, setState] = useState<State>({
loading: true,
errorMessage: "",
});
useEffect(() => {
const code = searchParams.get("code");
const state = searchParams.get("state");
if (!code || !state) {
setState({
loading: false,
errorMessage: "Failed to authorize. Invalid state passed to the auth callback.",
});
return;
}
const idpName = last(state.split("-"));
if (!idpName) {
setState({
loading: false,
errorMessage: "No identity provider name found in the state parameter.",
});
return;
}
const redirectUri = absolutifyLink("/auth/callback");
(async () => {
try {
await authServiceClient.signInWithSSO({
idpName,
code,
redirectUri,
});
setState({
loading: false,
errorMessage: "",
});
await userStore.fetchCurrentUser();
navigateTo("/");
} catch (error: any) {
console.error(error);
setState({
loading: false,
errorMessage: (error as ClientError).details,
});
}
})();
}, [searchParams]);
return (
<div className="p-4 py-24 w-full h-full flex justify-center items-center">
{state.loading ? (
<Icon.Loader className="animate-spin dark:text-gray-200" />
) : (
<div className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
)}
</div>
);
};
export default AuthCallback;

View File

@ -1,13 +1,15 @@
import { Button, Input } from "@mui/joy";
import { Button, Divider, Input } from "@mui/joy";
import React, { FormEvent, useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import Logo from "@/components/Logo";
import { authServiceClient } from "@/grpcweb";
import { absolutifyLink } from "@/helpers/utils";
import useLoading from "@/hooks/useLoading";
import useNavigateTo from "@/hooks/useNavigateTo";
import { useUserStore, useWorkspaceStore } from "@/stores";
import { IdentityProvider, IdentityProvider_Type } from "@/types/proto/api/v1/workspace_service";
const SignIn: React.FC = () => {
const { t } = useTranslation();
@ -59,6 +61,24 @@ const SignIn: React.FC = () => {
actionBtnLoadingState.setFinish();
};
const handleSignInWithIdentityProvider = async (identityProvider: IdentityProvider) => {
const stateQueryParameter = `auth.signin.${identityProvider.title}-${identityProvider.name}`;
if (identityProvider.type === IdentityProvider_Type.OAUTH2) {
const redirectUri = absolutifyLink("/auth/callback");
const oauth2Config = identityProvider.config?.oauth2;
if (!oauth2Config) {
toast.error("Identity provider configuration is invalid.");
return;
}
const authUrl = `${oauth2Config.authUrl}?client_id=${
oauth2Config.clientId
}&redirect_uri=${redirectUri}&state=${stateQueryParameter}&response_type=code&scope=${encodeURIComponent(
oauth2Config.scopes.join(" "),
)}`;
window.location.href = authUrl;
}
};
return (
<div className="flex flex-row justify-center items-center w-full h-auto pt-12 sm:pt-24 bg-white dark:bg-zinc-900">
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-start items-center">
@ -105,6 +125,25 @@ const SignIn: React.FC = () => {
</Link>
</p>
)}
{workspaceStore.setting.identityProviders.length > 0 && (
<>
<Divider className="!my-4">{t("common.or")}</Divider>
<div className="w-full flex flex-col space-y-2">
{workspaceStore.setting.identityProviders.map((identityProvider) => (
<Button
key={identityProvider.name}
variant="outlined"
color="neutral"
className="w-full"
size="md"
onClick={() => handleSignInWithIdentityProvider(identityProvider)}
>
{t("auth.sign-in-with", { provider: identityProvider.title })}
</Button>
))}
</div>
</>
)}
</div>
</div>
</div>