diff --git a/client/packages/lowcoder-design/src/components/tacoInput.tsx b/client/packages/lowcoder-design/src/components/tacoInput.tsx index 1258400112..e7ce12111c 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 d38ee18438..a0edb74243 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 98e6141d73..00f2b7fcfa 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 c3d66e5571..6e7c532e4d 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 f6cbdac583..539844834a 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 316103c3df..1dc12e2029 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 a8d4213fb7..66f762da1b 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 61450a61f0..d30bd72d40 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 ec0a332572..aa05b4e3ff 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 b5e1fdb9ed..20619731bf 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 4843d0492d..fa235a9e65 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 e05a87a279..091aae36eb 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 e43e3b94f1..0000000000 --- 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 0000000000..596369d36b --- /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 0000000000..958995e74c --- /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 7d28cd551c..40e7a1bc15 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 4b2d895247..bad5349099 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 0000000000..f91663128a --- /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 88e6cadd7f..62bd7f7c2c 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 b21650e0e2..189afe5737 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 dda424c1f5..6725028072 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 6d3a0310c1..83be6cdbb1 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 b6299d6266..a2d4247873 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 2d888a9c97..308543d5eb 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 || {}; +}