diff --git a/client/packages/lowcoder-design/src/components/tacoInput.tsx b/client/packages/lowcoder-design/src/components/tacoInput.tsx index 125840011..e7ce12111 100644 --- a/client/packages/lowcoder-design/src/components/tacoInput.tsx +++ b/client/packages/lowcoder-design/src/components/tacoInput.tsx @@ -331,12 +331,14 @@ const FormInput = (props: { check: (value: string) => boolean; }; formName?: string; + onBlur?: () => void; onChange?: (value: string, valid: boolean) => void; className?: string; inputRef?: Ref; msg?: string; + defaultValue?: string; }) => { - const { mustFill, checkRule, label, placeholder, onChange, formName, className, inputRef } = + const { mustFill, checkRule, label, placeholder, onBlur, onChange, formName, className, inputRef, defaultValue } = props; const [valueValid, setValueValid] = useState(true); return ( @@ -350,6 +352,7 @@ const FormInput = (props: { ref={inputRef} name={formName} placeholder={placeholder} + defaultValue={defaultValue} onChange={(e) => { let valid = true; if (checkRule) { @@ -358,6 +361,7 @@ const FormInput = (props: { } onChange && onChange(e.target.value, valid); }} + onBlur={() => onBlur?.()} /> ); diff --git a/client/packages/lowcoder/src/api/applicationApi.ts b/client/packages/lowcoder/src/api/applicationApi.ts index d38ee1843..a0edb7424 100644 --- a/client/packages/lowcoder/src/api/applicationApi.ts +++ b/client/packages/lowcoder/src/api/applicationApi.ts @@ -98,6 +98,7 @@ class ApplicationApi extends Api { static publicToMarketplaceURL = (applicationId: string) => `/applications/${applicationId}/public-to-marketplace`; static getMarketplaceAppURL = (applicationId: string) => `/applications/${applicationId}/view_marketplace`; static setAppEditingStateURL = (applicationId: string) => `/applications/editState/${applicationId}`; + static serverSettingsURL = () => `/serverSettings`; static fetchHomeData(request: HomeDataPayload): AxiosPromise { return Api.get(ApplicationApi.fetchHomeDataURL, request); @@ -240,6 +241,10 @@ class ApplicationApi extends Api { editingFinished, }); } + + static fetchServerSettings(): AxiosPromise { + return Api.get(ApplicationApi.serverSettingsURL()); + } } export default ApplicationApi; diff --git a/client/packages/lowcoder/src/api/idSourceApi.ts b/client/packages/lowcoder/src/api/idSourceApi.ts index 98e6141d7..00f2b7fcf 100644 --- a/client/packages/lowcoder/src/api/idSourceApi.ts +++ b/client/packages/lowcoder/src/api/idSourceApi.ts @@ -44,8 +44,8 @@ class IdSourceApi extends Api { return Api.post(IdSourceApi.saveConfigURL, request); } - static deleteConfig(id: string): AxiosPromise { - return Api.delete(IdSourceApi.deleteConfigURL(id)); + static deleteConfig(id: string, deleteConfig?: boolean): AxiosPromise { + return Api.delete(IdSourceApi.deleteConfigURL(id), {delete: deleteConfig}); } static syncManual(authType: string): AxiosPromise { diff --git a/client/packages/lowcoder/src/api/orgApi.ts b/client/packages/lowcoder/src/api/orgApi.ts index c3d66e557..6e7c532e4 100644 --- a/client/packages/lowcoder/src/api/orgApi.ts +++ b/client/packages/lowcoder/src/api/orgApi.ts @@ -52,6 +52,7 @@ export class OrgApi extends Api { static deleteOrgURL = (orgId: string) => `/organizations/${orgId}`; static updateOrgURL = (orgId: string) => `/organizations/${orgId}/update`; static fetchUsage = (orgId: string) => `/organizations/${orgId}/api-usage`; + static fetchOrgsByEmailURL = (email: string) => `organizations/byuser/${email}`; static createGroup(request: { name: string }): AxiosPromise> { return Api.post(OrgApi.createGroupURL, request); @@ -141,6 +142,9 @@ export class OrgApi extends Api { return Api.get(OrgApi.fetchUsage(orgId), { lastMonthOnly: true }); } + static fetchOrgsByEmail(email: string): AxiosPromise { + return Api.get(OrgApi.fetchOrgsByEmailURL(email)); + } } export default OrgApi; diff --git a/client/packages/lowcoder/src/app.tsx b/client/packages/lowcoder/src/app.tsx index f6cbdac58..539844834 100644 --- a/client/packages/lowcoder/src/app.tsx +++ b/client/packages/lowcoder/src/app.tsx @@ -28,6 +28,7 @@ import { ADMIN_APP_URL, ORG_AUTH_FORGOT_PASSWORD_URL, ORG_AUTH_RESET_PASSWORD_URL, + ADMIN_AUTH_URL, } from "constants/routesURL"; import React from "react"; import { createRoot } from "react-dom/client"; @@ -55,7 +56,7 @@ import { getBrandingConfig } from "./redux/selectors/configSelectors"; import { buildMaterialPreviewURL } from "./util/materialUtils"; import GlobalInstances from 'components/GlobalInstances'; // import posthog from 'posthog-js' -import { fetchHomeData } from "./redux/reduxActions/applicationActions"; +import { fetchHomeData, fetchServerSettingsAction } from "./redux/reduxActions/applicationActions"; import { getNpmPackageMeta } from "./comps/utils/remote"; import { packageMetaReadyAction, setLowcoderCompsLoading } from "./redux/reduxActions/npmPluginActions"; @@ -94,6 +95,7 @@ type AppIndexProps = { fetchHomeData: (currentUserAnonymous?: boolean | undefined) => void; fetchLowcoderCompVersions: () => void; getCurrentUser: () => void; + fetchServerSettings: () => void; favicon: string; brandName: string; uiLanguage: string; @@ -102,6 +104,7 @@ type AppIndexProps = { class AppIndex extends React.Component { componentDidMount() { this.props.getCurrentUser(); + this.props.fetchServerSettings(); // if (!this.props.currentUserAnonymous) { // this.props.fetchHomeData(this.props.currentUserAnonymous); // } @@ -337,6 +340,7 @@ class AppIndex extends React.Component { // component={ApplicationListPage} component={LazyApplicationHome} /> + ({ dispatch(setLowcoderCompsLoading(false)); } }, + fetchServerSettings: () => { + dispatch(fetchServerSettingsAction()); + } }); const AppIndexWithProps = connect(mapStateToProps, mapDispatchToProps)(AppIndex); diff --git a/client/packages/lowcoder/src/constants/reduxActionConstants.ts b/client/packages/lowcoder/src/constants/reduxActionConstants.ts index 316103c3d..1dc12e202 100644 --- a/client/packages/lowcoder/src/constants/reduxActionConstants.ts +++ b/client/packages/lowcoder/src/constants/reduxActionConstants.ts @@ -145,6 +145,8 @@ export const ReduxActionTypes = { FETCH_ALL_MARKETPLACE_APPS: "FETCH_ALL_MARKETPLACE_APPS", FETCH_ALL_MARKETPLACE_APPS_SUCCESS: "FETCH_ALL_MARKETPLACE_APPS_SUCCESS", SET_APP_EDITING_STATE: "SET_APP_EDITING_STATE", + FETCH_SERVER_SETTINGS: "FETCH_SERVER_SETTINGS", + FETCH_SERVER_SETTINGS_SUCCESS: "FETCH_SERVER_SETTINGS_SUCCESS", /* user profile */ SET_USER_PROFILE_SETTING_MODAL_VISIBLE: "SET_USER_PROFILE_SETTING_MODAL_VISIBLE", diff --git a/client/packages/lowcoder/src/constants/routesURL.ts b/client/packages/lowcoder/src/constants/routesURL.ts index a8d4213fb..66f762da1 100644 --- a/client/packages/lowcoder/src/constants/routesURL.ts +++ b/client/packages/lowcoder/src/constants/routesURL.ts @@ -4,6 +4,7 @@ import { UserGuideLocationState } from "pages/tutorials/tutorialsConstant"; import { DatasourceType } from "@lowcoder-ee/constants/queryConstants"; export const BASE_URL = "/"; +export const ADMIN_AUTH_URL = "/admin/login"; export const USER_AUTH_URL = "/user/auth"; export const USER_PROFILE_URL = "/user/profile"; export const NEWS_URL = "/news"; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 61450a61f..d30bd72d4 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3008,7 +3008,11 @@ export const en = { "resetSuccessDesc": "Password Reset Succeeded. The New Password is: {password}", "resetLostPasswordSuccess": "Password Reset Succeeded. Please login again.", "copyPassword": "Copy Password", - "poweredByLowcoder": "Powered by: Lowcoder.cloud" + "poweredByLowcoder": "Powered by: Lowcoder.cloud", + "continue": "Continue", + "enterPassword": "Enter your password", + "selectAuthProvider": "Select Authentication Provider", + "selectWorkspace": "Select your workspace", }, "preLoad": { "jsLibraryHelpText": "Add JavaScript Libraries to Your Current Application via URL Addresses. lodash, day.js, uuid, numbro are Built into the System for Immediate Use. JavaScript Libraries are Loaded Before the Application is Initialized, Which Can Have an Impact on Application Performance.", @@ -3542,12 +3546,13 @@ export const en = { "formSelectPlaceholder": "Please Select the {label}", "saveSuccess": "Saved Successfully", "dangerLabel": "Danger Zone", - "dangerTip": "Disabling This ID Provider May Result in Some Users Being Unable to Log In. Proceed With Caution.", + "dangerTip": "Disabling or Deleting This ID Provider May Result in Some Users Being Unable to Log In. Proceed With Caution.", + "lastEnabledConfig": "You can't disable/delete config as this is the only enabled configuration.", "disable": "Disable", "disableSuccess": "Disabled Successfully", "encryptedServer": "-------- Encrypted on the Server Side --------", "disableTip": "Tips", - "disableContent": "Disabling This ID Provider May Result in Some Users Being Unable to Log In. Are You Sure to Proceed?", + "disableContent": "{action} This ID Provider May Result in Some Users Being Unable to Log In. Are You Sure to Proceed?", "manualTip": "", "lockTip": "The Content is Locked. To Make Changes, Please Click the {icon} to Unlock.", "lockModalContent": "Changing the 'ID Attribute' Field Can Have Significant Impacts on User Identification. Please Confirm That You Understand the Implications of This Change Before Proceeding.", 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 ec0a33257..aa05b4e3f 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/detail/deleteConfig.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/detail/deleteConfig.tsx @@ -4,42 +4,73 @@ import { trans } from "i18n"; import { useState } from "react"; import { validateResponse } from "api/apiUtils"; import IdSourceApi from "api/idSourceApi"; -import { DangerIcon, CustomModal } from "lowcoder-design"; +import { CustomModal } from "lowcoder-design"; import history from "util/history"; import { OAUTH_PROVIDER_SETTING } from "constants/routesURL"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; +import Flex from "antd/es/flex"; +import Alert from "antd/es/alert"; -export const DeleteConfig = (props: { id: string }) => { +export const DeleteConfig = (props: { + id: string, + allowDelete?: boolean, + allowDisable?: boolean, + isLastEnabledConfig?: boolean, +}) => { + const [disableLoading, setDisableLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); - const handleDelete = () => { + + const handleDelete = (deleteConfig?: boolean) => { + const setLoading = deleteConfig ? setDeleteLoading : setDisableLoading; + const action = deleteConfig ? trans("delete") : trans("idSource.disable"); CustomModal.confirm({ title: trans("idSource.disableTip"), - content: trans("idSource.disableContent"), + content: trans("idSource.disableContent", {action}), onConfirm: () => { - setDeleteLoading(true); - IdSourceApi.deleteConfig(props.id) - .then((resp) => { - if (validateResponse(resp)) { - messageInstance.success(trans("idSource.disableSuccess"), 0.8, () => + setLoading(true); + IdSourceApi.deleteConfig(props.id, deleteConfig) + .then((resp) => { + if (validateResponse(resp)) { + const successMsg = deleteConfig ? trans("home.deleteSuccessMsg") : trans("idSource.disableSuccess"); + messageInstance.success(successMsg, 0.8, () => history.push(OAUTH_PROVIDER_SETTING) ); } }) .catch((e) => messageInstance.error(e.message)) - .finally(() => setDeleteLoading(false)); + .finally(() => setLoading(false)); }, }); }; return ( -
{trans("idSource.dangerLabel")}
-
- - {trans("idSource.dangerTip")} -
- +

{trans("idSource.dangerLabel")}

+ + {props.isLastEnabledConfig && ( + + )} + + {props.allowDisable && ( + + )} + {props.allowDelete && ( + + )} +
); }; 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 b5e1fdb9e..20619731b 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx @@ -44,19 +44,22 @@ import { sourceMappingKeys } from "../OAuthForms/GenericOAuthForm"; import Flex from "antd/es/flex"; type IdSourceDetailProps = { - location: Location & { state: ConfigItem }; + location: Location & { state: { config: ConfigItem, totalEnabledConfigs: number }}; }; export const IdSourceDetail = (props: IdSourceDetailProps) => { - const configDetail = props.location.state; + const { + config: configDetail, + totalEnabledConfigs, + } = props.location.state; const [form] = useForm(); const [lock, setLock] = useState(() => { - const config = props.location.state; + const { config } = props.location.state; return !config.ifLocal; }); const [saveLoading, setSaveLoading] = useState(false); const [saveDisable, setSaveDisable] = useState(() => { - const config = props.location.state; + const { config } = props.location.state; if ( (config.authType === AuthType.Form && !config.enable) || (!config.ifLocal && !config.enable) @@ -321,12 +324,15 @@ export const IdSourceDetail = (props: IdSourceDetailProps) => { )} - {configDetail.enable && ( - <> - - - - )} + <> + + + ); diff --git a/client/packages/lowcoder/src/pages/setting/idSource/list.tsx b/client/packages/lowcoder/src/pages/setting/idSource/list.tsx index 4843d0492..fa235a9e6 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/list.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/list.tsx @@ -33,7 +33,6 @@ import { FreeTypes } from "pages/setting/idSource/idSourceConstants"; import { messageInstance, AddIcon } from "lowcoder-design"; import { currentOrgAdmin } from "../../../util/permissionUtils"; import CreateModal from "./createModal"; -import _ from "lodash"; import { HelpText } from "components/HelpText"; import { IconControlView } from "@lowcoder-ee/comps/controls/iconControl"; @@ -42,6 +41,7 @@ export const IdSourceList = (props: any) => { const config = useSelector(selectSystemConfig); const { currentOrgId} = user; const [configs, setConfigs] = useState([]); + const [enabledConfigs, setEnabledConfigs] = useState([]); const [fetching, setFetching] = useState(false); const [modalVisible, setModalVisible] = useState(false); const enableEnterpriseLogin = useSelector(selectSystemConfig)?.featureFlag?.enableEnterpriseLogin; @@ -76,8 +76,8 @@ export const IdSourceList = (props: any) => { let res: ConfigItem[] = resp.data.data.filter((item: ConfigItem) => IdSource.includes(item.authType) ); - // res = _.uniqBy(res, 'authType'); setConfigs(res); + setEnabledConfigs(res.filter(item => item.enable)); } }) .catch((e) => { @@ -126,7 +126,7 @@ export const IdSourceList = (props: any) => { } history.push({ pathname: OAUTH_PROVIDER_DETAIL, - state: record, + state: { config: record, totalEnabledConfigs: enabledConfigs.length }, }); }, })} diff --git a/client/packages/lowcoder/src/pages/setting/idSource/styledComponents.tsx b/client/packages/lowcoder/src/pages/setting/idSource/styledComponents.tsx index e05a87a27..091aae36e 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/styledComponents.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/styledComponents.tsx @@ -258,35 +258,16 @@ export const DeleteWrapper = styled.div` line-height: 19px; .danger-tip { - height: 32px; - padding: 0 16px 0 8px; - margin: 5px 0 8px 0; - background: #fff3f1; + max-width: 440px; + padding: 8px 16px; + margin: 5px 0 12px 0; border-radius: 4px; - display: inline-flex; align-items: center; svg { margin-right: 8px; } } - - .ant-btn { - min-width: 84px; - display: block; - padding: 4px 8px; - background: #fef4f4; - border: 1px solid #fccdcd; - font-size: 13px; - color: #f73131; - - &:hover, - &.ant-btn-loading { - background: #feecec; - } - - ${btnLoadingCss} - } `; export const StatusSpan = styled.span` diff --git a/client/packages/lowcoder/src/pages/userAuth/formLogin.tsx b/client/packages/lowcoder/src/pages/userAuth/formLogin.tsx deleted file mode 100644 index e43e3b94f..000000000 --- a/client/packages/lowcoder/src/pages/userAuth/formLogin.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { FormInput, PasswordInput } from "lowcoder-design"; -import { - AuthBottomView, - ConfirmButton, - FormWrapperMobile, - LoginCardTitle, - StyledRouteLink, -} from "pages/userAuth/authComponents"; -import React, { useContext, useState } from "react"; -import styled from "styled-components"; -import UserApi from "api/userApi"; -import { useRedirectUrl } from "util/hooks"; -import { checkEmailValid, checkPhoneValid } from "util/stringUtils"; -import { UserConnectionSource } from "@lowcoder-ee/constants/userConstants"; -import { trans } from "i18n"; -import { AuthContext, useAuthSubmit } from "pages/userAuth/authUtils"; -import { ThirdPartyAuth } from "pages/userAuth/thirdParty/thirdPartyAuth"; -import { AUTH_FORGOT_PASSWORD_URL, AUTH_REGISTER_URL, ORG_AUTH_FORGOT_PASSWORD_URL, ORG_AUTH_REGISTER_URL } from "constants/routesURL"; -import { Link, useLocation, useParams } from "react-router-dom"; -import { Divider } from "antd"; -import Flex from "antd/es/flex"; - -const AccountLoginWrapper = styled(FormWrapperMobile)` - display: flex; - flex-direction: column; - margin-bottom: 0px; - - .form-input.password-input { - margin-bottom: 0px; - } -`; - -type FormLoginProps = { - organizationId?: string; -} - -export default function FormLogin(props: FormLoginProps) { - const [account, setAccount] = useState(""); - const [password, setPassword] = useState(""); - const redirectUrl = useRedirectUrl(); - const { systemConfig, inviteInfo, fetchUserAfterAuthSuccess } = useContext(AuthContext); - const invitationId = inviteInfo?.invitationId; - const authId = systemConfig?.form.id; - const location = useLocation(); - const orgId = useParams().orgId; - - const { onSubmit, loading } = useAuthSubmit( - () => - UserApi.formLogin({ - register: false, - loginId: account, - password: password, - invitationId: invitationId, - source: UserConnectionSource.email, - orgId: props.organizationId, - authId, - }), - false, - redirectUrl, - fetchUserAfterAuthSuccess, - ); - - return ( - <> - {/* {trans("userAuth.login")} */} - - setAccount(valid ? value : "")} - placeholder={trans("userAuth.inputEmail")} - checkRule={{ - check: (value) => checkPhoneValid(value) || checkEmailValid(value), - errorMsg: trans("userAuth.inputValidEmail"), - }} - /> - setPassword(value)} - valueCheck={() => [true, ""]} - /> - - - {`${trans("userAuth.forgotPassword")}?`} - - - - {trans("userAuth.login")} - - {props.organizationId && ( - - )} - - - - {trans("userAuth.register")} - - - - ); -} diff --git a/client/packages/lowcoder/src/pages/userAuth/formLoginAdmin.tsx b/client/packages/lowcoder/src/pages/userAuth/formLoginAdmin.tsx new file mode 100644 index 000000000..596369d36 --- /dev/null +++ b/client/packages/lowcoder/src/pages/userAuth/formLoginAdmin.tsx @@ -0,0 +1,72 @@ +import { FormInput, PasswordInput } from "lowcoder-design"; +import { + ConfirmButton, + FormWrapperMobile, +} from "pages/userAuth/authComponents"; +import React, { useContext, useState } from "react"; +import styled from "styled-components"; +import UserApi from "api/userApi"; +import { checkEmailValid, checkPhoneValid } from "util/stringUtils"; +import { UserConnectionSource } from "@lowcoder-ee/constants/userConstants"; +import { trans } from "i18n"; +import { AuthContext, useAuthSubmit } from "pages/userAuth/authUtils"; + +export const AccountLoginWrapper = styled(FormWrapperMobile)` + position: relative; + display: flex; + flex-direction: column; + margin-bottom: 0px; + + .form-input.password-input { + margin-bottom: 0px; + } +`; + +type FormLoginProps = { + organizationId?: string; +} + +export default function FormLogin(props: FormLoginProps) { + const [account, setAccount] = useState(""); + const [password, setPassword] = useState(""); + const { fetchUserAfterAuthSuccess } = useContext(AuthContext); + + const { onSubmit, loading } = useAuthSubmit( + () => + UserApi.formLogin({ + register: false, + loginId: account, + password: password, + source: UserConnectionSource.email, + orgId: props.organizationId, + }), + false, + null, + fetchUserAfterAuthSuccess, + ); + + return ( + <> + + setAccount(valid ? value : "")} + placeholder={trans("userAuth.inputEmail")} + checkRule={{ + check: (value) => checkPhoneValid(value) || checkEmailValid(value), + errorMsg: trans("userAuth.inputValidEmail"), + }} + /> + setPassword(value)} + valueCheck={() => [true, ""]} + /> + + {trans("userAuth.login")} + + + + ); +} diff --git a/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx new file mode 100644 index 000000000..958995e74 --- /dev/null +++ b/client/packages/lowcoder/src/pages/userAuth/formLoginSteps.tsx @@ -0,0 +1,300 @@ +import { FormInput, messageInstance, PasswordInput } from "lowcoder-design"; +import { + AuthBottomView, + ConfirmButton, + FormWrapperMobile, + LoginCardTitle, + StyledRouteLink, +} from "pages/userAuth/authComponents"; +import React, { useContext, useEffect, useState } from "react"; +import styled from "styled-components"; +import UserApi from "api/userApi"; +import { useRedirectUrl } from "util/hooks"; +import { checkEmailValid, checkPhoneValid } from "util/stringUtils"; +import { UserConnectionSource } from "@lowcoder-ee/constants/userConstants"; +import { trans } from "i18n"; +import { AuthContext, useAuthSubmit } from "pages/userAuth/authUtils"; +import { ThirdPartyAuth } from "pages/userAuth/thirdParty/thirdPartyAuth"; +import { AUTH_FORGOT_PASSWORD_URL, AUTH_REGISTER_URL, ORG_AUTH_FORGOT_PASSWORD_URL, ORG_AUTH_REGISTER_URL } from "constants/routesURL"; +import { Link, useLocation, useParams } from "react-router-dom"; +import { Divider } from "antd"; +import Flex from "antd/es/flex"; +import { validateResponse } from "@lowcoder-ee/api/apiUtils"; +import OrgApi from "@lowcoder-ee/api/orgApi"; +import { AccountLoginWrapper } from "./formLoginAdmin"; +import { default as Button } from "antd/es/button"; +import LeftOutlined from "@ant-design/icons/LeftOutlined"; +import { fetchConfigAction } from "@lowcoder-ee/redux/reduxActions/configActions"; +import { useDispatch, useSelector } from "react-redux"; +import history from "util/history"; +import ApplicationApi from "@lowcoder-ee/api/applicationApi"; +import { getServerSettings } from "@lowcoder-ee/redux/selectors/applicationSelector"; + +const StyledCard = styled.div<{$selected: boolean}>` + display: flex; + justify-content: center; + flex-direction: column; + min-height: 56px; + margin-bottom: -1px; + padding: 0 24px; + color: rgba(0, 0, 0, 0.88); + font-size: 16px; + background: transparent; + border: 1px solid #f0f0f0; + border-radius: 8px; + cursor: pointer; + margin-bottom: 16px; + // box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02); + ${props => props.$selected && `background: #e6f4ff;`} + + &:hover { + box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09); + } +`; + +type OrgItem = { + orgId: string; + orgName: string; +} + +enum CurrentStepEnum { + EMAIL = "EMAIL", + WORKSPACES = "WORKSPACES", + AUTH_PROVIDERS = "AUTH_PROVIDERS", +} + +const StepHeader = (props : { + title: string, +}) => ( + +

{props.title}

+
+) + +const StepBackButton = (props : { + onClick: () => void, +}) => ( + +) + +type FormLoginProps = { + organizationId?: string; +} + +export default function FormLoginSteps(props: FormLoginProps) { + const dispatch = useDispatch(); + const location = useLocation(); + const [account, setAccount] = useState(() => { + const { email } = (location.state || {}) as any; + return email ?? ''; + }); + const [password, setPassword] = useState(""); + const redirectUrl = useRedirectUrl(); + const { systemConfig, inviteInfo, fetchUserAfterAuthSuccess } = useContext(AuthContext); + const invitationId = inviteInfo?.invitationId; + const authId = systemConfig?.form.id; + const isFormLoginEnabled = systemConfig?.form.enableLogin; + const [orgLoading, setOrgLoading] = useState(false); + const [orgList, setOrgList] = useState([]); + const [currentStep, setCurrentStep] = useState(CurrentStepEnum.EMAIL); + const [organizationId, setOrganizationId] = useState(props.organizationId); + const [skipWorkspaceStep, setSkipWorkspaceStep] = useState(false); + const [signupEnabled, setSignupEnabled] = useState(true); + const serverSettings = useSelector(getServerSettings); + + useEffect(() => { + const { LOWCODER_EMAIL_SIGNUP_ENABLED } = serverSettings; + if (!LOWCODER_EMAIL_SIGNUP_ENABLED) { + return setSignupEnabled(true); + } + setSignupEnabled(LOWCODER_EMAIL_SIGNUP_ENABLED === 'true'); + }, [serverSettings]); + + const { onSubmit, loading } = useAuthSubmit( + () => + UserApi.formLogin({ + register: false, + loginId: account, + password: password, + invitationId: invitationId, + source: UserConnectionSource.email, + orgId: organizationId, + authId, + }), + false, + redirectUrl, + fetchUserAfterAuthSuccess, + ); + + const fetchOrgsByEmail = () => { + // if user is invited or using org's login url then avoid fetching workspaces + // and skip workspace selection step + if (Boolean(organizationId)) { + setSkipWorkspaceStep(true); + dispatch(fetchConfigAction(organizationId)); + setCurrentStep(CurrentStepEnum.AUTH_PROVIDERS); + return; + } + + setOrgLoading(true); + OrgApi.fetchOrgsByEmail(account) + .then((resp) => { + if (validateResponse(resp)) { + setOrgList(resp.data.data); + if (!resp.data.data.length) { + history.push( + AUTH_REGISTER_URL, + {...location.state || {}, email: account}, + ) + return; + } + if (resp.data.data.length === 1) { + setOrganizationId(resp.data.data[0].orgId); + dispatch(fetchConfigAction(resp.data.data[0].orgId)); + setCurrentStep(CurrentStepEnum.AUTH_PROVIDERS); + return; + } + setCurrentStep(CurrentStepEnum.WORKSPACES); + } else { + throw new Error('Error while fetching organizations'); + } + }) + .catch((e) => { + messageInstance.error(e.message); + }) + .finally(() => { + setOrgLoading(false); + }); + } + + if(currentStep === CurrentStepEnum.EMAIL) { + return ( + <> + + + setAccount(valid ? value : "")} + placeholder={trans("userAuth.inputEmail")} + checkRule={{ + check: (value) => checkPhoneValid(value) || checkEmailValid(value), + errorMsg: trans("userAuth.inputValidEmail"), + }} + /> + + {trans("userAuth.continue")} + + + {signupEnabled && ( + <> + + + + {trans("userAuth.register")} + + + + )} + + ) + } + + if (currentStep === CurrentStepEnum.WORKSPACES) { + return ( + <> + + setCurrentStep(CurrentStepEnum.EMAIL)} /> + + {orgList.map(org => ( + { + setOrganizationId(org.orgId); + dispatch(fetchConfigAction(org.orgId)); + setCurrentStep(CurrentStepEnum.AUTH_PROVIDERS); + }} + > + {org.orgName} + + ))} + + + ) + } + + return ( + <> + + { + if (skipWorkspaceStep) return setCurrentStep(CurrentStepEnum.EMAIL); + setCurrentStep(CurrentStepEnum.WORKSPACES) + }} /> + + {isFormLoginEnabled && ( + <> + setPassword(value)} + valueCheck={() => [true, ""]} + /> + + + {`${trans("userAuth.forgotPassword")}?`} + + + + {trans("userAuth.login")} + + + )} + {organizationId && ( + + )} + + {isFormLoginEnabled && signupEnabled && ( + <> + + + + {trans("userAuth.register")} + + + + )} + + ); +} diff --git a/client/packages/lowcoder/src/pages/userAuth/index.tsx b/client/packages/lowcoder/src/pages/userAuth/index.tsx index 7d28cd551..40e7a1bc1 100644 --- a/client/packages/lowcoder/src/pages/userAuth/index.tsx +++ b/client/packages/lowcoder/src/pages/userAuth/index.tsx @@ -1,4 +1,4 @@ -import { AUTH_LOGIN_URL, USER_AUTH_URL } from "constants/routesURL"; +import { ADMIN_AUTH_URL, AUTH_LOGIN_URL, USER_AUTH_URL } from "constants/routesURL"; import { Redirect, Route, Switch, useLocation, useParams } from "react-router-dom"; import React, { useEffect, useMemo } from "react"; import { useSelector, useDispatch } from "react-redux"; @@ -9,6 +9,7 @@ import { AuthLocationState } from "constants/authConstants"; import { ProductLoading } from "components/ProductLoading"; import { fetchConfigAction } from "redux/reduxActions/configActions"; import { fetchUserAction } from "redux/reduxActions/userActions"; +import LoginAdmin from "./loginAdmin"; import _ from "lodash"; export default function UserAuth() { @@ -51,6 +52,7 @@ export default function UserAuth() { > + {AuthRoutes.map((route) => ( ))} diff --git a/client/packages/lowcoder/src/pages/userAuth/login.tsx b/client/packages/lowcoder/src/pages/userAuth/login.tsx index 4b2d89524..bad534909 100644 --- a/client/packages/lowcoder/src/pages/userAuth/login.tsx +++ b/client/packages/lowcoder/src/pages/userAuth/login.tsx @@ -3,12 +3,13 @@ import { AuthSearchParams } from "constants/authConstants"; import { CommonTextLabel } from "components/Label"; import { trans } from "i18n"; import { ThirdPartyAuth } from "pages/userAuth/thirdParty/thirdPartyAuth"; -import FormLogin from "@lowcoder-ee/pages/userAuth/formLogin"; +import FormLogin from "@lowcoder-ee/pages/userAuth/formLoginAdmin"; import { AuthContainer } from "pages/userAuth/authComponents"; import React, { useContext, useMemo } from "react"; import { AuthContext, getLoginTitle } from "pages/userAuth/authUtils"; import styled from "styled-components"; import { requiresUnAuth } from "pages/userAuth/authHOC"; +import FormLoginSteps from "./formLoginSteps"; const ThirdAuthWrapper = styled.div` display: flex; @@ -87,7 +88,7 @@ function Login() { const invitationId = inviteInfo?.invitationId; const location = useLocation(); const queryParams = new URLSearchParams(location.search); - const orgId = useParams().orgId; + const { orgId } = useParams<{orgId?: string}>(); const loginType = systemConfig?.authConfigs.find( (config) => config.sourceType === queryParams.get(AuthSearchParams.loginType) @@ -143,7 +144,7 @@ function Login() { heading={loginHeading} subHeading={loginSubHeading} > - + ); diff --git a/client/packages/lowcoder/src/pages/userAuth/loginAdmin.tsx b/client/packages/lowcoder/src/pages/userAuth/loginAdmin.tsx new file mode 100644 index 000000000..f91663128 --- /dev/null +++ b/client/packages/lowcoder/src/pages/userAuth/loginAdmin.tsx @@ -0,0 +1,23 @@ +import { trans } from "i18n"; +import FormLogin from "@lowcoder-ee/pages/userAuth/formLoginAdmin"; +import { AuthContainer } from "pages/userAuth/authComponents"; +import { requiresUnAuth } from "pages/userAuth/authHOC"; + +// this is the classic Sign In for super admin +function LoginAdmin() { + const loginHeading = trans("userAuth.userLogin"); + const loginSubHeading = trans("userAuth.poweredByLowcoder"); + + return ( + <> + + + + + ); +} + +export default requiresUnAuth(LoginAdmin); diff --git a/client/packages/lowcoder/src/pages/userAuth/register.tsx b/client/packages/lowcoder/src/pages/userAuth/register.tsx index 88e6cadd7..62bd7f7c2 100644 --- a/client/packages/lowcoder/src/pages/userAuth/register.tsx +++ b/client/packages/lowcoder/src/pages/userAuth/register.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState, useMemo } from "react"; +import React, { useContext, useState, useMemo, useEffect } from "react"; import { AuthContainer, ConfirmButton, @@ -7,7 +7,7 @@ import { StyledRouteLinkLogin, TermsAndPrivacyInfo, } from "pages/userAuth/authComponents"; -import { FormInput, PasswordInput } from "lowcoder-design"; +import { FormInput, messageInstance, PasswordInput } from "lowcoder-design"; import { AUTH_LOGIN_URL, ORG_AUTH_LOGIN_URL } from "constants/routesURL"; import UserApi from "api/userApi"; import { useRedirectUrl } from "util/hooks"; @@ -21,6 +21,13 @@ import { AuthContext, checkPassWithMsg, useAuthSubmit } from "pages/userAuth/aut import { ThirdPartyAuth } from "pages/userAuth/thirdParty/thirdPartyAuth"; import { useParams } from "react-router-dom"; import { Divider } from "antd"; +import { OrgApi } from "@lowcoder-ee/api/orgApi"; +import { validateResponse } from "@lowcoder-ee/api/apiUtils"; +import history from "util/history"; +import LoadingOutlined from "@ant-design/icons/LoadingOutlined"; +import Spin from "antd/es/spin"; +import { useSelector } from "react-redux"; +import { getServerSettings } from "@lowcoder-ee/redux/selectors/applicationSelector"; const StyledFormInput = styled(FormInput)` margin-bottom: 16px; @@ -37,11 +44,16 @@ const RegisterContent = styled(FormWrapperMobile)` `; function UserRegister() { + const location = useLocation(); const [submitBtnDisable, setSubmitBtnDisable] = useState(false); - const [account, setAccount] = useState(""); + const [account, setAccount] = useState(() => { + const { email } = (location.state || {}) as any; + return email ?? ''; + }); const [password, setPassword] = useState(""); + const [orgLoading, setOrgLoading] = useState(false); + const [lastEmailChecked, setLastEmailChecked] = useState(""); const redirectUrl = useRedirectUrl(); - const location = useLocation(); const { systemConfig, inviteInfo, fetchUserAfterAuthSuccess } = useContext(AuthContext); const invitationId = inviteInfo?.invitationId; @@ -55,6 +67,21 @@ function UserRegister() { const authId = systemConfig?.form.id; + const serverSettings = useSelector(getServerSettings); + + useEffect(() => { + const { LOWCODER_EMAIL_SIGNUP_ENABLED } = serverSettings; + if( + serverSettings.hasOwnProperty('LOWCODER_EMAIL_SIGNUP_ENABLED') + && LOWCODER_EMAIL_SIGNUP_ENABLED === 'false' + ) { + history.push( + AUTH_LOGIN_URL, + {...location.state || {}, email: account}, + ) + }; + }, [serverSettings]); + const { loading, onSubmit } = useAuthSubmit( () => UserApi.formLogin({ @@ -71,60 +98,86 @@ function UserRegister() { fetchUserAfterAuthSuccess, ); + const checkEmailExist = () => { + if (!Boolean(account.length) || lastEmailChecked === account) return; + + setOrgLoading(true); + OrgApi.fetchOrgsByEmail(account) + .then((resp) => { + if (validateResponse(resp)) { + const orgList = resp.data.data; + if (orgList.length) { + messageInstance.error('Email is already registered'); + history.push( + AUTH_LOGIN_URL, + {...location.state || {}, email: account}, + ) + } + } + }) + .finally(() => { + setLastEmailChecked(account) + setOrgLoading(false); + }); + } + const registerHeading = trans("userAuth.register") const registerSubHeading = trans("userAuth.poweredByLowcoder"); return ( - - - {/* {trans("userAuth.registerByEmail")} */} - setAccount(valid ? value : "")} - placeholder={trans("userAuth.inputEmail")} - checkRule={{ - check: checkEmailValid, - errorMsg: trans("userAuth.inputValidEmail"), - }} - /> - setPassword(valid ? value : "")} - doubleCheck - /> - - {trans("userAuth.register")} - - setSubmitBtnDisable(!e.target.checked)} /> - {organizationId && ( - } spinning={orgLoading}> + + + setAccount(valid ? value : "")} + onBlur={checkEmailExist} + placeholder={trans("userAuth.inputEmail")} + checkRule={{ + check: checkEmailValid, + errorMsg: trans("userAuth.inputValidEmail"), + }} + /> + setPassword(valid ? value : "")} + doubleCheck /> - )} - - - {trans("userAuth.userLogin")} - - + + {trans("userAuth.register")} + + setSubmitBtnDisable(!e.target.checked)} /> + {organizationId && ( + + )} + + + {trans("userAuth.userLogin")} + + + ); } diff --git a/client/packages/lowcoder/src/pages/userAuth/thirdParty/thirdPartyAuth.tsx b/client/packages/lowcoder/src/pages/userAuth/thirdParty/thirdPartyAuth.tsx index b21650e0e..189afe573 100644 --- a/client/packages/lowcoder/src/pages/userAuth/thirdParty/thirdPartyAuth.tsx +++ b/client/packages/lowcoder/src/pages/userAuth/thirdParty/thirdPartyAuth.tsx @@ -7,15 +7,20 @@ import { WhiteLoading } from "lowcoder-design"; import history from "util/history"; import { LoginLogoStyle, LoginLabelStyle, StyledLoginButton } from "pages/userAuth/authComponents"; import { useSelector } from "react-redux"; -import { selectSystemConfig } from "redux/selectors/configSelectors"; +import { getSystemConfigFetching, selectSystemConfig } from "redux/selectors/configSelectors"; import React from "react"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import styled from "styled-components"; import { trans } from "i18n"; import { geneAuthStateAndSaveParam, getAuthUrl, getRedirectUrl } from "pages/userAuth/authUtils"; import { default as Divider } from "antd/es/divider"; +import { default as Typography } from "antd/es/typography"; import { useRedirectUrl } from "util/hooks"; import { MultiIconDisplay } from "../../../comps/comps/multiIconDisplay"; +import Spin from "antd/es/spin"; +import { LoadingOutlined } from "@ant-design/icons"; + +const { Text } = Typography; const ThirdPartyLoginButtonWrapper = styled.div` button{ @@ -104,7 +109,14 @@ export function ThirdPartyAuth(props: { authGoal: ThirdPartyAuthGoal; labelFormatter?: (name: string) => string; }) { + const systemConfigFetching = useSelector(getSystemConfigFetching); const systemConfig = useSelector(selectSystemConfig); + const isFormLoginEnabled = systemConfig?.form.enableLogin; + + if (systemConfigFetching) { + return } />; + } + if (!systemConfig) { return null; } @@ -128,7 +140,11 @@ export function ThirdPartyAuth(props: { }); return ( - { Boolean(socialLoginButtons.length) && } + { isFormLoginEnabled && Boolean(socialLoginButtons.length) && ( + + or + + )} {socialLoginButtons} ); diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/applicationReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/applicationReducer.ts index dda424c1f..672502807 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/applicationReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/applicationReducer.ts @@ -337,6 +337,13 @@ const usersReducer = createReducer(initialState, { fetchingAppDetail: false, }, }), + [ReduxActionTypes.FETCH_SERVER_SETTINGS_SUCCESS]: ( + state: ApplicationReduxState, + action: ReduxAction> + ): ApplicationReduxState => ({ + ...state, + serverSettings: action.payload, + }), }); export interface ApplicationReduxState { @@ -348,6 +355,7 @@ export interface ApplicationReduxState { appPermissionInfo?: AppPermissionInfo; currentApplication?: ApplicationMeta; templateId?: string; + serverSettings?: Record; loadingStatus: { deletingApplication: boolean; isFetchingHomeData: boolean; // fetching app list diff --git a/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts b/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts index 6d3a0310c..83be6cdbb 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts @@ -181,3 +181,7 @@ export const setAppEditingState = (payload: SetAppEditingStatePayload) => ({ type: ReduxActionTypes.SET_APP_EDITING_STATE, payload: payload, }); + +export const fetchServerSettingsAction = () => ({ + type: ReduxActionTypes.FETCH_SERVER_SETTINGS, +}); diff --git a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts index b6299d626..a2d424787 100644 --- a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts @@ -403,6 +403,23 @@ function* setAppEditingStateSaga(action: ReduxAction) } } +export function* fetchServerSettingsSaga() { + try { + const response: AxiosResponse>> = yield call( + ApplicationApi.fetchServerSettings + ); + if (Boolean(response.data)) { + yield put({ + type: ReduxActionTypes.FETCH_SERVER_SETTINGS_SUCCESS, + payload: response.data, + }); + } + } catch (error: any) { + log.debug("fetch server settings error: ", error); + messageInstance.error(error.message); + } +} + export default function* applicationSagas() { yield all([ takeLatest(ReduxActionTypes.FETCH_HOME_DATA, fetchHomeDataSaga), @@ -429,5 +446,6 @@ export default function* applicationSagas() { fetchAllMarketplaceAppsSaga, ), takeLatest(ReduxActionTypes.SET_APP_EDITING_STATE, setAppEditingStateSaga), + takeLatest(ReduxActionTypes.FETCH_SERVER_SETTINGS, fetchServerSettingsSaga), ]); } diff --git a/client/packages/lowcoder/src/redux/selectors/applicationSelector.ts b/client/packages/lowcoder/src/redux/selectors/applicationSelector.ts index 2d888a9c9..308543d5e 100644 --- a/client/packages/lowcoder/src/redux/selectors/applicationSelector.ts +++ b/client/packages/lowcoder/src/redux/selectors/applicationSelector.ts @@ -43,3 +43,7 @@ export const isApplicationPublishing = (state: AppState): boolean => { export const getTemplateId = (state: AppState): any => { return state.ui.application.templateId; }; + +export const getServerSettings = (state: AppState): Record => { + return state.ui.application.serverSettings || {}; +}