mirror of
https://github.com/aykhans/slash-e.git
synced 2025-07-04 04:23:16 +00:00
feat: implement identity provider settings
This commit is contained in:
302
frontend/web/src/components/CreateIdentityProviderDrawer.tsx
Normal file
302
frontend/web/src/components/CreateIdentityProviderDrawer.tsx
Normal file
@ -0,0 +1,302 @@
|
||||
import { Button, DialogActions, DialogContent, DialogTitle, Divider, Drawer, Input, ModalClose } from "@mui/joy";
|
||||
import { isUndefined } from "lodash-es";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { workspaceServiceClient } from "@/grpcweb";
|
||||
import { absolutifyLink } from "@/helpers/utils";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import { useWorkspaceStore } from "@/stores";
|
||||
import { IdentityProvider, IdentityProvider_Type, IdentityProviderConfig_OAuth2Config } from "@/types/proto/api/v1/workspace_service";
|
||||
|
||||
interface Props {
|
||||
identityProvider?: IdentityProvider;
|
||||
onClose: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
identityProviderCreate: IdentityProvider;
|
||||
}
|
||||
|
||||
const CreateIdentityProviderDrawer: React.FC<Props> = (props: Props) => {
|
||||
const { onClose, onConfirm, identityProvider } = props;
|
||||
const { t } = useTranslation();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const [state, setState] = useState<State>({
|
||||
identityProviderCreate: IdentityProvider.fromPartial(
|
||||
identityProvider || {
|
||||
type: IdentityProvider_Type.OAUTH2,
|
||||
config: {
|
||||
oauth2: IdentityProviderConfig_OAuth2Config.fromPartial({
|
||||
scopes: [],
|
||||
fieldMapping: {},
|
||||
}),
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
const isCreating = isUndefined(identityProvider);
|
||||
const requestState = useLoading(false);
|
||||
|
||||
const setPartialState = (partialState: Partial<State>) => {
|
||||
setState({
|
||||
...state,
|
||||
...partialState,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNameInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
identityProviderCreate: Object.assign(state.identityProviderCreate, {
|
||||
name: e.target.value,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
identityProviderCreate: Object.assign(state.identityProviderCreate, {
|
||||
title: e.target.value,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleOAuth2ConfigChange = (e: React.ChangeEvent<HTMLInputElement>, field: string) => {
|
||||
if (!state.identityProviderCreate.config || !state.identityProviderCreate.config.oauth2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = field === "scopes" ? e.target.value.split(" ") : e.target.value;
|
||||
setPartialState({
|
||||
identityProviderCreate: Object.assign(state.identityProviderCreate, {
|
||||
config: Object.assign(state.identityProviderCreate.config, {
|
||||
oauth2: Object.assign(state.identityProviderCreate.config.oauth2, {
|
||||
[field]: value,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleFieldMappingChange = (e: React.ChangeEvent<HTMLInputElement>, field: string) => {
|
||||
if (
|
||||
!state.identityProviderCreate.config ||
|
||||
!state.identityProviderCreate.config.oauth2 ||
|
||||
!state.identityProviderCreate.config.oauth2.fieldMapping
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPartialState({
|
||||
identityProviderCreate: Object.assign(state.identityProviderCreate, {
|
||||
config: Object.assign(state.identityProviderCreate.config, {
|
||||
oauth2: Object.assign(state.identityProviderCreate.config.oauth2, {
|
||||
fieldMapping: Object.assign(state.identityProviderCreate.config.oauth2.fieldMapping, {
|
||||
[field]: e.target.value,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
if (!state.identityProviderCreate.name || !state.identityProviderCreate.title) {
|
||||
toast.error("Please fill in required fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isCreating) {
|
||||
await workspaceServiceClient.updateWorkspaceSetting({
|
||||
setting: {
|
||||
identityProviders: workspaceStore.setting.identityProviders.map((idp) =>
|
||||
idp.name === state.identityProviderCreate.name ? state.identityProviderCreate : idp,
|
||||
),
|
||||
},
|
||||
updateMask: ["identity_providers"],
|
||||
});
|
||||
} else {
|
||||
await workspaceServiceClient.updateWorkspaceSetting({
|
||||
setting: {
|
||||
identityProviders: [...workspaceStore.setting.identityProviders, state.identityProviderCreate],
|
||||
},
|
||||
updateMask: ["identity_providers"],
|
||||
});
|
||||
}
|
||||
|
||||
if (onConfirm) {
|
||||
onConfirm();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.details);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer anchor="right" open={true} onClose={onClose}>
|
||||
<DialogTitle>{isCreating ? "Create Identity Provider" : "Edit Identity Provider"}</DialogTitle>
|
||||
<ModalClose />
|
||||
<DialogContent className="w-full max-w-full">
|
||||
<div className="overflow-y-auto w-full mt-2 px-4 pb-4 sm:w-[24rem]">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Name <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="The unique name of your identity provider"
|
||||
value={state.identityProviderCreate.name}
|
||||
onChange={handleNameInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Title <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="A short title will be displayed in the UI"
|
||||
value={state.identityProviderCreate.title}
|
||||
onChange={handleTitleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isCreating && (
|
||||
<p className="border rounded-md p-2 text-sm w-full mb-2 break-all">Redirect URL: {absolutifyLink("/auth/callback")}</p>
|
||||
)}
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Client ID <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="Client ID of the OAuth2 provider"
|
||||
value={state.identityProviderCreate.config?.oauth2?.clientId}
|
||||
onChange={(e) => handleOAuth2ConfigChange(e, "clientId")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Client Secret <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="Client Secret of the OAuth2 provider"
|
||||
value={state.identityProviderCreate.config?.oauth2?.clientSecret}
|
||||
onChange={(e) => handleOAuth2ConfigChange(e, "clientSecret")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Authorization endpoint <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="Authorization endpoint of the OAuth2 provider"
|
||||
value={state.identityProviderCreate.config?.oauth2?.authUrl}
|
||||
onChange={(e) => handleOAuth2ConfigChange(e, "authUrl")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Token endpoint <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="Token endpoint of the OAuth2 provider"
|
||||
value={state.identityProviderCreate.config?.oauth2?.tokenUrl}
|
||||
onChange={(e) => handleOAuth2ConfigChange(e, "tokenUrl")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
User endpoint <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="User endpoint of the OAuth2 provider"
|
||||
value={state.identityProviderCreate.config?.oauth2?.userInfoUrl}
|
||||
onChange={(e) => handleOAuth2ConfigChange(e, "userInfoUrl")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Scopes <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="Scopes of the OAuth2 provider, separated by space"
|
||||
value={state.identityProviderCreate.config?.oauth2?.scopes.join(" ")}
|
||||
onChange={(e) => handleOAuth2ConfigChange(e, "scopes")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className="!mb-3" />
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Identifier <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="The field in the user info response to identify the user"
|
||||
value={state.identityProviderCreate.config?.oauth2?.fieldMapping?.identifier}
|
||||
onChange={(e) => handleFieldMappingChange(e, "identifier")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<span className="mb-2">Display name</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="The field in the user info response to display the user"
|
||||
value={state.identityProviderCreate.config?.oauth2?.fieldMapping?.displayName}
|
||||
onChange={(e) => handleFieldMappingChange(e, "displayName")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<div className="w-full flex flex-row justify-end items-center px-3 py-4 space-x-2">
|
||||
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onSave}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateIdentityProviderDrawer;
|
141
frontend/web/src/components/setting/SSOSection.tsx
Normal file
141
frontend/web/src/components/setting/SSOSection.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import { Button, IconButton } from "@mui/joy";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { workspaceServiceClient } from "@/grpcweb";
|
||||
import { useWorkspaceStore } from "@/stores";
|
||||
import { IdentityProvider } from "@/types/proto/api/v1/workspace_service";
|
||||
import CreateIdentityProviderDrawer from "../CreateIdentityProviderDrawer";
|
||||
import Icon from "../Icon";
|
||||
|
||||
interface EditState {
|
||||
open: boolean;
|
||||
// Leave empty to create new identity provider.
|
||||
identityProvider: IdentityProvider | undefined;
|
||||
}
|
||||
|
||||
const SSOSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
|
||||
const [editState, setEditState] = useState<EditState>({ open: false, identityProvider: undefined });
|
||||
|
||||
useEffect(() => {
|
||||
fetchIdentityProviderList();
|
||||
}, []);
|
||||
|
||||
const fetchIdentityProviderList = async () => {
|
||||
const setting = workspaceStore.fetchWorkspaceSetting();
|
||||
setIdentityProviderList((await setting).identityProviders);
|
||||
};
|
||||
|
||||
const handleDeleteIdentityProvider = async (identityProvider: IdentityProvider) => {
|
||||
const confirmed = window.confirm(`Are you sure you want to delete ${identityProvider.title}?`);
|
||||
if (confirmed) {
|
||||
try {
|
||||
await workspaceServiceClient.updateWorkspaceSetting({
|
||||
setting: {
|
||||
identityProviders: identityProviderList.filter((idp) => idp.name !== identityProvider.name),
|
||||
},
|
||||
updateMask: ["identity_providers"],
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.details);
|
||||
}
|
||||
await fetchIdentityProviderList();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-col gap-2 pt-2 pb-4">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-1">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<span className="font-medium dark:text-gray-400">SSO</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="neutral"
|
||||
onClick={() =>
|
||||
setEditState({
|
||||
open: true,
|
||||
identityProvider: undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</div>
|
||||
{identityProviderList.length > 0 && (
|
||||
<div className="mt-2 flow-root">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full py-2 align-middle">
|
||||
<table className="min-w-full divide-y divide-gray-300 dark:divide-zinc-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
|
||||
ID
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
|
||||
Title
|
||||
</th>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4">
|
||||
<span className="sr-only">{t("common.edit")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800">
|
||||
{identityProviderList.map((identityProvider) => (
|
||||
<tr key={identityProvider.name}>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm text-gray-900 dark:text-gray-500">
|
||||
{identityProvider.name}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{identityProvider.title}</td>
|
||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm">
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="plain"
|
||||
onClick={() =>
|
||||
setEditState({
|
||||
open: true,
|
||||
identityProvider: identityProvider,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Icon.PenBox className="w-4 h-auto" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="sm"
|
||||
color="danger"
|
||||
variant="plain"
|
||||
onClick={() => handleDeleteIdentityProvider(identityProvider)}
|
||||
>
|
||||
<Icon.Trash className="w-4 h-auto" />
|
||||
</IconButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editState.open && (
|
||||
<CreateIdentityProviderDrawer
|
||||
identityProvider={editState.identityProvider}
|
||||
onClose={() => setEditState({ open: false, identityProvider: undefined })}
|
||||
onConfirm={() => {
|
||||
setEditState({ open: false, identityProvider: undefined });
|
||||
fetchIdentityProviderList();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SSOSection;
|
@ -1,4 +1,4 @@
|
||||
import { Button, Option, Select, Textarea } from "@mui/joy";
|
||||
import { Button, Divider, Option, Select, Textarea } from "@mui/joy";
|
||||
import { head, isEqual } from "lodash-es";
|
||||
import { useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
@ -10,6 +10,7 @@ import { PlanType } from "@/types/proto/api/v1/subscription_service";
|
||||
import { WorkspaceSetting } from "@/types/proto/api/v1/workspace_service";
|
||||
import FeatureBadge from "../FeatureBadge";
|
||||
import Icon from "../Icon";
|
||||
import SSOSection from "./SSOSection";
|
||||
|
||||
const getDefaultVisibility = (visibility?: Visibility) => {
|
||||
if (!visibility || [Visibility.VISIBILITY_UNSPECIFIED, Visibility.UNRECOGNIZED].includes(visibility)) {
|
||||
@ -158,6 +159,10 @@ const WorkspaceSection = () => {
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<SSOSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
81
frontend/web/src/pages/AuthCallback.tsx
Normal file
81
frontend/web/src/pages/AuthCallback.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import App from "@/App";
|
||||
import Root from "@/layouts/Root";
|
||||
import AuthCallback from "@/pages/AuthCallback";
|
||||
import CollectionDashboard from "@/pages/CollectionDashboard";
|
||||
import CollectionSpace from "@/pages/CollectionSpace";
|
||||
import Home from "@/pages/Home";
|
||||
@ -21,11 +22,20 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{
|
||||
path: "/auth",
|
||||
element: <SignIn />,
|
||||
},
|
||||
{
|
||||
path: "/auth/signup",
|
||||
element: <SignUp />,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
element: <SignIn />,
|
||||
},
|
||||
{
|
||||
path: "signup",
|
||||
element: <SignUp />,
|
||||
},
|
||||
{
|
||||
path: "callback",
|
||||
element: <AuthCallback />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
|
Reference in New Issue
Block a user