Merge branch 'develop' into feature/new-settings-description
# Conflicts: # packages/client/src/pages/PortalSettings/categories/security/StyledSecurity.js # packages/client/src/pages/PortalSettings/categories/security/sub-components/category-wrapper.js # packages/common/store/SettingsStore.js
This commit is contained in:
commit
a952793dcb
@ -63,6 +63,11 @@ public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
|
||||
status = HttpStatusCode.Forbidden;
|
||||
message = "Access denied";
|
||||
break;
|
||||
case BruteForceCredentialException:
|
||||
case RecaptchaException:
|
||||
status = HttpStatusCode.Forbidden;
|
||||
withStackTrace = false;
|
||||
break;
|
||||
case AuthenticationException:
|
||||
status = HttpStatusCode.Unauthorized;
|
||||
withStackTrace = false;
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"AllowedHosts": "*",
|
||||
"core": {
|
||||
"base-domain": "",
|
||||
@ -286,16 +286,20 @@
|
||||
"max-upload-size": 5242880,
|
||||
"zendesk-key": "",
|
||||
"samesite": "",
|
||||
"sso": {
|
||||
"saml": {
|
||||
"login":{
|
||||
"url" :"/sso/login"
|
||||
},
|
||||
"logout":{
|
||||
"url" :"/sso/slo"
|
||||
}
|
||||
}
|
||||
}
|
||||
"sso": {
|
||||
"saml": {
|
||||
"login": {
|
||||
"url": "/sso/login"
|
||||
},
|
||||
"logout": {
|
||||
"url": "/sso/slo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"recaptcha": {
|
||||
"public-key": "",
|
||||
"private-key": ""
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"default": {
|
||||
|
@ -43,6 +43,7 @@
|
||||
"BackupList": "Backup List",
|
||||
"BackupListWarningText": "If you delete any items from the list, their corresponding files will also be deleted. This action cannot be undone. To delete all the files use the link:",
|
||||
"BetaLabel": "BETA",
|
||||
"BlockingTime": "Blocking time (sec)",
|
||||
"Branding": "Branding",
|
||||
"BrandingSectionDescription": "Specify your company information, add links to external resources and email addresses displayed within the DocSpace interface.",
|
||||
"BrandingSubtitle": "Use this option to provide on-brand experience to users.",
|
||||
@ -50,11 +51,15 @@
|
||||
"BreakpointSmallTextPrompt": "Please resize the window or enable full-screen mode",
|
||||
"BreakpointWarningText": "This section is only available in the desktop version",
|
||||
"BreakpointWarningTextPrompt": "Please use the desktop site to access <1>{{sectionName}}</1> settings.",
|
||||
"BruteForceProtection": "Brute Force Protection",
|
||||
"BruteForceProtectionDescription": "Set up the limit of unsuccessful login attempts by the user to protect the space against brute-force attacks. When the limit is reached, attempts coming from the associated IP address will be banned for specified period of time, or captcha will be requested if enabled.",
|
||||
"BruteForceProtectionDescriptionMobile": "To protect the portal against brute-force attacks, you can set up the limits of unsuccessful login attempts by the user.",
|
||||
"ButtonsColor": "Buttons",
|
||||
"ByApp": "By authenticator app",
|
||||
"BySms": "By sms",
|
||||
"ChangeLogoButton": "Change Logo",
|
||||
"Characters": "{{length}} characters",
|
||||
"CheckPeriod": "Check period (sec)",
|
||||
"ClearBackupList": "Delete all backups",
|
||||
"CompanyInfoSettings": "Company info settings",
|
||||
"CompanyInfoSettingsDescription": "This information will be displayed in the <1>{{link}}</1> window.",
|
||||
@ -100,7 +105,10 @@
|
||||
"EmptyBackupList": "No backups have been created yet. Create one or more backups for them to appear in this list.",
|
||||
"EnableAutomaticBackup": "Enable automatic backup.",
|
||||
"EnableAutomaticBackupDescription": "Use this option to back up the space data.",
|
||||
"EnterNumber": "Enter number",
|
||||
"EnterTime": "Enter time",
|
||||
"EnterTitle": "Enter title",
|
||||
"ErrorMessageBruteForceProtection": "Specified argument was out of the range of valid values.",
|
||||
"EveryDay": "Every day",
|
||||
"EveryMonth": "Every month",
|
||||
"EveryWeek": "Every week",
|
||||
@ -137,6 +145,7 @@
|
||||
"MaxCopies": "{{copiesCount}} - maximum number of backup copies",
|
||||
"Migration": "Migration",
|
||||
"NewColorScheme": "New color scheme",
|
||||
"NumberOfAttempts": "Number of attempts",
|
||||
"PasswordMinLenght": "Minimal password length",
|
||||
"Path": "Path",
|
||||
"PleaseNote": "Please note",
|
||||
|
@ -57,7 +57,7 @@ export const StyledCategoryWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
@ -73,7 +73,7 @@ export const StyledMobileCategoryWrapper = styled.div`
|
||||
.category-item-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.category-item-subheader {
|
||||
@ -86,12 +86,13 @@ export const StyledMobileCategoryWrapper = styled.div`
|
||||
color: ${(props) => props.theme.client.settings.security.descriptionColor};
|
||||
font-size: 13px;
|
||||
max-width: 1024px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.inherit-title-link {
|
||||
margin-right: 7px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
@ -118,9 +119,66 @@ export const LearnMoreWrapper = styled.div`
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
padding-right: 8px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: ${(props) =>
|
||||
props.theme.client.settings.security.descriptionColor} !important;
|
||||
}
|
||||
|
||||
.learn-subtitle {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledBruteForceProtection = styled.div`
|
||||
width: 100%;
|
||||
|
||||
.brute-force-protection-input {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
color: #f21c0e;
|
||||
}
|
||||
|
||||
.save-cancel-buttons {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mobile-description {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
max-width: 700px;
|
||||
padding-bottom: 19px;
|
||||
|
||||
.page-subtitle {
|
||||
line-height: 20px;
|
||||
color: ${(props) =>
|
||||
props.theme.client.settings.security.descriptionColor};
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.link {
|
||||
line-height: 15px;
|
||||
font-weight: 600;
|
||||
color: ${(props) =>
|
||||
props.theme.client.settings.security.descriptionColor};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -0,0 +1,343 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { StyledBruteForceProtection } from "../StyledSecurity";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import FieldContainer from "@docspace/components/field-container";
|
||||
import toastr from "@docspace/components/toast/toastr";
|
||||
import TextInput from "@docspace/components/text-input";
|
||||
import SaveCancelButtons from "@docspace/components/save-cancel-buttons";
|
||||
import Text from "@docspace/components/text";
|
||||
import { size } from "@docspace/components/utils/device";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { saveToSessionStorage, getFromSessionStorage } from "../../../utils";
|
||||
import BruteForceProtectionLoader from "../sub-components/loaders/brute-force-protection-loader";
|
||||
import Link from "@docspace/components/link";
|
||||
|
||||
const BruteForceProtection = (props) => {
|
||||
const {
|
||||
t,
|
||||
numberAttempt,
|
||||
blockingTime,
|
||||
checkPeriod,
|
||||
setBruteForceProtection,
|
||||
getBruteForceProtection,
|
||||
initSettings,
|
||||
isInit,
|
||||
bruteForceProtectionUrl,
|
||||
} = props;
|
||||
|
||||
const defaultNumberAttempt = numberAttempt?.toString();
|
||||
const defaultBlockingTime = blockingTime?.toString();
|
||||
const defaultCheckPeriod = checkPeriod?.toString();
|
||||
|
||||
const [currentNumberAttempt, setCurrentNumberAttempt] =
|
||||
useState(defaultNumberAttempt);
|
||||
|
||||
const [currentBlockingTime, setCurrentBlockingTime] =
|
||||
useState(defaultBlockingTime);
|
||||
const [currentCheckPeriod, setCurrentCheckPeriod] =
|
||||
useState(defaultCheckPeriod);
|
||||
|
||||
const [showReminder, setShowReminder] = useState(false);
|
||||
const [isGetSettingsLoaded, setIsGetSettingsLoaded] = useState(false);
|
||||
|
||||
const [isLoadingSave, setIsLoadingSave] = useState(false);
|
||||
|
||||
const [hasErrorNumberAttempt, setHasErrorNumberAttempt] = useState(false);
|
||||
const [hasErrorBlockingTime, setHasErrorBlockingTime] = useState(false);
|
||||
const [hasErrorCheckPeriod, setHasErrorCheckPeriod] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentNumberAttempt == null ||
|
||||
currentCheckPeriod == null ||
|
||||
currentBlockingTime == null
|
||||
)
|
||||
return;
|
||||
|
||||
setHasErrorNumberAttempt(!parseInt(currentNumberAttempt));
|
||||
setHasErrorBlockingTime(!parseInt(currentBlockingTime));
|
||||
setHasErrorCheckPeriod(!parseInt(currentCheckPeriod));
|
||||
}, [currentNumberAttempt, currentBlockingTime, currentCheckPeriod]);
|
||||
|
||||
useEffect(() => {
|
||||
isInit && getSettings();
|
||||
}, [isInit]);
|
||||
|
||||
useEffect(() => {
|
||||
checkWidth();
|
||||
window.addEventListener("resize", checkWidth);
|
||||
|
||||
if (!isInit) initSettings();
|
||||
|
||||
return () => window.removeEventListener("resize", checkWidth);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGetSettingsLoaded) return;
|
||||
|
||||
const defaultSettings = getFromSessionStorage(
|
||||
"defaultBruteForceProtection"
|
||||
);
|
||||
|
||||
const checkNullNumberAttempt = !+currentNumberAttempt;
|
||||
const checkNullBlockingTime = !+currentBlockingTime;
|
||||
const checkNullCheckPeriod = !+currentCheckPeriod;
|
||||
|
||||
const newSettings = {
|
||||
numberAttempt: checkNullNumberAttempt
|
||||
? currentNumberAttempt
|
||||
: currentNumberAttempt.replace(/^0+/, ""),
|
||||
blockingTime: checkNullBlockingTime
|
||||
? currentBlockingTime
|
||||
: currentBlockingTime.replace(/^0+/, ""),
|
||||
checkPeriod: checkNullCheckPeriod
|
||||
? currentCheckPeriod
|
||||
: currentCheckPeriod.replace(/^0+/, ""),
|
||||
};
|
||||
|
||||
saveToSessionStorage("currentBruteForceProtection", newSettings);
|
||||
|
||||
if (isEqual(defaultSettings, newSettings)) {
|
||||
setShowReminder(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowReminder(true);
|
||||
}, [
|
||||
currentNumberAttempt,
|
||||
currentBlockingTime,
|
||||
currentCheckPeriod,
|
||||
isGetSettingsLoaded,
|
||||
]);
|
||||
|
||||
const checkWidth = () => {
|
||||
window.innerWidth > size.smallTablet &&
|
||||
location.pathname.includes("brute-force-protection") &&
|
||||
navigate("/portal-settings/security/access-portal");
|
||||
};
|
||||
|
||||
const getSettings = () => {
|
||||
const currentSettings = getFromSessionStorage(
|
||||
"currentBruteForceProtection"
|
||||
);
|
||||
|
||||
const defaultData = {
|
||||
numberAttempt: defaultNumberAttempt.replace(/^0+/, ""),
|
||||
blockingTime: defaultBlockingTime.replace(/^0+/, ""),
|
||||
checkPeriod: defaultCheckPeriod.replace(/^0+/, ""),
|
||||
};
|
||||
saveToSessionStorage("defaultBruteForceProtection", defaultData);
|
||||
|
||||
if (currentSettings) {
|
||||
setCurrentNumberAttempt(currentSettings.numberAttempt);
|
||||
setCurrentBlockingTime(currentSettings.blockingTime);
|
||||
setCurrentCheckPeriod(currentSettings.checkPeriod);
|
||||
setIsGetSettingsLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentNumberAttempt(defaultNumberAttempt);
|
||||
setCurrentBlockingTime(defaultBlockingTime);
|
||||
setCurrentCheckPeriod(defaultCheckPeriod);
|
||||
setIsGetSettingsLoaded(true);
|
||||
};
|
||||
|
||||
const onValidation = (inputValue) => {
|
||||
const isPositiveOrZeroNumber =
|
||||
Math.sign(inputValue) === 1 || Math.sign(inputValue) === 0;
|
||||
|
||||
return !(
|
||||
!isPositiveOrZeroNumber ||
|
||||
inputValue.indexOf(".") !== -1 ||
|
||||
inputValue.indexOf(" ") !== -1 ||
|
||||
inputValue.length > 4
|
||||
);
|
||||
};
|
||||
|
||||
const onChangeNumberAttempt = (e) => {
|
||||
const inputValue = e.target.value;
|
||||
|
||||
onValidation(inputValue) &&
|
||||
setCurrentNumberAttempt(inputValue) &&
|
||||
setShowReminder(true);
|
||||
};
|
||||
|
||||
const onChangeBlockingTime = (e) => {
|
||||
const inputValue = e.target.value;
|
||||
|
||||
onValidation(inputValue) &&
|
||||
setCurrentBlockingTime(inputValue) &&
|
||||
setShowReminder(true);
|
||||
};
|
||||
|
||||
const onChangeCheckPeriod = (e) => {
|
||||
const inputValue = e.target.value;
|
||||
|
||||
onValidation(inputValue) &&
|
||||
setCurrentCheckPeriod(inputValue) &&
|
||||
setShowReminder(true);
|
||||
};
|
||||
|
||||
const onSaveClick = () => {
|
||||
if (hasErrorNumberAttempt || hasErrorCheckPeriod) return;
|
||||
setIsLoadingSave(true);
|
||||
|
||||
const numberCurrentNumberAttempt = parseInt(currentNumberAttempt);
|
||||
const numberCurrentBlockingTime = parseInt(currentBlockingTime);
|
||||
const numberCurrentCheckPeriod = parseInt(currentCheckPeriod);
|
||||
|
||||
setBruteForceProtection(
|
||||
numberCurrentNumberAttempt,
|
||||
numberCurrentBlockingTime,
|
||||
numberCurrentCheckPeriod
|
||||
)
|
||||
.then(() => {
|
||||
saveToSessionStorage("defaultBruteForceProtection", {
|
||||
numberAttempt: currentNumberAttempt.replace(/^0+/, ""),
|
||||
blockingTime: currentBlockingTime.replace(/^0+/, ""),
|
||||
checkPeriod: currentCheckPeriod.replace(/^0+/, ""),
|
||||
});
|
||||
|
||||
getBruteForceProtection();
|
||||
setShowReminder(false);
|
||||
setIsLoadingSave(false);
|
||||
toastr.success(t("SuccessfullySaveSettingsMessage"));
|
||||
})
|
||||
.catch((error) => {
|
||||
toastr.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const onCancelClick = () => {
|
||||
const defaultSettings = getFromSessionStorage(
|
||||
"defaultBruteForceProtection"
|
||||
);
|
||||
setCurrentNumberAttempt(defaultSettings.numberAttempt);
|
||||
setCurrentBlockingTime(defaultSettings.blockingTime);
|
||||
setCurrentCheckPeriod(defaultSettings.checkPeriod);
|
||||
setShowReminder(false);
|
||||
};
|
||||
|
||||
const errorNode = (
|
||||
<div className="error-text">{t("ErrorMessageBruteForceProtection")}</div>
|
||||
);
|
||||
|
||||
if (isMobile && !isGetSettingsLoaded) return <BruteForceProtectionLoader />;
|
||||
|
||||
return (
|
||||
<StyledBruteForceProtection>
|
||||
<div className="description">
|
||||
<Text className="page-subtitle">
|
||||
{t("BruteForceProtectionDescription")}
|
||||
</Text>
|
||||
|
||||
<Link
|
||||
className="link"
|
||||
fontSize="13px"
|
||||
target="_blank"
|
||||
isHovered
|
||||
href={bruteForceProtectionUrl}
|
||||
>
|
||||
{t("Common:LearnMore")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<FieldContainer
|
||||
className="input-container"
|
||||
labelText={t("NumberOfAttempts")}
|
||||
isVertical={true}
|
||||
>
|
||||
<TextInput
|
||||
className="brute-force-protection-input"
|
||||
tabIndex={1}
|
||||
value={currentNumberAttempt}
|
||||
onChange={onChangeNumberAttempt}
|
||||
isDisabled={isLoadingSave}
|
||||
placeholder={t("EnterNumber")}
|
||||
hasError={hasErrorNumberAttempt}
|
||||
/>
|
||||
{hasErrorNumberAttempt && errorNode}
|
||||
</FieldContainer>
|
||||
|
||||
<FieldContainer
|
||||
className="input-container"
|
||||
labelText={t("BlockingTime")}
|
||||
isVertical={true}
|
||||
>
|
||||
<TextInput
|
||||
className="brute-force-protection-input"
|
||||
tabIndex={2}
|
||||
value={currentBlockingTime}
|
||||
onChange={onChangeBlockingTime}
|
||||
isDisabled={isLoadingSave}
|
||||
placeholder={t("EnterTime")}
|
||||
hasError={hasErrorBlockingTime}
|
||||
/>
|
||||
{hasErrorBlockingTime && errorNode}
|
||||
</FieldContainer>
|
||||
|
||||
<FieldContainer
|
||||
className="input-container"
|
||||
labelText={t("CheckPeriod")}
|
||||
isVertical={true}
|
||||
>
|
||||
<TextInput
|
||||
className="brute-force-protection-input"
|
||||
tabIndex={3}
|
||||
value={currentCheckPeriod}
|
||||
onChange={onChangeCheckPeriod}
|
||||
isDisabled={isLoadingSave}
|
||||
placeholder={t("EnterTime")}
|
||||
hasError={hasErrorCheckPeriod}
|
||||
/>
|
||||
{hasErrorCheckPeriod && errorNode}
|
||||
|
||||
<SaveCancelButtons
|
||||
className="save-cancel-buttons"
|
||||
tabIndex={4}
|
||||
onSaveClick={onSaveClick}
|
||||
onCancelClick={onCancelClick}
|
||||
showReminder={showReminder}
|
||||
reminderTest={t("YouHaveUnsavedChanges")}
|
||||
saveButtonLabel={t("Common:SaveButton")}
|
||||
cancelButtonLabel={t("Common:CancelButton")}
|
||||
displaySettings={true}
|
||||
hasScroll={false}
|
||||
additionalClassSaveButton="brute-force-protection-save"
|
||||
additionalClassCancelButton="brute-force-protection-cancel"
|
||||
isSaving={isLoadingSave}
|
||||
/>
|
||||
</FieldContainer>
|
||||
</StyledBruteForceProtection>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ auth, setup }) => {
|
||||
const {
|
||||
numberAttempt,
|
||||
blockingTime,
|
||||
checkPeriod,
|
||||
setBruteForceProtection,
|
||||
getBruteForceProtection,
|
||||
bruteForceProtectionUrl,
|
||||
} = auth.settingsStore;
|
||||
|
||||
const { initSettings, isInit } = setup;
|
||||
|
||||
return {
|
||||
numberAttempt,
|
||||
blockingTime,
|
||||
checkPeriod,
|
||||
setBruteForceProtection,
|
||||
getBruteForceProtection,
|
||||
initSettings,
|
||||
isInit,
|
||||
bruteForceProtectionUrl,
|
||||
};
|
||||
})(withTranslation(["Settings", "Common"])(observer(BruteForceProtection)));
|
@ -9,6 +9,7 @@ import TrustedMailSection from "./trustedMail";
|
||||
import IpSecuritySection from "./ipSecurity";
|
||||
import AdminMessageSection from "./adminMessage";
|
||||
import SessionLifetimeSection from "./sessionLifetime";
|
||||
import BruteForceProtectionSection from "./bruteForceProtection";
|
||||
import MobileView from "./mobileView";
|
||||
import StyledSettingsSeparator from "SRC_DIR/pages/PortalSettings/StyledSettingsSeparator";
|
||||
import { size } from "@docspace/components/utils/device";
|
||||
@ -141,6 +142,15 @@ const AccessPortal = (props) => {
|
||||
</div>
|
||||
|
||||
<IpSecuritySection />
|
||||
|
||||
<StyledSettingsSeparator />
|
||||
<CategoryWrapper
|
||||
notTooltip={true}
|
||||
t={t}
|
||||
title={t("BruteForceProtection")}
|
||||
/>
|
||||
<BruteForceProtectionSection />
|
||||
|
||||
<StyledSettingsSeparator />
|
||||
|
||||
<Text fontSize="16px" fontWeight="700">
|
||||
@ -212,4 +222,4 @@ export default inject(({ auth }) => {
|
||||
lifetimeSettingsUrl,
|
||||
ipSettingsUrl,
|
||||
};
|
||||
})(withTranslation("Settings")(observer(AccessPortal)));
|
||||
})(withTranslation(["Settings", "Profile"])(observer(AccessPortal)));
|
||||
|
@ -57,6 +57,12 @@ const MobileView = (props) => {
|
||||
url="/portal-settings/security/access-portal/ip"
|
||||
onClickLink={onClickLink}
|
||||
/>
|
||||
<MobileCategoryWrapper
|
||||
title={t("BruteForceProtection")}
|
||||
subtitle={t("BruteForceProtectionDescriptionMobile")}
|
||||
url="/portal-settings/security/access-portal/brute-force-protection"
|
||||
onClickLink={onClickLink}
|
||||
/>
|
||||
<MobileCategoryWrapper
|
||||
title={t("AdminsMessage")}
|
||||
subtitle={
|
||||
|
@ -0,0 +1,51 @@
|
||||
import styled from "styled-components";
|
||||
import Loaders from "@docspace/common/components/Loaders";
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
padding-right: 8px;
|
||||
|
||||
.header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
width: calc(100% - 32px);
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const BruteForceProtectionLoader = () => {
|
||||
return (
|
||||
<StyledLoader>
|
||||
<Loaders.Rectangle className="header" height="80px" />
|
||||
|
||||
<div className="content">
|
||||
<div>
|
||||
<Loaders.Rectangle width="140px" height="20px" />
|
||||
<Loaders.Rectangle height="32px" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Loaders.Rectangle width="117px" height="20px" />
|
||||
<Loaders.Rectangle height="32px" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Loaders.Rectangle width="117px" height="20px" />
|
||||
<Loaders.Rectangle height="32px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Loaders.Rectangle className="buttons" height="40px" />
|
||||
</StyledLoader>
|
||||
);
|
||||
};
|
||||
|
||||
export default BruteForceProtectionLoader;
|
@ -2,23 +2,19 @@ import { saveToSessionStorage, getFromSessionStorage } from "../utils";
|
||||
|
||||
export const resetSessionStorage = () => {
|
||||
const portalNameFromSessionStorage = getFromSessionStorage("portalName");
|
||||
const portalNameDefaultFromSessionStorage = getFromSessionStorage(
|
||||
"portalNameDefault"
|
||||
);
|
||||
const greetingTitleFromSessionStorage = getFromSessionStorage(
|
||||
"greetingTitle"
|
||||
);
|
||||
const portalNameDefaultFromSessionStorage =
|
||||
getFromSessionStorage("portalNameDefault");
|
||||
const greetingTitleFromSessionStorage =
|
||||
getFromSessionStorage("greetingTitle");
|
||||
const greetingTitleDefaultFromSessionStorage = getFromSessionStorage(
|
||||
"greetingTitleDefault"
|
||||
);
|
||||
const languageFromSessionStorage = getFromSessionStorage("language");
|
||||
const languageDefaultFromSessionStorage = getFromSessionStorage(
|
||||
"languageDefault"
|
||||
);
|
||||
const languageDefaultFromSessionStorage =
|
||||
getFromSessionStorage("languageDefault");
|
||||
const timezoneFromSessionStorage = getFromSessionStorage("timezone");
|
||||
const timezoneDefaultFromSessionStorage = getFromSessionStorage(
|
||||
"timezoneDefault"
|
||||
);
|
||||
const timezoneDefaultFromSessionStorage =
|
||||
getFromSessionStorage("timezoneDefault");
|
||||
|
||||
const selectColorId = getFromSessionStorage("selectColorId");
|
||||
const defaultColorId = getFromSessionStorage("defaultColorId");
|
||||
@ -41,6 +37,12 @@ export const resetSessionStorage = () => {
|
||||
);
|
||||
const currentIPSettings = getFromSessionStorage("currentIPSettings");
|
||||
const defaultIPSettings = getFromSessionStorage("defaultIPSettings");
|
||||
const currentBruteForceProtection = getFromSessionStorage(
|
||||
"currentBruteForceProtection"
|
||||
);
|
||||
const defaultBruteForceProtection = getFromSessionStorage(
|
||||
"defaultBruteForceProtection"
|
||||
);
|
||||
const currentAdminMessageSettings = getFromSessionStorage(
|
||||
"currentAdminMessageSettings"
|
||||
);
|
||||
@ -58,10 +60,9 @@ export const resetSessionStorage = () => {
|
||||
"defaultStoragePeriod"
|
||||
);
|
||||
|
||||
const companyNameFromeSessionStorage = getFromSessionStorage("companyName");
|
||||
const companySettingsFromSessionStorage = getFromSessionStorage(
|
||||
"companySettings"
|
||||
);
|
||||
const companyNameFromSessionStorage = getFromSessionStorage("companyName");
|
||||
const companySettingsFromSessionStorage =
|
||||
getFromSessionStorage("companySettings");
|
||||
const defaultCompanySettingsFromSessionStorage = getFromSessionStorage(
|
||||
"defaultCompanySettings"
|
||||
);
|
||||
@ -100,6 +101,12 @@ export const resetSessionStorage = () => {
|
||||
if (currentIPSettings !== defaultIPSettings) {
|
||||
saveToSessionStorage("currentIPSettings", defaultIPSettings);
|
||||
}
|
||||
if (currentBruteForceProtection !== defaultBruteForceProtection) {
|
||||
saveToSessionStorage(
|
||||
"currentBruteForceProtection",
|
||||
defaultBruteForceProtection
|
||||
);
|
||||
}
|
||||
if (currentAdminMessageSettings !== defaultAdminMessageSettings) {
|
||||
saveToSessionStorage(
|
||||
"currentAdminMessageSettings",
|
||||
|
@ -154,15 +154,22 @@ export const settingsTree = [
|
||||
tKey: "IPSecurity",
|
||||
},
|
||||
{
|
||||
id: "portal-settings_catalog-admin-message",
|
||||
id: "portal-settings_catalog-brute-force-protection",
|
||||
key: "1-0-4",
|
||||
icon: "",
|
||||
link: "brute-force-protection",
|
||||
tKey: "BruteForceProtection",
|
||||
},
|
||||
{
|
||||
id: "portal-settings_catalog-admin-message",
|
||||
key: "1-0-5",
|
||||
icon: "",
|
||||
link: "admin-message",
|
||||
tKey: "AdminsMessage",
|
||||
},
|
||||
{
|
||||
id: "portal-settings_catalog-session-life-time",
|
||||
key: "1-0-5",
|
||||
key: "1-0-6",
|
||||
icon: "",
|
||||
link: "lifetime",
|
||||
tKey: "SessionLifetime",
|
||||
|
@ -54,6 +54,11 @@ const TrustedMailPage = loadable(() =>
|
||||
const IpSecurityPage = loadable(() =>
|
||||
import("../pages/PortalSettings/categories/security/access-portal/ipSecurity")
|
||||
);
|
||||
const BruteForceProtectionPage = loadable(() =>
|
||||
import(
|
||||
"../pages/PortalSettings/categories/security/access-portal/bruteForceProtection"
|
||||
)
|
||||
);
|
||||
const AdminMessagePage = loadable(() =>
|
||||
import(
|
||||
"../pages/PortalSettings/categories/security/access-portal/adminMessage"
|
||||
@ -187,6 +192,10 @@ const PortalSettingsRoutes = {
|
||||
path: "security/access-portal/ip",
|
||||
element: <IpSecurityPage />,
|
||||
},
|
||||
{
|
||||
path: "security/access-portal/brute-force-protection",
|
||||
element: <BruteForceProtectionPage />,
|
||||
},
|
||||
{
|
||||
path: "security/access-portal/admin-message",
|
||||
element: <AdminMessagePage />,
|
||||
|
@ -84,7 +84,6 @@ class SettingsSetupStore {
|
||||
|
||||
initSettings = async () => {
|
||||
if (this.isInit) return;
|
||||
this.isInit = true;
|
||||
|
||||
if (authStore.isAuthenticated) {
|
||||
await authStore.settingsStore.getPortalPasswordSettings();
|
||||
@ -92,7 +91,10 @@ class SettingsSetupStore {
|
||||
await authStore.settingsStore.getIpRestrictionsEnable();
|
||||
await authStore.settingsStore.getIpRestrictions();
|
||||
await authStore.settingsStore.getSessionLifetime();
|
||||
await authStore.settingsStore.getBruteForceProtection();
|
||||
}
|
||||
|
||||
this.isInit = true;
|
||||
};
|
||||
|
||||
setIsLoadingDownloadReport = (state) => {
|
||||
|
@ -30,7 +30,12 @@ export function getPortalPasswordSettings(confirmKey = null) {
|
||||
return request(options);
|
||||
}
|
||||
|
||||
export function setPortalPasswordSettings(minLength, upperCase, digits, specSymbols) {
|
||||
export function setPortalPasswordSettings(
|
||||
minLength,
|
||||
upperCase,
|
||||
digits,
|
||||
specSymbols
|
||||
) {
|
||||
return request({
|
||||
method: "put",
|
||||
url: "/settings/security/password",
|
||||
@ -123,6 +128,21 @@ export function setLifetimeAuditSettings(data) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getBruteForceProtection() {
|
||||
return request({
|
||||
method: "get",
|
||||
url: "/settings/security/loginSettings",
|
||||
});
|
||||
}
|
||||
|
||||
export function setBruteForceProtection(AttemptCount, BlockTime, CheckPeriod) {
|
||||
return request({
|
||||
method: "put",
|
||||
url: "/settings/security/loginSettings",
|
||||
data: { AttemptCount, BlockTime, CheckPeriod },
|
||||
});
|
||||
}
|
||||
|
||||
export function getLoginHistoryReport() {
|
||||
return request({
|
||||
method: "post",
|
||||
@ -240,7 +260,13 @@ export function restoreWhiteLabelSettings(isDefault) {
|
||||
});
|
||||
}
|
||||
|
||||
export function setCompanyInfoSettings(address, companyName, email, phone, site) {
|
||||
export function setCompanyInfoSettings(
|
||||
address,
|
||||
companyName,
|
||||
email,
|
||||
phone,
|
||||
site
|
||||
) {
|
||||
const data = {
|
||||
settings: { address, companyName, email, phone, site },
|
||||
};
|
||||
@ -276,7 +302,7 @@ export function getCustomSchemaList() {
|
||||
export function setAdditionalResources(
|
||||
feedbackAndSupportEnabled,
|
||||
videoGuidesEnabled,
|
||||
helpCenterEnabled,
|
||||
helpCenterEnabled
|
||||
) {
|
||||
const data = {
|
||||
settings: {
|
||||
@ -323,7 +349,7 @@ export function setCustomSchema(
|
||||
regDateCaption,
|
||||
groupHeadCaption,
|
||||
guestCaption,
|
||||
guestsCaption,
|
||||
guestsCaption
|
||||
) {
|
||||
const data = {
|
||||
userCaption,
|
||||
@ -402,7 +428,14 @@ export function getMachineName(confirmKey = null) {
|
||||
return request(options);
|
||||
}
|
||||
|
||||
export function setPortalOwner(email, hash, lng, timeZone, confirmKey = null, analytics) {
|
||||
export function setPortalOwner(
|
||||
email,
|
||||
hash,
|
||||
lng,
|
||||
timeZone,
|
||||
confirmKey = null,
|
||||
analytics
|
||||
) {
|
||||
const options = {
|
||||
method: "put",
|
||||
url: "/settings/wizard/complete",
|
||||
@ -780,7 +813,15 @@ export function removeWebhook(id) {
|
||||
}
|
||||
|
||||
export function getWebhooksJournal(props) {
|
||||
const { configId, eventId, count, startIndex, deliveryFrom, deliveryTo, groupStatus } = props;
|
||||
const {
|
||||
configId,
|
||||
eventId,
|
||||
count,
|
||||
startIndex,
|
||||
deliveryFrom,
|
||||
deliveryTo,
|
||||
groupStatus,
|
||||
} = props;
|
||||
|
||||
const params = {};
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { request, setWithCredentialsStatus } from "../client";
|
||||
|
||||
export function login(userName, passwordHash, session) {
|
||||
export function login(userName, passwordHash, session, recaptchaResponse) {
|
||||
const data = {
|
||||
userName,
|
||||
passwordHash,
|
||||
session,
|
||||
recaptchaResponse,
|
||||
};
|
||||
|
||||
return request({
|
||||
|
@ -157,6 +157,10 @@ class SettingsStore {
|
||||
|
||||
interfaceDirection = localStorage.getItem("interfaceDirection") || "ltr";
|
||||
|
||||
numberAttempt = null;
|
||||
blockingTime = null;
|
||||
checkPeriod = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
@ -289,6 +293,10 @@ class SettingsStore {
|
||||
return `${this.helpLink}/administration/docspace-settings.aspx#ipsecurity`;
|
||||
}
|
||||
|
||||
get bruteForceProtectionUrl() {
|
||||
return `${this.helpLink}/administration/configuration.aspx#loginsettings`;
|
||||
}
|
||||
|
||||
get administratorMessageSettingsUrl() {
|
||||
return `${this.helpLink}/administration/docspace-settings.aspx#administratormessage`;
|
||||
}
|
||||
@ -833,6 +841,26 @@ class SettingsStore {
|
||||
return res;
|
||||
};
|
||||
|
||||
setBruteForceProtectionSettings = (settings) => {
|
||||
this.numberAttempt = settings.attemptCount;
|
||||
this.blockingTime = settings.blockTime;
|
||||
this.checkPeriod = settings.checkPeriod;
|
||||
};
|
||||
|
||||
getBruteForceProtection = async () => {
|
||||
const res = await api.settings.getBruteForceProtection();
|
||||
|
||||
this.setBruteForceProtectionSettings(res);
|
||||
};
|
||||
|
||||
setBruteForceProtection = async (AttemptCount, BlockTime, CheckPeriod) => {
|
||||
return api.settings.setBruteForceProtection(
|
||||
AttemptCount,
|
||||
BlockTime,
|
||||
CheckPeriod
|
||||
);
|
||||
};
|
||||
|
||||
setIsBurgerLoading = (isBurgerLoading) => {
|
||||
this.isBurgerLoading = isBurgerLoading;
|
||||
};
|
||||
|
@ -161,6 +161,7 @@ class AxiosClient {
|
||||
}
|
||||
break;
|
||||
case 403:
|
||||
if (options.skipLogout) return Promise.reject(error);
|
||||
const pathname = window.location.pathname;
|
||||
const isArchived = pathname.indexOf("/rooms/archived") !== -1;
|
||||
|
||||
|
@ -3,10 +3,11 @@ import { setWithCredentialsStatus } from "../api/client";
|
||||
export async function login(
|
||||
user: string,
|
||||
hash: string,
|
||||
session = true
|
||||
session = true,
|
||||
captchaToken: string
|
||||
): Promise<string | object> {
|
||||
try {
|
||||
const response = await api.user.login(user, hash, session);
|
||||
const response = await api.user.login(user, hash, session, captchaToken);
|
||||
|
||||
if (!response || (!response.token && !response.tfa))
|
||||
throw response.error.message;
|
||||
|
@ -53,6 +53,8 @@ const {
|
||||
strongBlue,
|
||||
lightGrayishStrongBlue,
|
||||
darkRed,
|
||||
|
||||
lightErrorStatus,
|
||||
} = globalColors;
|
||||
|
||||
const Base = {
|
||||
@ -2785,6 +2787,11 @@ const Base = {
|
||||
container: {
|
||||
backgroundColor: grayLightMid,
|
||||
},
|
||||
|
||||
captcha: {
|
||||
border: `1px solid ${lightErrorStatus}`,
|
||||
color: lightErrorStatus,
|
||||
},
|
||||
},
|
||||
|
||||
facebookButton: {
|
||||
|
@ -47,6 +47,8 @@ const {
|
||||
strongBlue,
|
||||
lightGrayishStrongBlue,
|
||||
darkRed,
|
||||
|
||||
darkErrorStatus,
|
||||
} = globalColors;
|
||||
|
||||
const Dark = {
|
||||
@ -2791,6 +2793,11 @@ const Dark = {
|
||||
container: {
|
||||
backgroundColor: "#474747",
|
||||
},
|
||||
|
||||
captcha: {
|
||||
border: `1px solid ${darkErrorStatus}`,
|
||||
color: darkErrorStatus,
|
||||
},
|
||||
},
|
||||
|
||||
facebookButton: {
|
||||
|
@ -56,6 +56,9 @@ const globalColors = {
|
||||
hoverError: "#F7CDBE",
|
||||
hoverInfo: "#F8F7BF",
|
||||
hoverWarning: "#F7E6BE",
|
||||
|
||||
lightErrorStatus: "#F24724",
|
||||
darkErrorStatus: "#E06451",
|
||||
};
|
||||
|
||||
export default globalColors;
|
||||
|
11
packages/login/index.d.ts
vendored
11
packages/login/index.d.ts
vendored
@ -35,6 +35,8 @@ declare global {
|
||||
size: number;
|
||||
};
|
||||
|
||||
type CaptchaPublicKeyType = string | undefined;
|
||||
|
||||
interface IEmailValid {
|
||||
value: string;
|
||||
isValid: boolean;
|
||||
@ -60,6 +62,7 @@ declare global {
|
||||
version: string;
|
||||
standalone: boolean;
|
||||
trustedDomains: string[];
|
||||
recaptchaPublicKey: CaptchaPublicKeyType;
|
||||
}
|
||||
|
||||
interface IBuildInfo {
|
||||
@ -85,7 +88,7 @@ declare global {
|
||||
type TThemeObj = {
|
||||
accent: string;
|
||||
buttons: string;
|
||||
}
|
||||
};
|
||||
|
||||
interface ITheme {
|
||||
id: number;
|
||||
@ -165,17 +168,17 @@ declare global {
|
||||
type TLogoPath = {
|
||||
light: string;
|
||||
dark?: string;
|
||||
}
|
||||
};
|
||||
|
||||
type TLogoSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
isEmpty: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
interface ILogoUrl {
|
||||
name: string;
|
||||
path: TLogoPath;
|
||||
size: TLogoSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,6 +120,7 @@
|
||||
"iconv-lite": "^0.6.3",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"utf-8-validate": "^5.0.10",
|
||||
"winston": "^3.8.2",
|
||||
"winston-cloudwatch": "^6.1.1",
|
||||
|
@ -7,6 +7,7 @@
|
||||
"InvalidUsernameOrPassword": "Invalid username or password",
|
||||
"LoginWithAccountNotFound": "Can't find associated third-party account. You need to connect your social networking account at the profile editing page first.",
|
||||
"LoginWithBruteForce": "Authorization temporarily blocked",
|
||||
"LoginWithBruteForceCaptcha": "Confirm that you are not a robot",
|
||||
"RecaptchaInvalid": "Invalid Recaptcha",
|
||||
"SsoAttributesNotFound": "Authentication failed (assertion attributes not found)",
|
||||
"SsoAuthFailed": "Authentication failed",
|
||||
|
@ -47,6 +47,7 @@ interface ILoginProps extends IInitialState {
|
||||
isDesktopEditor?: boolean;
|
||||
theme: IUserTheme;
|
||||
setTheme: (theme: IUserTheme) => void;
|
||||
isBaseTheme: boolean;
|
||||
}
|
||||
|
||||
const Login: React.FC<ILoginProps> = ({
|
||||
@ -59,6 +60,7 @@ const Login: React.FC<ILoginProps> = ({
|
||||
theme,
|
||||
setTheme,
|
||||
logoUrls,
|
||||
isBaseTheme,
|
||||
}) => {
|
||||
const isRestoringPortal =
|
||||
portalSettings?.tenantStatus === TenantStatus.PortalRestore;
|
||||
@ -266,6 +268,8 @@ const Login: React.FC<ILoginProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<LoginForm
|
||||
isBaseTheme={isBaseTheme}
|
||||
recaptchaPublicKey={portalSettings?.recaptchaPublicKey}
|
||||
isDesktop={!!isDesktopEditor}
|
||||
isLoading={isLoading}
|
||||
hashSettings={portalSettings?.passwordHash}
|
||||
@ -314,5 +318,6 @@ export default inject(({ loginStore }) => {
|
||||
return {
|
||||
theme: loginStore.theme,
|
||||
setTheme: loginStore.setTheme,
|
||||
isBaseTheme: loginStore.theme.isBase,
|
||||
};
|
||||
})(observer(Login));
|
||||
|
@ -22,6 +22,11 @@ interface ILoginContentProps {
|
||||
enabledJoin?: boolean;
|
||||
}
|
||||
|
||||
interface IStyledCaptchaProps {
|
||||
isCaptchaError?: boolean;
|
||||
theme?: IUserTheme;
|
||||
}
|
||||
|
||||
export const LoginFormWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-rows: ${(props: ILoginFormWrapperProps) =>
|
||||
@ -38,7 +43,7 @@ export const LoginFormWrapper = styled.div`
|
||||
}
|
||||
|
||||
.bg-cover {
|
||||
background-image: ${props => props.bgPattern};
|
||||
background-image: ${(props) => props.bgPattern};
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
background-size: cover;
|
||||
@ -55,20 +60,43 @@ export const LoginFormWrapper = styled.div`
|
||||
`;
|
||||
|
||||
export const LoginContent = styled.div`
|
||||
min-height: ${(props: ILoginContentProps) => props.enabledJoin ? "calc(100vh - 68px)" : "100vh"};
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
|
||||
@media ${hugeMobile} {
|
||||
min-height: 100%;
|
||||
justify-content: start;
|
||||
}
|
||||
min-height: ${(props: ILoginContentProps) =>
|
||||
props.enabledJoin ? "calc(100vh - 68px)" : "100vh"};
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
|
||||
@media ${hugeMobile} {
|
||||
min-height: 100%;
|
||||
justify-content: start;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledCaptcha = styled.div`
|
||||
margin: 24px 0;
|
||||
|
||||
width: fit-content;
|
||||
.captcha-wrapper {
|
||||
${(props: IStyledCaptchaProps) =>
|
||||
props.isCaptchaError &&
|
||||
css`
|
||||
border: ${props.theme.login.captcha.border};
|
||||
padding: 4px 4px 4px 2px;
|
||||
`};
|
||||
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
${(props: IStyledCaptchaProps) =>
|
||||
props.isCaptchaError &&
|
||||
css`
|
||||
p {
|
||||
color: ${props.theme.login.captcha.color};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
@ -16,6 +16,8 @@ import toastr from "@docspace/components/toast/toastr";
|
||||
import { thirdPartyLogin } from "@docspace/common/api/user";
|
||||
import { setWithCredentialsStatus } from "@docspace/common/api/client";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import { StyledCaptcha } from "../StyledLogin";
|
||||
|
||||
interface ILoginFormProps {
|
||||
isLoading: boolean;
|
||||
@ -25,6 +27,8 @@ interface ILoginFormProps {
|
||||
match: MatchType;
|
||||
onRecoverDialogVisible: () => void;
|
||||
enableAdmMess: boolean;
|
||||
recaptchaPublicKey: CaptchaPublicKeyType;
|
||||
isBaseTheme: boolean;
|
||||
}
|
||||
|
||||
const settings = {
|
||||
@ -43,7 +47,11 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
onRecoverDialogVisible,
|
||||
enableAdmMess,
|
||||
cookieSettingsEnabled,
|
||||
recaptchaPublicKey,
|
||||
isBaseTheme,
|
||||
}) => {
|
||||
const captchaRef = useRef(null);
|
||||
|
||||
const [isEmailErrorShow, setIsEmailErrorShow] = useState(false);
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [identifier, setIdentifier] = useState("");
|
||||
@ -53,9 +61,12 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isDialogVisible, setIsDialogVisible] = useState(false);
|
||||
const [isWithoutPasswordLogin, setIsWithoutPasswordLogin] = useState(
|
||||
IS_ROOMS_MODE
|
||||
);
|
||||
const [isCaptcha, setIsCaptcha] = useState(false);
|
||||
const [isWithoutPasswordLogin, setIsWithoutPasswordLogin] =
|
||||
useState(IS_ROOMS_MODE);
|
||||
|
||||
const [isCaptchaSuccessful, setIsCaptchaSuccess] = useState(false);
|
||||
const [isCaptchaError, setIsCaptchaError] = useState(false);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@ -142,6 +153,17 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
|
||||
const onSubmit = () => {
|
||||
//errorText && setErrorText("");
|
||||
let captchaToken = "";
|
||||
|
||||
if (recaptchaPublicKey && isCaptcha) {
|
||||
if (!isCaptchaSuccessful) {
|
||||
setIsCaptchaError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
captchaToken = captchaRef.current.getValue();
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
|
||||
const user = identifier.trim();
|
||||
@ -173,7 +195,8 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
|
||||
isDesktop && checkPwd();
|
||||
const session = !isChecked;
|
||||
login(user, hash, session)
|
||||
|
||||
login(user, hash, session, captchaToken)
|
||||
.then((res: string | object) => {
|
||||
const isConfirm = typeof res === "string" && res.includes("confirm");
|
||||
const redirectPath = sessionStorage.getItem("referenceUrl");
|
||||
@ -198,6 +221,14 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
errorMessage = error;
|
||||
}
|
||||
|
||||
if (recaptchaPublicKey && error?.response?.status === 403) {
|
||||
setIsCaptcha(true);
|
||||
}
|
||||
|
||||
if (isCaptcha) {
|
||||
captchaRef.current.reset();
|
||||
}
|
||||
|
||||
setIsEmailErrorShow(true);
|
||||
setErrorText(errorMessage);
|
||||
setPasswordValid(!errorMessage);
|
||||
@ -251,6 +282,10 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onSuccessfullyComplete = () => {
|
||||
setIsCaptchaSuccess(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="auth-form-container">
|
||||
<FieldContainer
|
||||
@ -356,6 +391,21 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
onDialogClose={onDialogClose}
|
||||
/>
|
||||
)}
|
||||
{recaptchaPublicKey && isCaptcha && (
|
||||
<StyledCaptcha isCaptchaError={isCaptchaError}>
|
||||
<div className="captcha-wrapper">
|
||||
<ReCAPTCHA
|
||||
sitekey={recaptchaPublicKey}
|
||||
ref={captchaRef}
|
||||
theme={isBaseTheme ? "light" : "dark"}
|
||||
onChange={onSuccessfullyComplete}
|
||||
/>
|
||||
</div>
|
||||
{isCaptchaError && (
|
||||
<Text>{t("Errors:LoginWithBruteForceCaptcha")}</Text>
|
||||
)}
|
||||
</StyledCaptcha>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -521,7 +521,7 @@ public class AuthenticationController : ControllerBase
|
||||
|
||||
var requestIp = MessageSettings.GetIP(Request);
|
||||
|
||||
(_, user) = await _bruteForceLoginManager.AttemptAsync(inDto.UserName, inDto.PasswordHash, requestIp);
|
||||
user = await _bruteForceLoginManager.AttemptAsync(inDto.UserName, inDto.PasswordHash, requestIp, inDto.RecaptchaResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -549,7 +549,12 @@ public class AuthenticationController : ControllerBase
|
||||
catch (BruteForceCredentialException)
|
||||
{
|
||||
await _messageService.SendAsync(!string.IsNullOrEmpty(inDto.UserName) ? inDto.UserName : AuditResource.EmailNotSpecified, MessageAction.LoginFailBruteForce);
|
||||
throw new AuthenticationException("Login Fail. Too many attempts");
|
||||
throw new BruteForceCredentialException(Resource.ErrorTooManyLoginAttempts);
|
||||
}
|
||||
catch (RecaptchaException)
|
||||
{
|
||||
await _messageService.SendAsync(!string.IsNullOrEmpty(inDto.UserName) ? inDto.UserName : AuditResource.EmailNotSpecified, MessageAction.LoginFailRecaptcha);
|
||||
throw new RecaptchaException(Resource.RecaptchaInvalid);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -437,7 +437,7 @@ public class SecurityController : BaseSettingsController
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(checkPeriod));
|
||||
}
|
||||
if (blockTime < 0)
|
||||
if (blockTime < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(blockTime));
|
||||
}
|
||||
|
@ -68,7 +68,11 @@ public class AuthRequestsDto
|
||||
|
||||
/// <summary>Confirmation data</summary>
|
||||
/// <type>ASC.Web.Api.ApiModel.RequestsDto.ConfirmData, ASC.Web.Api</type>
|
||||
public ConfirmData ConfirmData { get; set; }
|
||||
public ConfirmData ConfirmData { get; set; }
|
||||
|
||||
/// <summary>Recaptcha response</summary>
|
||||
/// <type>System.String, System</type>
|
||||
public string RecaptchaResponse { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -33,14 +33,23 @@ public class BruteForceLoginManager
|
||||
private readonly UserManager _userManager;
|
||||
private readonly TenantManager _tenantManager;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
private readonly SetupInfo _setupInfo;
|
||||
private readonly Recaptcha _recaptcha;
|
||||
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
|
||||
|
||||
public BruteForceLoginManager(SettingsManager settingsManager, UserManager userManager, TenantManager tenantManager, IDistributedCache distributedCache)
|
||||
public BruteForceLoginManager(SettingsManager settingsManager,
|
||||
UserManager userManager,
|
||||
TenantManager tenantManager,
|
||||
IDistributedCache distributedCache,
|
||||
SetupInfo setupInfo,
|
||||
Recaptcha recaptcha)
|
||||
{
|
||||
_settingsManager = settingsManager;
|
||||
_userManager = userManager;
|
||||
_tenantManager = tenantManager;
|
||||
_distributedCache = distributedCache;
|
||||
_setupInfo = setupInfo;
|
||||
_recaptcha = recaptcha;
|
||||
}
|
||||
|
||||
public async Task<(bool, bool)> IncrementAsync(string key, string requestIp, bool throwException, string exceptionMessage = null)
|
||||
@ -124,15 +133,17 @@ public class BruteForceLoginManager
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool, UserInfo)> AttemptAsync(string login, string passwordHash, string requestIp)
|
||||
public async Task<UserInfo> AttemptAsync(string login, string passwordHash, string requestIp, string recaptchaResponse)
|
||||
{
|
||||
UserInfo user = null;
|
||||
|
||||
var showRecaptcha = true;
|
||||
var secretEmail = SetupInfo.IsSecretEmail(login);
|
||||
|
||||
var recaptchaPassed = secretEmail || await CheckRecaptchaAsync(recaptchaResponse, requestIp);
|
||||
|
||||
var blockCacheKey = GetBlockCacheKey(login, requestIp);
|
||||
|
||||
if (GetFromCache<string>(blockCacheKey) != null)
|
||||
if (!recaptchaPassed && GetFromCache<string>(blockCacheKey) != null)
|
||||
{
|
||||
throw new BruteForceCredentialException();
|
||||
}
|
||||
@ -140,7 +151,7 @@ public class BruteForceLoginManager
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
if (GetFromCache<string>(blockCacheKey) != null)
|
||||
if (!recaptchaPassed && GetFromCache<string>(blockCacheKey) != null)
|
||||
{
|
||||
throw new BruteForceCredentialException();
|
||||
}
|
||||
@ -149,9 +160,8 @@ public class BruteForceLoginManager
|
||||
var now = DateTime.UtcNow;
|
||||
LoginSettingsWrapper settings = null;
|
||||
List<DateTime> history = null;
|
||||
var secretEmail = SetupInfo.IsSecretEmail(login);
|
||||
|
||||
if (!secretEmail)
|
||||
if (!recaptchaPassed)
|
||||
{
|
||||
historyCacheKey = GetHistoryCacheKey(login, requestIp);
|
||||
|
||||
@ -162,8 +172,6 @@ public class BruteForceLoginManager
|
||||
history = history.Where(item => item > checkTime).ToList();
|
||||
history.Add(now);
|
||||
|
||||
showRecaptcha = history.Count > settings.AttemptCount - 1;
|
||||
|
||||
if (history.Count > settings.AttemptCount)
|
||||
{
|
||||
SetToCache(blockCacheKey, "block", now.Add(settings.BlockTime));
|
||||
@ -184,7 +192,7 @@ public class BruteForceLoginManager
|
||||
throw new Exception("user not found");
|
||||
}
|
||||
|
||||
if (!secretEmail)
|
||||
if (!recaptchaPassed)
|
||||
{
|
||||
history.RemoveAt(history.Count - 1);
|
||||
|
||||
@ -200,7 +208,26 @@ public class BruteForceLoginManager
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
return (showRecaptcha, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private async Task<bool> CheckRecaptchaAsync(string recaptchaResponse, string requestIp)
|
||||
{
|
||||
var recaptchaPassed = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_setupInfo.RecaptchaPublicKey) &&
|
||||
!string.IsNullOrEmpty(_setupInfo.RecaptchaPrivateKey) &&
|
||||
!string.IsNullOrEmpty(recaptchaResponse))
|
||||
{
|
||||
recaptchaPassed = await _recaptcha.ValidateRecaptchaAsync(recaptchaResponse, requestIp);
|
||||
|
||||
if (!recaptchaPassed)
|
||||
{
|
||||
throw new RecaptchaException();
|
||||
}
|
||||
}
|
||||
|
||||
return recaptchaPassed;
|
||||
}
|
||||
|
||||
private T GetFromCache<T>(string key)
|
||||
|
@ -1842,6 +1842,15 @@ namespace ASC.Web.Core.PublicResources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Too many login attempts. Please try again later.
|
||||
/// </summary>
|
||||
public static string ErrorTooManyLoginAttempts {
|
||||
get {
|
||||
return ResourceManager.GetString("ErrorTooManyLoginAttempts", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The user could not be found.
|
||||
/// </summary>
|
||||
|
@ -991,4 +991,7 @@
|
||||
<data name="TariffsFeature_thirdparty" xml:space="preserve">
|
||||
<value>Third-party integrations</value>
|
||||
</data>
|
||||
<data name="ErrorTooManyLoginAttempts" xml:space="preserve">
|
||||
<value>Too many login attempts. Please try again later</value>
|
||||
</data>
|
||||
</root>
|
@ -169,9 +169,9 @@ public class SetupInfo
|
||||
SalesEmail = GetAppSettings("web.payment.email", "sales@onlyoffice.com");
|
||||
_webAutotestSecretEmail = (configuration["web:autotest:secret-email"] ?? "").Trim();
|
||||
|
||||
RecaptchaPublicKey = GetAppSettings("web.recaptcha.public-key", null);
|
||||
RecaptchaPrivateKey = GetAppSettings("web.recaptcha.private-key", "");
|
||||
RecaptchaVerifyUrl = GetAppSettings("web.recaptcha.verify-url", "https://www.recaptcha.net/recaptcha/api/siteverify");
|
||||
RecaptchaPublicKey = GetAppSettings("web:recaptcha:public-key", null);
|
||||
RecaptchaPrivateKey = GetAppSettings("web:recaptcha:private-key", null);
|
||||
RecaptchaVerifyUrl = GetAppSettings("web:recaptcha:verify-url", "https://www.recaptcha.net/recaptcha/api/siteverify");
|
||||
|
||||
_webDisplayMobappsBanner = (configuration["web.display.mobapps.banner"] ?? "").Trim().Split(new char[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
ShareTwitterUrl = GetAppSettings("web.share.twitter", "https://twitter.com/intent/tweet?text={0}");
|
||||
|
27
yarn.lock
27
yarn.lock
@ -3376,6 +3376,7 @@ __metadata:
|
||||
nodemon: ^2.0.22
|
||||
npm-run-all: ^4.1.5
|
||||
playwright: ^1.32.0
|
||||
react-google-recaptcha: ^3.1.0
|
||||
sass: ^1.59.3
|
||||
sass-loader: ^12.6.0
|
||||
serve: 14.2.0
|
||||
@ -20855,7 +20856,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.6, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
"prop-types@npm:^15.0.0, prop-types@npm:^15.5.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.6, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
version: 15.8.1
|
||||
resolution: "prop-types@npm:15.8.1"
|
||||
dependencies:
|
||||
@ -21250,6 +21251,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-async-script@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "react-async-script@npm:1.2.0"
|
||||
dependencies:
|
||||
hoist-non-react-statics: ^3.3.0
|
||||
prop-types: ^15.5.0
|
||||
peerDependencies:
|
||||
react: ">=16.4.1"
|
||||
checksum: 303890eeaf9e18d59fca77f9c891bf3b52d2ec9ea88f0af9d19c160a1f101b447c5104ca46e2dd84c19de756d4797f1f054d041b888a3d57204d9145f4b1b532
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-autosize-textarea@npm:^7.1.0":
|
||||
version: 7.1.0
|
||||
resolution: "react-autosize-textarea@npm:7.1.0"
|
||||
@ -21497,6 +21510,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-google-recaptcha@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "react-google-recaptcha@npm:3.1.0"
|
||||
dependencies:
|
||||
prop-types: ^15.5.0
|
||||
react-async-script: ^1.2.0
|
||||
peerDependencies:
|
||||
react: ">=16.4.1"
|
||||
checksum: 9dc64daf9684d979b1f66d97e00a42c9ceaa9b9fe8b29c4d02d77edf86781e2008f2ae1bef14509351ff3b2ffbcc26463f0d88dd612d7280b455b8c07101e663
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-hammerjs@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "react-hammerjs@npm:1.0.1"
|
||||
|
Loading…
Reference in New Issue
Block a user