Merge branch 'feature/ip-security' of github.com:ONLYOFFICE/AppServer into feature/admin-messages

This commit is contained in:
Viktor Fomin 2022-04-06 16:54:22 +03:00
commit ca7c998e69
25 changed files with 876 additions and 96 deletions

View File

@ -54,10 +54,17 @@ export function setDNSSettings(dnsName, enable) {
});
}
export function getIpRestrictions() {
return request({
method: "get",
url: "/settings/iprestrictions",
});
}
export function setIpRestrictions(data) {
return request({
method: "put",
url: "/settings/iprestrictions.json",
url: "/settings/iprestrictions",
data,
});
}
@ -65,7 +72,7 @@ export function setIpRestrictions(data) {
export function setIpRestrictionsEnable(data) {
return request({
method: "put",
url: "/settings/iprestrictions/settings.json",
url: "/settings/iprestrictions/settings",
data,
});
}

View File

@ -54,6 +54,8 @@ class AuthStore {
if (this.isAuthenticated) {
this.settingsStore.getPortalPasswordSettings();
this.tfaStore.getTfaType();
this.settingsStore.getIpRestrictions();
}
return Promise.all(requests);

View File

@ -31,7 +31,8 @@ class SettingsStore {
: Base;
trustedDomains = [];
trustedDomainsType = 0;
trustedDomains = [];
ipRestrictionEnable = false;
ipRestrictions = [];
timezone = "UTC";
timezones = [];
utcOffset = "00:00:00";
@ -443,6 +444,37 @@ class SettingsStore {
this.theme = themes[theme];
localStorage.setItem("theme", theme);
};
setMailDomainSettings = async (data) => {
const res = await api.settings.setMailDomainSettings(data);
this.trustedDomainsType = data.type;
this.trustedDomains = data.domains;
return res;
};
getIpRestrictions = async () => {
const res = await api.settings.getIpRestrictions();
if (res.length === 0) this.ipRestrictionEnabled = false;
else this.ipRestrictionEnabled = true;
};
setIpRestrictions = async (ips) => {
const data = {
ips: ips,
};
const res = await api.settings.setIpRestrictions(data);
console.log("setIpRestrictions", res);
this.ipRestrictions = ips;
};
setIpRestrictionsEnable = async (enable) => {
const data = {
enable: enable,
};
const res = await api.settings.setIpRestrictionsEnable(data);
console.log("setIpRestrictionsEnable", res);
this.ipRestrictionEnabled = enable;
};
}
export default SettingsStore;

View File

@ -4,6 +4,8 @@ import history from "../history";
class TfaStore {
tfaSettings = null;
smsAvailable = null;
appAvailable = null;
backupCodes = [];
tfaAndroidAppUrl =
"https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2";
@ -22,6 +24,8 @@ class TfaStore {
const type = sms ? "sms" : app ? "app" : "none";
this.tfaSettings = type;
this.smsAvailable = res[0].avaliable;
this.appAvailable = res[1].avaliable;
return type;
};

View File

@ -2551,6 +2551,7 @@ const Base = {
},
settings: {
iconFill: black,
article: {
titleColor: grayMain,
fillIcon: "dimgray",

View File

@ -2563,6 +2563,7 @@ const Dark = {
},
settings: {
iconFill: white,
article: {
titleColor: "#c4c4c4",
fillIcon: "#c4c4c4",

View File

@ -8,17 +8,20 @@
"AccessRightsUsersFromList": "{{users}} from the list",
"AccessSettings": "Access settings",
"AccessRightsSubTitle": "This section allows you to transfer portal owner rights and manage administrator access rights.",
"AddTrustedDomain": "Add trusted domain",
"AutoBackup": "Automatic backup",
"AutoBackupDescription": "Use this option for automatic backup of the portal data.",
"AutoBackupHelp": "The <strong>Automatic backup</strong> option is used to automate the portal data backup process to be able to restore it later to a local server.",
"AutoBackupHelpNote": "Choose the data storage, automatic backup period and maximal number of saved copies.<br/><strong>Note:</strong> before you can save the backup data to a third-party account (DropBox, Box.com, OneDrive or Google Drive), you will need to connect this account to {{organizationName}} Common folder.",
"AutoSavePeriod": "Autosave period",
"AddAdmins": "Add admins",
"AddAllowedIP": "Add allowed IP address",
"AddName": "Add Name",
"AdminInModules": "Admin in modules",
"AdministratorsAddedSuccessfully": "Administrators added successfully",
"AdministratorsRemovedSuccessfully": "Administrators removed successfully",
"Admins": "Admins",
"AllDomains": "Any domains",
"Backup": "Backup",
"BackupCreatedSuccess": "The backup copy has been successfully created.",
"BackupCreatedError": "An error has been encountered. Please contact your administrator.",
@ -36,6 +39,7 @@
"ConfirmEmailSended": "Confirmation e-mail has been sent to {{ownerName}}",
"Customization": "Customization",
"CustomizationDescription": "This subsection allows you to change the look and feel of your portal. You can use your own company logo, name and text to match your organization brand.",
"CustomDomains": "Custom domains",
"CustomTitles": "Custom titles",
"CustomTitlesFrom": "From",
"CustomTitlesSettingsDescription": "Welcome Page Settings is a way to change the default portal title to be displayed on the Welcome Page of your portal. The same name is also used for the From field of your portal email notifications.",
@ -62,6 +66,10 @@
"GroupLead": "Group Lead",
"Groups": "Groups",
"Guests": "Guests",
"IPSecurity": "IP Security",
"IPSecurityDescription": "IP Security is used to restrict login to the portal from all IP addresses except certain addresses.",
"IPSecurityHelper": "You can set the allowed IP addresses using either exact IP addresses in the IPv4 format (#.#.#.#, where # is a numeric value from 0 to 255) or IP range (in the #.#.#.#-#.#.#.# format).",
"IPSecurityWarningHelper": "First you need to specify your current IP or the IP range your current IP address belongs to, otherwise your portal access will be blocked right after you save the settings. The portal owner will have the portal access from any IP address.",
"Job/Title": "Job/Title",
"LanguageAndTimeZoneSettingsDescription": "Language and Time Zone Settings is a way to change the language of the whole portal for all portal users and to configure the time zone so that all the events of the ©linney portal will be shown with the correct date and time.",
"LanguageTimeSettingsTooltip": "<0>{{text}}</0> is a way to change the language of the whole portal for all portal users and to configure the time zone so that all the events of the ONLYOFFICE portal will be shown with the correct date and time.",
@ -133,6 +141,10 @@
"ThirdPartyStorage": "Third-party storage",
"ThirdPartyStorageDescription": "Backup can be saved to a third-party storage. Before, you need to connect the corresponding service in the 'Integration' section. Otherwise, the following settings will be inactive.",
"TimeZone": "Time Zone",
"TrustedMail": "Trusted mail domain settings",
"TrustedMailDescription": "Trusted Mail Domain Settings is a way to specify the mail servers used for user self-registration.",
"TrustedMailHelper": "You can either check the Custom domains option and enter the trusted mail server in the field below so that a person who has an account at it will be able to register him(her)self by clicking the Join link on the Sign In page or disable this option.",
"TrustedMailWarningHelper": "Users with trusted email domains will automatically be listed in the Waiting Room section of the Address book.",
"TwoFactorAuth": "Two-factor authentication",
"TwoFactorAuthDescription": "Two-factor authentication provides a more secure way to log in. After entering the credentials, the user will have to enter a code from an SMS or the authentication app. ",
"TwoFactorAuthHelper": "Note: SMS messages can be sent if you have a positive balance only. You can always check your current balance in your SMS provider account. Do not forget to replenish your balance in good time. ",

View File

@ -8,6 +8,7 @@
"AccessRightsUsersFromList": "Участников со статусом Пользователи из списка",
"AccessSettings": "Настройки доступа",
"AccessRightsSubTitle": "Данный раздел позволяет передавать права владельца портала и управлять правами доступа администраторов.",
"AddTrustedDomain": "Добавить доверенный домен",
"AutoBackup": "Автоматическое резервное копирование",
"AutoBackupDescription": "Используйте эту опцию для автоматического выполнения резервного копирования данных портала.",
"AutoBackupHelp": "Опция <strong>Автоматическое резервное копирование</strong> данных используется для автоматизации процесса создания резервных копий данных для последующего их восстановления на локальном сервере.",
@ -15,11 +16,13 @@
"AutoSavePeriod": "Период автоматического сохранения",
"AutoSavePeriodHelp": "Указанное ниже время соотвествует часовому поясу, выставленному на портале",
"AddAdmins": "Добавить администраторов",
"AddAllowedIP": "Добавить разрешенный IP-адрес",
"AddName": "Добавьте наименование",
"AdminInModules": "Администратор в модулях",
"AdministratorsAddedSuccessfully": "Администраторы успешно добавлены",
"AdministratorsRemovedSuccessfully": "Администраторы успешно удалены",
"Admins": "Администраторы",
"AllDomains": "Любые домены",
"Backup": "Резервное копирование",
"BackupCreatedSuccess": "Резервная копия успешно создана.",
"BackupCreatedError": "Произошла ошибка. Пожалуйста, обратитесь к администратору.",
@ -37,6 +40,7 @@
"ConfirmEmailSended": "Письмо с подтверждением отправлено {{ownerName}}",
"Customization": "Кастомизация",
"CustomizationDescription": "Этот раздел позволяет изменить оформление портала. Вы можете использовать логотип, название и слоган своей компании, чтобы портал соответствовал корпоративному стилю.",
"CustomDomains": "Пользовательские домены",
"CustomTitles": "Пользовательские заголовки",
"CustomTitlesFrom": "От кого",
"CustomTitlesSettingsDescription": "Настройки страницы приветствия позволяют изменить заголовок портала по умолчанию, который должен отображаться на странице приветствия вашего портала. Этот заголовок также используется в поле От кого в оповещениях портала.",
@ -63,6 +67,10 @@
"GroupLead": "Руководитель группы",
"Groups": "Группы",
"Guests": "Гости",
"IPSecurity": "IP-безопасность",
"IPSecurityDescription": "Настройки IP-безопасности используются для ограничения возможности входа на портал со всех IP-адресов, кроме указанных.",
"IPSecurityHelper": "Вы можете задать разрешенные IP-адреса, указав конкретные значения IP-адресов в формате IPv4 (#.#.#.#, где # - это число от 0 до 255) или диапазон IP-адресов (в формате #.#.#.#-#.#.#.#).",
"IPSecurityWarningHelper": "Первым необходимо указать ваш текущий IP-адрес или диапазон IP-адресов, в который входит ваш текущий IP, иначе после сохранения настроек Вам будет заблокирован доступ к порталу. Владелец портала будет иметь доступ с любого IP-адреса.",
"Job/Title": "Должность/Позиция",
"LanguageAndTimeZoneSettingsDescription": "Настройки языка и часового пояса позволяют изменить язык всего портала для всех пользователей и настроить часовой пояс, чтобы все события на портале ©linney отображались с корректной датой и временем.",
"LanguageTimeSettingsTooltip": "<0>{{text}}</0> позволяют изменить язык всего портала для всех пользователей и настроить часовой пояс, чтобы все события на портале ONLYOFFICE отображались с корректной датой и временем.",
@ -133,6 +141,10 @@
"ThirdPartyResource": "Сторонний ресурс",
"ThirdPartyResourceDescription": "Резервная копия может быть сохранена на вашем стороннем ресурсе (Dropbox, Box.com, OneDrive или Google Drive). Прежде чем Вы сможете сохранять резервные копии в стороннем аккаунте (Dropbox, Box.com, OneDrive или Google Drive), потребуется подключить его к папке 'Общие'",
"TimeZone": "Часовой пояс",
"TrustedMail": "Настройки доверенных почтовых доменов",
"TrustedMailDescription": "Настройки доверенных почтовых доменов позволяют указать почтовые серверы, которые могут использовать пользователи при самостоятельной регистрации в ONLYOFFICE.",
"TrustedMailHelper": "Можно отметить опцию Пользовательские домены и ввести доверенный почтовый сервер в поле ниже, чтобы любой сотрудник вашей компании, имеющий учетную запись на указанном почтовом сервере, смог зарегистрироваться самостоятельно, нажав ссылку Присоединиться на странице входа и введя адрес электронной почты с именем доверенного домена, который Вы добавили.",
"TrustedMailWarningHelper": "Пользователи с доверенными почтовыми доменами будут автоматически попадать в Waiting Room в разделе Address book.",
"TwoFactorAuth": "Двухфакторная аутентификация",
"TwoFactorAuthDescription": "Двухфакторная аутентификация обеспечивает более безопасный способ входа на портал. После ввода учетных данных пользователь должен будет ввести код из SMS или приложения для аутентификации.",
"TwoFactorAuthHelper": "Обратите внимание: отправка SMS-сообщений осуществляется только при положительном балансе. Вы всегда можете проверить текущий баланс в учетной записи вашего SMS-провайдера. Не забывайте своевременно пополнять баланс. ",

View File

@ -84,6 +84,7 @@ export const ButtonsWrapper = styled.div`
flex-direction: row;
gap: 8px;
align-items: center;
margin-top: 24px;
@media (max-width: 600px) {
position: absolute;

View File

@ -6,6 +6,8 @@ import { setDocumentTitle } from "../../../../../../helpers/utils";
import { MainContainer } from "../StyledSecurity";
import TfaSection from "./tfa";
import PasswordStrengthSection from "./passwordStrength";
import TrustedMailSection from "./trustedMail";
import IpSecuritySection from "./ipSecurity";
import MobileView from "./mobileView";
import CategoryWrapper from "../sub-components/category-wrapper";
import { size } from "@appserver/components/utils/device";
@ -42,6 +44,18 @@ const AccessPortal = (props) => {
tooltipContent={t("TwoFactorAuthDescription")}
/>
<TfaSection />
<hr />
<CategoryWrapper
title={t("TrustedMail")}
tooltipContent={t("TrustedMailDescription")}
/>
<TrustedMailSection />
<hr />
<CategoryWrapper
title={t("IPSecurity")}
tooltipContent={t("IPSecurityDescription")}
/>
<IpSecuritySection />
</MainContainer>
);
};

View File

@ -0,0 +1,225 @@
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import { withRouter } from "react-router";
import { withTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import Text from "@appserver/components/text";
import RadioButtonGroup from "@appserver/components/radio-button-group";
import toastr from "@appserver/components/toast/toastr";
import { LearnMoreWrapper } from "../StyledSecurity";
import UserFields from "../sub-components/user-fields";
import Buttons from "../sub-components/buttons";
import { size } from "@appserver/components/utils/device";
import { saveToSessionStorage, getFromSessionStorage } from "../../../utils";
import isEqual from "lodash/isEqual";
const MainContainer = styled.div`
width: 100%;
.page-subtitle {
margin-bottom: 10px;
}
.user-fields {
margin-bottom: 18px;
}
.box {
margin-bottom: 11px;
}
.warning-text {
margin-bottom: 9px;
}
`;
const IpSecurity = (props) => {
const {
t,
history,
ipRestrictionEnabled,
setIpRestrictionsEnable,
ipRestrictions,
setIpRestrictions,
} = props;
const regexp = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])/; //check ip valid
const [enable, setEnable] = useState(false);
const [ips, setIps] = useState();
const [showReminder, setShowReminder] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const getSettings = () => {
const currentSettings = getFromSessionStorage("currentIPSettings");
const defaultSettings = getFromSessionStorage("defaultIPSettings");
if (defaultSettings) {
saveToSessionStorage("defaultIPSettings", defaultSettings);
} else {
const defaultData = {
enable: ipRestrictionEnabled,
ips: ipRestrictions,
};
saveToSessionStorage("defaultIPSettings", defaultData);
}
if (currentSettings) {
setEnable(currentSettings.enable);
setIps(currentSettings.ips);
} else {
setEnable(ipRestrictionEnabled);
setIps(ipRestrictions);
}
setIsLoading(true);
};
useEffect(() => {
checkWidth();
getSettings();
window.addEventListener("resize", checkWidth);
return () => window.removeEventListener("resize", checkWidth);
}, [isLoading]);
useEffect(() => {
if (!isLoading) return;
const defaultSettings = getFromSessionStorage("defaultIPSettings");
const newSettings = {
enable: enable,
ips: ips,
};
saveToSessionStorage("currentIPSettings", newSettings);
if (isEqual(defaultSettings, newSettings)) {
setShowReminder(false);
} else {
setShowReminder(true);
}
}, [enable, ips]);
const checkWidth = () => {
window.innerWidth > size.smallTablet &&
history.location.pathname.includes("ip") &&
history.push("/settings/security/access-portal");
};
const onSelectType = (e) => {
setEnable(e.target.value === "enable" ? true : false);
};
const onChangeInput = (e, index) => {
let newInputs = Array.from(ips);
newInputs[index] = e.target.value;
setIps(newInputs);
};
const onDeleteInput = (index) => {
let newInputs = Array.from(ips);
newInputs.splice(index, 1);
setIps(newInputs);
};
const onClickAdd = () => {
setIps([...ips, ""]);
};
const onSaveClick = () => {
const valid = ips.map((ip) => regexp.test(ip));
if (valid.includes(false)) {
return;
}
setIpRestrictions(ips);
setIpRestrictionsEnable(enable);
saveToSessionStorage("defaultIPSettings", {
enable: enable,
ips: ips,
});
setShowReminder(false);
toastr.success(t("SuccessfullySaveSettingsMessage"));
};
const onCancelClick = () => {
const defaultSettings = getFromSessionStorage("defaultIPSettings");
setEnable(defaultSettings.enable);
setIps(defaultSettings.ips);
setShowReminder(false);
};
return (
<MainContainer>
<LearnMoreWrapper>
<Text className="page-subtitle">{t("IPSecurityHelper")}</Text>
</LearnMoreWrapper>
<RadioButtonGroup
className="box"
fontSize="13px"
fontWeight="400"
name="group"
orientation="vertical"
spacing="8px"
options={[
{
label: t("Disabled"),
value: "disabled",
},
{
label: t("Common:Enable"),
value: "enable",
},
]}
selected={enable ? "enable" : "disabled"}
onClick={onSelectType}
/>
{enable && (
<UserFields
className="user-fields"
inputs={ips}
buttonLabel={t("AddAllowedIP")}
onChangeInput={onChangeInput}
onDeleteInput={onDeleteInput}
onClickAdd={onClickAdd}
regexp={regexp}
/>
)}
<Text
color="#F21C0E"
fontSize="16px"
fontWeight="700"
className="warning-text"
>
{t("Common:Warning")}!
</Text>
<Text>{t("IPSecurityWarningHelper")}</Text>
<Buttons
t={t}
showReminder={showReminder}
onSaveClick={onSaveClick}
onCancelClick={onCancelClick}
/>
</MainContainer>
);
};
export default inject(({ auth }) => {
const {
ipRestrictionEnabled,
setIpRestrictionsEnable,
ipRestrictions,
setIpRestrictions,
} = auth.settingsStore;
return {
ipRestrictionEnabled,
setIpRestrictionsEnable,
ipRestrictions,
setIpRestrictions,
};
})(withTranslation(["Settings", "Common"])(withRouter(observer(IpSecurity))));

View File

@ -31,6 +31,18 @@ const MobileView = (props) => {
url="/settings/security/access-portal/tfa"
onClickLink={onClickLink}
/>
<MobileCategoryWrapper
title={t("TrustedMail")}
subtitle={t("TrustedMailDescription")}
url="/settings/security/access-portal/trusted-mail"
onClickLink={onClickLink}
/>
<MobileCategoryWrapper
title={t("IPSecurity")}
subtitle={t("IPSecurityDescription")}
url="/settings/security/access-portal/ip"
onClickLink={onClickLink}
/>
</MainContainer>
);
};

View File

@ -4,15 +4,15 @@ import { withRouter } from "react-router";
import { withTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import Box from "@appserver/components/box";
import Button from "@appserver/components/button";
import Text from "@appserver/components/text";
import Link from "@appserver/components/link";
import Slider from "@appserver/components/slider";
import Checkbox from "@appserver/components/checkbox";
import SectionLoader from "../sub-components/section-loader";
import { getLanguage } from "@appserver/common/utils";
import { ButtonsWrapper, LearnMoreWrapper } from "../StyledSecurity";
import { LearnMoreWrapper } from "../StyledSecurity";
import toastr from "@appserver/components/toast/toastr";
import Buttons from "../sub-components/buttons";
import { size } from "@appserver/components/utils/device";
import { saveToSessionStorage, getFromSessionStorage } from "../../../utils";
import isEqual from "lodash/isEqual";
@ -35,7 +35,6 @@ const MainContainer = styled.div`
flex-direction: column;
gap: 8px;
margin-top: 18px;
margin-bottom: 24px;
}
`;
@ -222,33 +221,12 @@ const PasswordStrength = (props) => {
/>
</Box>
<ButtonsWrapper>
<Button
label={t("Common:SaveButton")}
size="small"
primary={true}
className="button"
onClick={onSaveClick}
isDisabled={!showReminder}
/>
<Button
label={t("Common:CancelButton")}
size="small"
className="button"
onClick={onCancelClick}
isDisabled={!showReminder}
/>
{showReminder && (
<Text
color="#A3A9AE"
fontSize="12px"
fontWeight="600"
className="reminder"
>
{t("YouHaveUnsavedChanges")}
</Text>
)}
</ButtonsWrapper>
<Buttons
t={t}
showReminder={showReminder}
onSaveClick={onSaveClick}
onCancelClick={onCancelClick}
/>
</MainContainer>
);
};

View File

@ -4,14 +4,16 @@ import { withRouter } from "react-router";
import { withTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import RadioButtonGroup from "@appserver/components/radio-button-group";
import Button from "@appserver/components/button";
import Text from "@appserver/components/text";
import Link from "@appserver/components/link";
import toastr from "@appserver/components/toast/toastr";
import SectionLoader from "../sub-components/section-loader";
import { getLanguage } from "@appserver/common/utils";
import { ButtonsWrapper, LearnMoreWrapper } from "../StyledSecurity";
import Buttons from "../sub-components/buttons";
import { LearnMoreWrapper } from "../StyledSecurity";
import { size } from "@appserver/components/utils/device";
import { saveToSessionStorage, getFromSessionStorage } from "../../../utils";
import isEqual from "lodash/isEqual";
const MainContainer = styled.div`
width: 100%;
@ -21,28 +23,37 @@ const MainContainer = styled.div`
}
.box {
margin-bottom: 24px;
}
`;
const TwoFactorAuth = (props) => {
const { t, history } = props;
const [type, setType] = useState("none");
const [currentState, setCurrentState] = useState("");
const [smsDisabled, setSmsDisabled] = useState(false);
const [appDisabled, setAppDisabled] = useState(false);
const [showReminder, setShowReminder] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const getSettings = async () => {
const { getTfaType, getTfaSettings } = props;
const type = await getTfaType();
setType(type);
setCurrentState(type);
const getSettings = () => {
const { tfaSettings, smsAvailable, appAvailable } = props;
const currentSettings = getFromSessionStorage("currentTfaSettings");
const defaultSettings = getFromSessionStorage("defaultTfaSettings");
const settings = await getTfaSettings();
setSmsDisabled(settings[0].avaliable);
setAppDisabled(settings[1].avaliable);
if (defaultSettings) {
saveToSessionStorage("defaultTfaSettings", defaultSettings);
} else {
saveToSessionStorage("defaultTfaSettings", tfaSettings);
}
if (currentSettings) {
setType(currentSettings);
} else {
setType(tfaSettings);
}
setSmsDisabled(smsAvailable);
setAppDisabled(appAvailable);
setIsLoading(true);
};
@ -51,7 +62,20 @@ const TwoFactorAuth = (props) => {
getSettings();
window.addEventListener("resize", checkWidth);
return () => window.removeEventListener("resize", checkWidth);
}, []);
}, [isLoading]);
useEffect(() => {
if (!isLoading) return;
const defaultSettings = getFromSessionStorage("defaultTfaSettings");
saveToSessionStorage("currentTfaSettings", type);
if (defaultSettings === type) {
setShowReminder(false);
} else {
setShowReminder(true);
}
}, [type]);
const checkWidth = () => {
window.innerWidth > size.smallTablet &&
@ -62,10 +86,6 @@ const TwoFactorAuth = (props) => {
const onSelectTfaType = (e) => {
if (type !== e.target.value) {
setType(e.target.value);
setShowReminder(true);
}
if (e.target.value === currentState) {
setShowReminder(false);
}
};
@ -80,13 +100,16 @@ const TwoFactorAuth = (props) => {
);
}
setType(type);
saveToSessionStorage("defaultTfaSettings", type);
setShowReminder(false);
});
};
const onCancelClick = () => {
const defaultSettings = getFromSessionStorage("defaultTfaSettings");
setType(defaultSettings);
setShowReminder(false);
setType(currentState);
};
const lng = getLanguage(localStorage.getItem("language") || "en");
@ -132,52 +155,31 @@ const TwoFactorAuth = (props) => {
onClick={onSelectTfaType}
/>
<ButtonsWrapper>
<Button
label={t("Common:SaveButton")}
size="small"
primary={true}
className="button"
onClick={onSaveClick}
isDisabled={!showReminder}
/>
<Button
label={t("Common:CancelButton")}
size="small"
className="button"
onClick={onCancelClick}
isDisabled={!showReminder}
/>
{showReminder && (
<Text
color="#A3A9AE"
fontSize="12px"
fontWeight="600"
className="reminder"
>
{t("YouHaveUnsavedChanges")}
</Text>
)}
</ButtonsWrapper>
<Buttons
t={t}
showReminder={showReminder}
onSaveClick={onSaveClick}
onCancelClick={onCancelClick}
/>
</MainContainer>
);
};
export default inject(({ auth }) => {
const { organizationName } = auth.settingsStore;
const {
getTfaType,
getTfaSettings,
setTfaSettings,
getTfaConfirmLink,
tfaSettings,
smsAvailable,
appAvailable,
} = auth.tfaStore;
return {
organizationName,
getTfaType,
getTfaSettings,
setTfaSettings,
getTfaConfirmLink,
tfaSettings,
smsAvailable,
appAvailable,
};
})(
withTranslation(["Settings", "Common"])(withRouter(observer(TwoFactorAuth)))

View File

@ -0,0 +1,243 @@
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import { withRouter } from "react-router";
import { withTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import Text from "@appserver/components/text";
import Link from "@appserver/components/link";
import RadioButtonGroup from "@appserver/components/radio-button-group";
import { LearnMoreWrapper } from "../StyledSecurity";
import { getLanguage } from "@appserver/common/utils";
import toastr from "@appserver/components/toast/toastr";
import UserFields from "../sub-components/user-fields";
import Buttons from "../sub-components/buttons";
import { size } from "@appserver/components/utils/device";
import { saveToSessionStorage, getFromSessionStorage } from "../../../utils";
import isEqual from "lodash/isEqual";
const MainContainer = styled.div`
width: 100%;
.page-subtitle {
margin-bottom: 10px;
}
.user-fields {
margin-bottom: 18px;
}
.box {
margin-bottom: 11px;
}
.warning-text {
margin-bottom: 9px;
}
`;
const TrustedMail = (props) => {
const {
t,
history,
trustedDomainsType,
trustedDomains,
setMailDomainSettings,
} = props;
const regexp = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{1,})+/; //check domain name valid
const [type, setType] = useState("0");
const [domains, setDomains] = useState([]);
const [showReminder, setShowReminder] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const getSettings = async () => {
const currentSettings = getFromSessionStorage("currentTrustedMailSettings");
const defaultSettings = getFromSessionStorage("defaultTrustedMailSettings");
if (defaultSettings) {
saveToSessionStorage("defaultTrustedMailSettings", defaultSettings);
} else {
const defaultData = {
type: String(trustedDomainsType),
domains: trustedDomains,
};
saveToSessionStorage("defaultTrustedMailSettings", defaultData);
}
if (currentSettings) {
setType(currentSettings.type);
setDomains(currentSettings.domains);
} else {
setType(String(trustedDomainsType));
setDomains(trustedDomains);
}
setIsLoading(true);
};
useEffect(() => {
checkWidth();
getSettings();
window.addEventListener("resize", checkWidth);
return () => window.removeEventListener("resize", checkWidth);
}, [isLoading]);
useEffect(() => {
if (!isLoading) return;
const defaultSettings = getFromSessionStorage("defaultTrustedMailSettings");
const newSettings = {
type: type,
domains: domains,
};
saveToSessionStorage("currentTrustedMailSettings", newSettings);
if (isEqual(defaultSettings, newSettings)) {
setShowReminder(false);
} else {
setShowReminder(true);
}
}, [type, domains]);
const checkWidth = () => {
window.innerWidth > size.smallTablet &&
history.location.pathname.includes("trusted-mail") &&
history.push("/settings/security/access-portal");
};
const onSelectDomainType = (e) => {
if (type !== e.target.value) {
setType(e.target.value);
}
};
const onClickAdd = () => {
setDomains([...domains, ""]);
};
const onChangeInput = (e, index) => {
let newInputs = Array.from(domains);
newInputs[index] = e.target.value;
setDomains(newInputs);
};
const onDeleteInput = (index) => {
let newInputs = Array.from(domains);
newInputs.splice(index, 1);
setDomains(newInputs);
};
const onSaveClick = () => {
const valid = domains.map((domain) => regexp.test(domain));
if (valid.includes(false)) {
toastr.error(t("Common:IncorrectDomain"));
return;
}
const data = {
type: Number(type),
domains: domains,
inviteUsersAsVisitors: true,
};
setMailDomainSettings(data);
saveToSessionStorage("defaultTrustedMailSettings", {
type: type,
domains: domains,
});
setShowReminder(false);
toastr.success(t("SuccessfullySaveSettingsMessage"));
};
const onCancelClick = () => {
const defaultSettings = getFromSessionStorage("defaultTrustedMailSettings");
setType(defaultSettings.type);
setDomains(defaultSettings.domains);
setShowReminder(false);
};
const lng = getLanguage(localStorage.getItem("language") || "en");
return (
<MainContainer>
<LearnMoreWrapper>
<Text className="page-subtitle">{t("TrustedMailHelper")}</Text>
<Link
color="#316DAA"
target="_blank"
isHovered
href={`https://helpcenter.onlyoffice.com/${lng}/administration/configuration.aspx#ChangingSecuritySettings_block`}
>
{t("Common:LearnMore")}
</Link>
</LearnMoreWrapper>
<RadioButtonGroup
className="box"
fontSize="13px"
fontWeight="400"
name="group"
orientation="vertical"
spacing="8px"
options={[
{
label: t("Disabled"),
value: "0",
},
{
label: t("AllDomains"),
value: "2",
},
{
label: t("CustomDomains"),
value: "1",
},
]}
selected={type}
onClick={onSelectDomainType}
/>
{type === "1" && (
<UserFields
className="user-fields"
inputs={domains}
buttonLabel={t("AddTrustedDomain")}
onChangeInput={onChangeInput}
onDeleteInput={onDeleteInput}
onClickAdd={onClickAdd}
regexp={regexp}
/>
)}
<Text
color="#F21C0E"
fontSize="16px"
fontWeight="700"
className="warning-text"
>
{t("Common:Warning")}!
</Text>
<Text>{t("TrustedMailWarningHelper")}</Text>
<Buttons
t={t}
showReminder={showReminder}
onSaveClick={onSaveClick}
onCancelClick={onCancelClick}
/>
</MainContainer>
);
};
export default inject(({ auth, setup }) => {
const {
trustedDomainsType,
trustedDomains,
setMailDomainSettings,
} = auth.settingsStore;
return {
trustedDomainsType,
trustedDomains,
setMailDomainSettings,
};
})(withTranslation(["Settings", "Common"])(withRouter(observer(TrustedMail))));

View File

@ -0,0 +1,40 @@
import React from "react";
import Button from "@appserver/components/button";
import Text from "@appserver/components/text";
import { ButtonsWrapper } from "../StyledSecurity";
const Buttons = (props) => {
const { t, showReminder, onSaveClick, onCancelClick } = props;
return (
<ButtonsWrapper>
<Button
label={t("Common:SaveButton")}
size="small"
primary={true}
className="button"
onClick={onSaveClick}
isDisabled={!showReminder}
/>
<Button
label={t("Common:CancelButton")}
size="small"
className="button"
onClick={onCancelClick}
isDisabled={!showReminder}
/>
{showReminder && (
<Text
color="#A3A9AE"
fontSize="12px"
fontWeight="600"
className="reminder"
>
{t("YouHaveUnsavedChanges")}
</Text>
)}
</ButtonsWrapper>
);
};
export default Buttons;

View File

@ -0,0 +1,2 @@
export { default as PlusIcon } from "./plus.react.svg";
export { default as TrashIcon } from "./trash.react.svg";

View File

@ -0,0 +1,10 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_23038_6591)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.99998 7.00011L6.99998 11.0002H4.99998L4.99998 7.00028L1.00148 7.0006L1.00132 5.0006L4.99998 5.00028L4.99998 0.999416H6.99998L6.99998 5.00011L11.0003 4.99979L11.0004 6.99979L6.99998 7.00011Z" fill="#333333"/>
</g>
<defs>
<clipPath id="clip0_23038_6591">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View File

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_20466_224117)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.49945 0H8.49945C9.32788 0 9.99945 0.671573 9.99945 1.5V2H10.0005H13.0005C14.1053 2 15.0005 2.89593 15.0005 4.00022H13.0005V4H10.0005H8.49945H7.49945H6.00049H3.00049V4.00022H1.00049C1.00049 2.89593 1.89564 2 3.00049 2H5.99945V1.5C5.99945 0.671573 6.67102 0 7.49945 0ZM3.00049 5.00023V13.0002C3.00049 14.6571 4.34364 16.0002 6.00049 16.0002H10.0005C11.6573 16.0002 13.0005 14.6571 13.0005 13.0002V5.00023H11.0005V13.0002C11.0005 13.5525 10.5528 14.0002 10.0005 14.0002H6.00049C5.4482 14.0002 5.00049 13.5525 5.00049 13.0002V5.00023H3.00049ZM7.00049 6V14H9.00049V6H7.00049Z" fill="#A3A9AE"/>
</g>
<defs>
<clipPath id="clip0_20466_224117">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 896 B

View File

@ -0,0 +1,87 @@
import React, { useState } from "react";
import styled from "styled-components";
import commonIconsStyles from "@appserver/components/utils/common-icons-style";
import { PlusIcon, TrashIcon } from "./svg";
import Link from "@appserver/components/link";
import TextInput from "@appserver/components/text-input";
import { Base } from "@appserver/components/themes";
const StyledPlusIcon = styled(PlusIcon)`
${commonIconsStyles}
path {
fill: ${(props) => props.theme.studio.settings.iconFill};
}
`;
StyledPlusIcon.defaultProps = { theme: Base };
const StyledTrashIcon = styled(TrashIcon)`
${commonIconsStyles}
cursor: pointer;
`;
const StyledInputWrapper = styled.div`
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
margin-bottom: 8px;
width: 370px;
`;
const StyledAddWrapper = styled.div`
display: flex;
flex-direction: row;
gap: 6px;
align-items: center;
cursor: pointer;
`;
const UserFields = (props) => {
const {
className,
buttonLabel,
onChangeInput,
onDeleteInput,
onClickAdd,
inputs,
regexp,
} = props;
return (
<div className={className}>
{inputs ? (
inputs.map((input, index) => {
const error = !regexp.test(input);
return (
<StyledInputWrapper key={`domain-input-${index}`}>
<TextInput
id={`domain-input-${index}`}
value={input}
onChange={(e) => onChangeInput(e, index)}
hasError={error}
/>
<StyledTrashIcon
size="medium"
onClick={() => onDeleteInput(index)}
/>
</StyledInputWrapper>
);
})
) : (
<></>
)}
<StyledAddWrapper onClick={onClickAdd}>
<StyledPlusIcon size="small" />
<Link type="action" isHovered={true}>
{buttonLabel}
</Link>
</StyledAddWrapper>
</div>
);
};
export default UserFields;

View File

@ -11,6 +11,12 @@ const TfaPage = lazy(() => import("./categories/security/access-portal/tfa"));
const PasswordStrengthPage = lazy(() =>
import("./categories/security/access-portal/passwordStrength")
);
const TrustedMailPage = lazy(() =>
import("./categories/security/access-portal/trustedMail")
);
const IpSecurityPage = lazy(() =>
import("./categories/security/access-portal/ipSecurity")
);
const CommonSettings = lazy(() => import("./categories/common/index.js"));
@ -85,6 +91,14 @@ const PASSWORD_PAGE_URL = combineUrl(
PROXY_BASE_URL,
"/security/access-portal/password"
);
const TRUSTED_MAIL_PAGE_URL = combineUrl(
PROXY_BASE_URL,
"/security/access-portal/trusted-mail"
);
const IP_SECURITY_PAGE_URL = combineUrl(
PROXY_BASE_URL,
"/security/access-portal/ip"
);
const ADMINS_URL = combineUrl(PROXY_BASE_URL, "/security/access-rights/admins");
const THIRD_PARTY_URL = combineUrl(
@ -128,6 +142,12 @@ const Settings = () => {
path={PASSWORD_PAGE_URL}
component={PasswordStrengthPage}
/>
<Route
exact
path={TRUSTED_MAIL_PAGE_URL}
component={TrustedMailPage}
/>
<Route exact path={IP_SECURITY_PAGE_URL} component={IpSecurityPage} />
<Route exact path={THIRD_PARTY_URL} component={ThirdPartyServices} />
<Route

View File

@ -71,6 +71,18 @@ export const settingsTree = [
link: "tfa",
tKey: "TwoFactorAuth",
},
{
key: "1-0-2",
icon: "",
link: "trusted-mail",
tKey: "TrustedMail",
},
{
key: "1-0-3",
icon: "",
link: "ip",
tKey: "IPSecurity",
},
],
},
{

View File

@ -227,22 +227,10 @@ class SettingsSetupStore {
const res = await api.portal.setPortalRename(alias);
};
setMailDomainSettings = async (data) => {
const res = await api.settings.setMailDomainSettings(data);
};
setDNSSettings = async (dnsName, enable) => {
const res = await api.settings.setMailDomainSettings(dnsName, enable);
};
setIpRestrictions = async (data) => {
const res = await api.settings.setIpRestrictions(data);
};
setIpRestrictionsEnable = async (data) => {
const res = await api.settings.setIpRestrictionsEnable(data);
};
setMessageSettings = async (turnOn) => {
const res = await api.settings.setMessageSettings(turnOn);
};

View File

@ -795,3 +795,67 @@ Scenario("Setting password strength change test error", async ({ I }) => {
I.see("Error");
}
});
Scenario("Trusted mail settings change test success", async ({ I }) => {
I.mockEndpoint(Endpoints.settings, "settings");
I.mockEndpoint(Endpoints.build, "build");
I.mockEndpoint(Endpoints.info, "infoSettings");
I.mockEndpoint(Endpoints.self, "selfSettings");
I.mockEndpoint(Endpoints.common, "common");
if (deviceType === "mobile") {
I.amOnPage("/settings/security/access-portal/trusted-mail");
I.see("Trusted mail domain settings");
I.click({
react: "Checkbox",
props: {
value: "1",
},
});
I.see("You have unsaved changes");
I.click("Add trusted domain");
I.see({ react: "TextInput" });
I.fillField("#domain-input-0", "test.com");
I.click("Save");
I.dontSee("You have unsaved changes");
I.see("Settings have been successfully updated");
}
});
Scenario("Trusted mail settings change test error", async ({ I }) => {
I.mockEndpoint(Endpoints.settings, "settings");
I.mockEndpoint(Endpoints.build, "build");
I.mockEndpoint(Endpoints.info, "infoSettings");
I.mockEndpoint(Endpoints.self, "selfSettings");
I.mockEndpoint(Endpoints.common, "common");
if (deviceType === "mobile") {
I.amOnPage("/settings/security/access-portal/trusted-mail");
I.see("Trusted mail domain settings");
I.click({
react: "Checkbox",
props: {
value: "1",
},
});
I.see("You have unsaved changes");
I.click("Add trusted domain");
I.see({ react: "TextInput" });
I.fillField("#domain-input-0", "test");
I.click("Save");
I.see("You have unsaved changes");
I.see("Incorrect domain");
}
});

View File

@ -2,6 +2,7 @@
"count": 1,
"response": {
"trustedDomainsType": 0,
"trustedDomains": [],
"culture": "en-US",
"utcOffset": {
"ticks": 0,