diff --git a/client/packages/lowcoder/src/assets/icons/icon-login-keycloak.svg b/client/packages/lowcoder/src/assets/icons/icon-login-keycloak.svg new file mode 100644 index 000000000..44798d21c --- /dev/null +++ b/client/packages/lowcoder/src/assets/icons/icon-login-keycloak.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/lowcoder/src/assets/icons/icon-login-ory.svg b/client/packages/lowcoder/src/assets/icons/icon-login-ory.svg new file mode 100644 index 000000000..3ccd8fecf --- /dev/null +++ b/client/packages/lowcoder/src/assets/icons/icon-login-ory.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/client/packages/lowcoder/src/assets/icons/index.ts b/client/packages/lowcoder/src/assets/icons/index.ts index 409292ab7..e52837dc3 100644 --- a/client/packages/lowcoder/src/assets/icons/index.ts +++ b/client/packages/lowcoder/src/assets/icons/index.ts @@ -1,5 +1,7 @@ import GoogleLoginIcon from "./icon-login-google.svg"; import GithubLoginIcon from "./icon-login-github.svg"; +import OryLoginIcon from "./icon-login-ory.svg"; +import KeyCloakLoginIcon from "./icon-login-keycloak.svg"; import GeneralLoginIcon from "./icon-login-general.svg"; import EmailLoginIcon from "./icon-login-email.svg"; @@ -10,4 +12,11 @@ export { ReactComponent as DocIcon } from "./view-doc.svg"; export { ReactComponent as TutorialIcon } from "./tutorial.svg"; export { ReactComponent as ShortcutIcon } from "./icon-help-shortcut.svg"; -export { GoogleLoginIcon, GithubLoginIcon, GeneralLoginIcon, EmailLoginIcon }; +export { + GoogleLoginIcon, + GithubLoginIcon, + OryLoginIcon, + KeyCloakLoginIcon, + GeneralLoginIcon, + EmailLoginIcon +}; diff --git a/client/packages/lowcoder/src/constants/authConstants.ts b/client/packages/lowcoder/src/constants/authConstants.ts index 323615f16..bd92adaa2 100644 --- a/client/packages/lowcoder/src/constants/authConstants.ts +++ b/client/packages/lowcoder/src/constants/authConstants.ts @@ -11,7 +11,13 @@ import Login, { ThirdPartyBindCard } from "pages/userAuth/login"; import UserRegister from "pages/userAuth/register"; import { AuthRedirect } from "pages/userAuth/thirdParty/authRedirect"; import React from "react"; -import { GoogleLoginIcon, GithubLoginIcon, EmailLoginIcon } from "assets/icons"; +import { + GoogleLoginIcon, + GithubLoginIcon, + OryLoginIcon, + KeyCloakLoginIcon, + EmailLoginIcon +} from "assets/icons"; export type AuthInviteInfo = InviteInfo & { invitationId: string }; export type AuthLocationState = { inviteInfo?: AuthInviteInfo; thirdPartyAuthError?: boolean }; @@ -79,7 +85,7 @@ export const AuthRoutes: Array<{ path: string; component: React.ComponentType void; + onConfigCreate: () => void; +}; + +function CreateModal(props: CreateModalProp) { + const { + modalVisible, + oauthProvidersList, + closeModal, + onConfigCreate + } = props; + const [form] = Form.useForm(); + const [saveLoading, setSaveLoading] = useState(false); + + const handleOk = () => { + form.validateFields().then(values => { + console.log(values) + saveAuthProvider(values) + }) + } + function saveAuthProvider(values: ConfigItem) { + setSaveLoading(true); + const config = { + ...values, + enableRegister: true, + } + IdSourceApi.saveConfig(config) + .then((resp) => { + if (validateResponse(resp)) { + messageInstance.success(trans("idSource.saveSuccess")); + } + }) + .catch((e) => messageInstance.error(e.message)) + .finally(() => { + setSaveLoading(false); + onConfigCreate(); + }); + } + + function handleCancel() { + closeModal(); + form.resetFields(); + } + + const authConfigOptions = Object.values(authConfig) + .filter(config => { + return !(oauthProvidersList.indexOf(config.sourceValue) > -1) + }) + .map(config => ({ + label: config.sourceName, + value: config.sourceValue, + })); + + const selectedAuthType = Form.useWatch('authType', form);; + + const authConfigForm = useMemo(() => { + if(!authConfig[selectedAuthType]) return clientIdandSecretConfig; + return authConfig[selectedAuthType].form; + }, [selectedAuthType]) + + return ( + form.resetFields()} + > + + + + {authConfigOptions.map(config => ( + + + { + {config.value} + } + {config.label} + + + ))} + + + {Object.entries(authConfigForm).map(([key, value]) => { + const valueObject = _.isObject(value) ? (value as ItemType) : false; + const required = true; + const label = valueObject ? valueObject.label : value; + const tip = valueObject && valueObject.tip; + const isPassword = valueObject && valueObject.isPassword; + return ( +
+ + {label}: + + + ) : ( + + {label}: + + ) + } + > + {isPassword ? ( + + ) : ( + + )} + +
+ ); + })} + {/* + + + + {"Client secret"}: + + + } + rules={[{ + required: true, + message: trans("idSource.formPlaceholder", { + label: 'Client secret', + }) + }]} + > + + */} +
+
+ ); +} + +export default CreateModal; diff --git a/client/packages/lowcoder/src/pages/setting/idSource/detail/deleteConfig.tsx b/client/packages/lowcoder/src/pages/setting/idSource/detail/deleteConfig.tsx index 3682ea487..f9cfc370f 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/detail/deleteConfig.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/detail/deleteConfig.tsx @@ -6,7 +6,7 @@ import { validateResponse } from "api/apiUtils"; import IdSourceApi from "api/idSourceApi"; import { DangerIcon, CustomModal } from "lowcoder-design"; import history from "util/history"; -import { IDSOURCE_SETTING } from "constants/routesURL"; +import { OAUTH_PROVIDER_SETTING } from "constants/routesURL"; import { messageInstance } from "lowcoder-design"; export const DeleteConfig = (props: { id: string }) => { @@ -21,7 +21,7 @@ export const DeleteConfig = (props: { id: string }) => { .then((resp) => { if (validateResponse(resp)) { messageInstance.success(trans("idSource.disableSuccess"), 0.8, () => - history.push(IDSOURCE_SETTING) + history.push(OAUTH_PROVIDER_SETTING) ); } }) diff --git a/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx b/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx index 2b9807f54..80b3fd244 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx @@ -12,7 +12,7 @@ import { CloseEyeIcon, } from "lowcoder-design"; import history from "util/history"; -import { IDSOURCE_SETTING } from "constants/routesURL"; +import { OAUTH_PROVIDER_SETTING } from "constants/routesURL"; import { authConfig, AuthType, @@ -59,7 +59,7 @@ export const IdSourceDetail = (props: IdSourceDetailProps) => { }); const configDetail = props.location.state; const goList = () => { - history.push(IDSOURCE_SETTING); + history.push(OAUTH_PROVIDER_SETTING); }; if (!configDetail) { goList(); @@ -231,9 +231,9 @@ export const IdSourceDetail = (props: IdSourceDetailProps) => { ); })} - + {/* {trans("idSource.enableRegister")} - + */} diff --git a/client/packages/lowcoder/src/pages/setting/idSource/idSourceConstants.ts b/client/packages/lowcoder/src/pages/setting/idSource/idSourceConstants.ts index eb5053721..d4b09e173 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/idSourceConstants.ts +++ b/client/packages/lowcoder/src/pages/setting/idSource/idSourceConstants.ts @@ -5,9 +5,17 @@ export enum AuthType { Form = "FORM", Google = "GOOGLE", Github = "GITHUB", + Ory = "ORY", + KeyCloak = "KEYCLOAK", } -export const IdSource = [AuthType.Google, AuthType.Github, AuthType.Form]; +export const IdSource = [ + AuthType.Google, + AuthType.Github, + AuthType.Form, + AuthType.Ory, + AuthType.KeyCloak, +]; export const validatorOptions = []; @@ -22,19 +30,41 @@ export const clientIdandSecretConfig = { export const authConfig = { [AuthType.Form]: { sourceName: trans("idSource.form"), + sourceValue: AuthType.Form, form: {}, }, [AuthType.Github]: { sourceName: "GitHub", + sourceValue: AuthType.Github, form: clientIdandSecretConfig, }, [AuthType.Google]: { sourceName: "Google", + sourceValue: AuthType.Google, form: clientIdandSecretConfig, }, -} as { [key: string]: { sourceName: string; form: FormItemType } }; + [AuthType.Ory]: { + sourceName: "Ory", + sourceValue: AuthType.Ory, + form: { + ...clientIdandSecretConfig, + instanceId: "Instance ID", + scope: "Scope", + }, + }, + [AuthType.KeyCloak]: { + sourceName: "KeyCloak", + sourceValue: AuthType.KeyCloak, + form: { + ...clientIdandSecretConfig, + instanceId: "Instance ID", + realm: "Realm", + scope: "Scope", + }, + }, +} as { [key: string]: { sourceName: string; sourceValue: AuthType, form: FormItemType } }; -export const FreeTypes = [AuthType.Google, AuthType.Github, AuthType.Form]; +export const FreeTypes = [AuthType.Google, AuthType.Github, AuthType.Form, AuthType.Ory, AuthType.KeyCloak]; export const authTypeDisabled = (type: AuthType, enableEnterpriseLogin?: boolean) => { return !FreeTypes.includes(type); diff --git a/client/packages/lowcoder/src/pages/setting/idSource/index.tsx b/client/packages/lowcoder/src/pages/setting/idSource/index.tsx index 828792273..c0941a6f6 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/index.tsx @@ -1,13 +1,13 @@ import { Route, Switch } from "react-router"; import { IdSourceList } from "pages/setting/idSource/list"; import { IdSourceDetail } from "pages/setting/idSource/detail"; -import { IDSOURCE_SETTING, IDSOURCE_DETAIL } from "constants/routesURL"; +import { OAUTH_PROVIDER_SETTING, OAUTH_PROVIDER_DETAIL } from "constants/routesURL"; export const IdSourceHome = () => { return ( - - + + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/idSource/list.tsx b/client/packages/lowcoder/src/pages/setting/idSource/list.tsx index 4bfd06a6f..dedca4d34 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/list.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/list.tsx @@ -14,11 +14,15 @@ import { authTypeDisabled, IdSource, } from "@lowcoder-ee/pages/setting/idSource/idSourceConstants"; -import { SpanStyled, StatusSpan, TableStyled } from "pages/setting/idSource/styledComponents"; -import { genQueryId } from "comps/utils/idGenerator"; +import { + SpanStyled, + StatusSpan, + TableStyled, + CreateButton, +} from "pages/setting/idSource/styledComponents"; import FreeLimitTag from "pages/common/freeLimitTag"; import history from "util/history"; -import { IDSOURCE_DETAIL } from "constants/routesURL"; +import { OAUTH_PROVIDER_DETAIL } from "constants/routesURL"; import { selectSystemConfig } from "redux/selectors/configSelectors"; import { isEnterpriseMode, isSelfDomain } from "util/envUtils"; import { Badge } from "antd"; @@ -26,22 +30,28 @@ import { validateResponse } from "api/apiUtils"; import { ServerAuthTypeInfo } from "@lowcoder-ee/constants/authConstants"; import { GeneralLoginIcon } from "assets/icons"; import { FreeTypes } from "pages/setting/idSource/idSourceConstants"; -import { messageInstance } from "lowcoder-design"; +import { messageInstance, AddIcon } from "lowcoder-design"; +import { currentOrgAdmin } from "../../../util/permissionUtils"; +import CreateModal from "./createModal"; +import _ from "lodash"; -export const IdSourceList = () => { +export const IdSourceList = (props: any) => { const user = useSelector(getUser); const config = useSelector(selectSystemConfig); - const orgId = user.currentOrgId; - const [configs, setConfigs] = useState(); + const { currentOrgId} = user; + const [configs, setConfigs] = useState([]); const [fetching, setFetching] = useState(false); + const [modalVisible, setModalVisible] = useState(false); const enableEnterpriseLogin = useSelector(selectSystemConfig)?.featureFlag?.enableEnterpriseLogin; + useEffect(() => { - if (!orgId || (!isSelfDomain(config) && !isEnterpriseMode(config))) { + if (!currentOrgId || (!isSelfDomain(config) && !isEnterpriseMode(config))) { return; } getConfigs(); - }, [orgId]); - if (!orgId || (!isSelfDomain(config) && !isEnterpriseMode(config))) { + }, [currentOrgId]); + + if (!currentOrgId || (!isSelfDomain(config) && !isEnterpriseMode(config))) { return null; } @@ -50,34 +60,11 @@ export const IdSourceList = () => { IdSourceApi.getConfigs() .then((resp) => { if (validateResponse(resp)) { - const res: ConfigItem[] = resp.data.data.filter((item: ConfigItem) => + let res: ConfigItem[] = resp.data.data.filter((item: ConfigItem) => IdSource.includes(item.authType) ); - const data = IdSource.map((item: AuthType) => { - const config = res.find((config) => config.authType === item); - if (config) { - return config; - } else { - const form: { [key: string]: string | undefined } = {}; - Object.keys(authConfig[item].form).forEach((key: string) => { - form[key] = - key === "source" || key === "sourceName" - ? item - : key === "authServerId" - ? "default" - : undefined; - }); - return { - authType: item, - enable: false, - ifLocal: true, - id: genQueryId(), - enableRegister: true, - ...form, - }; - } - }); - setConfigs(data); + res = _.uniqBy(res, 'authType'); + setConfigs(res); } }) .catch((e) => { @@ -89,70 +76,94 @@ export const IdSourceList = () => { }; return ( - - {trans("idSource.title")} - { - return authTypeDisabled((record as ConfigItem).authType, enableEnterpriseLogin) - ? "row-disabled" - : ""; - }} - onRow={(record) => ({ - onClick: () => { - if (authTypeDisabled((record as ConfigItem).authType, enableEnterpriseLogin)) { - return; - } - history.push({ - pathname: IDSOURCE_DETAIL, - state: record, - }); - }, - })} - > - ( - - { - {value} + <> + + + {trans("idSource.title")} + {currentOrgAdmin(user) && ( + } + onClick={() => + setModalVisible(true) } - {authConfig[value as AuthType].sourceName} - {!FreeTypes.includes(value) && ( - - )} - + > + {"Add OAuth Provider"} + )} - /> - ( - - {value ? ( - - ) : ( - - )} - - )} - /> - - + + { + return authTypeDisabled((record as ConfigItem).authType, enableEnterpriseLogin) + ? "row-disabled" + : ""; + }} + onRow={(record) => ({ + onClick: () => { + if (authTypeDisabled((record as ConfigItem).authType, enableEnterpriseLogin)) { + return; + } + history.push({ + pathname: OAUTH_PROVIDER_DETAIL, + state: record, + }); + }, + })} + > + ( + + { + {value} + } + {authConfig[value as AuthType].sourceName} + {!FreeTypes.includes(value) && ( + + )} + + )} + /> + ( + + {value ? ( + + ) : ( + + )} + + )} + /> + + + config.authType)} + closeModal={() => setModalVisible(false)} + onConfigCreate={() => { + setModalVisible(false); + getConfigs(); + }} + /> + ); }; diff --git a/client/packages/lowcoder/src/pages/setting/idSource/styledComponents.tsx b/client/packages/lowcoder/src/pages/setting/idSource/styledComponents.tsx index fdb62ac9a..ee41f0b6a 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/styledComponents.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/styledComponents.tsx @@ -299,3 +299,30 @@ export const StatusSpan = styled.span` color: #8b8fa3; } `; + +export const CreateButton = styled(Button)` + background-color: #4965f2; + display: flex; + align-items: center; + justify-content: center; + padding: 4px 8px; + font-size: 13px; + border: 1px solid #4965f2; + box-shadow: none; + &.ant-btn-primary:hover, + &.ant-btn-primary:focus { + background: #315efb; + border-color: #315efb; + } + &:disabled, + &:disabled:hover { + background: #dbe1fd; + color: #ffffff; + border-color: #dbe1fd; + } + svg { + margin-right: 2px; + width: 12px; + height: 12px; + } +`; diff --git a/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx b/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx index c2f2a1425..4bf28dde5 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx @@ -72,7 +72,7 @@ function GroupUsersPermission(props: GroupPermissionProp) { <> - history.push(PERMISSION_SETTING)}>{trans("settings.member")} + history.push(PERMISSION_SETTING)}>{trans("settings.userGroups")} {isGroupAdmin(currentUserGroupRole) && !group.devGroup ? ( {group.groupName} diff --git a/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx b/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx index 50f9659a9..4fba28d1b 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx @@ -130,7 +130,7 @@ function OrgUsersPermission(props: UsersPermissionProp) { <> - history.push(PERMISSION_SETTING)}>{trans("settings.member")} + history.push(PERMISSION_SETTING)}>{trans("settings.userGroups")} {trans("memberSettings.allMembers")} diff --git a/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx b/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx index c26591618..30216a16a 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx @@ -118,7 +118,7 @@ export default function PermissionSetting() { return ( - {trans("settings.member")} + {trans("settings.userGroups")} {currentOrgAdmin(user) && ( ().setting || SettingPageEnum.Member; + const selectKey = useParams<{ setting: string }>().setting || SettingPageEnum.UserGroups; const items = [ { - key: SettingPageEnum.Member, - label: trans("settings.member"), + key: SettingPageEnum.UserGroups, + label: trans("settings.userGroups"), }, { key: SettingPageEnum.Organization, label: trans("settings.organization"), }, { - key: SettingPageEnum.IdSource, + key: SettingPageEnum.Theme, + label: trans("settings.theme"), + }, + { + key: SettingPageEnum.OAuthProvider, label: ( - {trans("settings.idSource")} + {trans("settings.oauthProviders")} {(!currentOrgAdmin(user) || (!isSelfDomain(config) && !isEnterpriseMode(config))) && ( )} @@ -55,6 +61,26 @@ export function SettingHome() { ), disabled: !currentOrgAdmin(user) || (!isSelfDomain(config) && !isEnterpriseMode(config)), }, + { + key: SettingPageEnum.Environments, + label: ( + + {trans("settings.environments")} + + + ), + disabled: true, + }, + { + key: SettingPageEnum.AppUsage, + label: ( + + {trans("settings.appUsage")} + + + ), + disabled: true, + }, { key: SettingPageEnum.Audit, label: ( @@ -67,10 +93,6 @@ export function SettingHome() { ), disabled: !showAuditLog(config) || !currentOrgAdmin(user), }, - { - key: SettingPageEnum.Theme, - label: trans("settings.theme"), - }, { key: SettingPageEnum.Branding, label: ( @@ -108,13 +130,13 @@ export function SettingHome() { items={items} /> - {selectKey === SettingPageEnum.Member && } + {selectKey === SettingPageEnum.UserGroups && } {selectKey === SettingPageEnum.Organization && } - {selectKey === SettingPageEnum.Audit && } {selectKey === SettingPageEnum.Theme && } + {selectKey === SettingPageEnum.OAuthProvider && } + {selectKey === SettingPageEnum.Audit && } {selectKey === SettingPageEnum.Branding && } {selectKey === SettingPageEnum.Advanced && } - {selectKey === SettingPageEnum.IdSource && } ); } diff --git a/client/packages/lowcoder/src/pages/setting/styled.tsx b/client/packages/lowcoder/src/pages/setting/styled.tsx index 3b05582e4..06f951857 100644 --- a/client/packages/lowcoder/src/pages/setting/styled.tsx +++ b/client/packages/lowcoder/src/pages/setting/styled.tsx @@ -1,5 +1,5 @@ import { GreyTextColor } from "constants/style"; -import { TacoButton } from "lowcoder-design"; +import { TacoButton, CustomModal, TacoInput } from "lowcoder-design"; import styled from "styled-components"; export const TwoColumnSettingPageContent = styled.div` @@ -58,3 +58,32 @@ export const SettingContent = styled.div` export const SaveButton = styled(TacoButton)` min-width: 76px; `; + +export const ModalNameDiv = styled.div` + display: flex; + align-items: center; + padding: 3.5px 0; + svg { + margin-right: 4px; + } +`; + +export const CustomModalStyled = styled(CustomModal)` + button { + margin-top: 20px; + } +`; + +export const TacoInputStyled = styled(TacoInput)` + margin-bottom: 5px; + height: 32px; + > input { + height: 100%; + } + .ant-input-suffix { + color: #999; + } + &.exceed .input-length { + color: #f73131; + } +`;