Merge branch 'develop' into refactoring/files-list

This commit is contained in:
Artem Tarasov 2021-04-23 14:47:40 +03:00
commit 39780ab498
39 changed files with 1022 additions and 120 deletions

View File

@ -90,6 +90,11 @@ server {
root $public_root;
try_files /offline/$basename /index.html =404;
}
location ~* /thirdparty/ {
root $public_root;
try_files /thirdparty/third-party.html /index.html =404;
}
}
location /login {

View File

@ -197,6 +197,29 @@ export function updateUserType(type, userIds) {
});
}
export function linkOAuth(serializedProfile) {
return request({
method: "put",
url: "people/thirdparty/linkaccount.json",
data: { serializedProfile },
});
}
export function signupOAuth(signupAccount) {
return request({
method: "post",
url: "people/thirdparty/signup.json",
data: signupAccount,
});
}
export function unlinkOAuth(provider) {
return request({
method: "delete",
url: `people/thirdparty/unlinkaccount.json?provider=${provider}`,
});
}
export function sendInstructionsToDelete() {
return request({
method: "put",

View File

@ -233,6 +233,13 @@ export function getConsumersList() {
});
}
export function getAuthProviders() {
return request({
method: "get",
url: `/settings/authproviders`,
});
}
export function updateConsumerProps(newProps) {
const options = {
method: "post",

View File

@ -13,6 +13,14 @@ export function login(userName, passwordHash) {
});
}
export function thirdPartyLogin(SerializedProfile) {
return request({
method: "post",
url: "authentication.json",
data: { SerializedProfile },
});
}
export function logout() {
return request({
method: "post",

View File

@ -108,7 +108,25 @@ export const ConflictResolveType = Object.freeze({
Overwrite: 1,
Duplicate: 2,
});
export const providersData = Object.freeze({
Google: {
label: "SignInWithGoogle",
icon: "/static/images/share.google.react.svg",
},
Facebook: {
label: "SignInWithFacebook",
icon: "/static/images/share.facebook.react.svg",
},
Twitter: {
label: "SignInWithTwitter",
icon: "/static/images/share.twitter.react.svg",
iconOptions: { color: "#2AA3EF" },
},
LinkedIn: {
label: "SignInWithLinkedIn",
icon: "/static/images/share.linkedin.react.svg",
},
});
export const i18nBaseSettings = {
lng: localStorage.getItem(LANGUAGE) || "en",
supportedLngs: ["en", "ru"],

View File

@ -20,6 +20,8 @@ class AuthStore {
isAuthenticated = false;
version = null;
providers = [];
constructor() {
this.userStore = new UserStore();
this.moduleStore = new ModuleStore();
@ -154,6 +156,22 @@ class AuthStore {
}
};
thirdPartyLogin = async (SerializedProfile) => {
try {
const response = await api.user.thirdPartyLogin(SerializedProfile);
if (!response || !response.token) throw "Empty API response";
setWithCredentialsStatus(true);
await this.init();
return Promise.resolve(true);
} catch (e) {
return Promise.reject(e);
}
};
reset = () => {
this.userStore = new UserStore();
this.moduleStore = new ModuleStore();
@ -250,6 +268,10 @@ class AuthStore {
setProductVersion = (version) => {
this.version = version;
};
setProviders = (providers) => {
this.providers = providers;
};
}
export default new AuthStore();

View File

@ -183,6 +183,33 @@ class SettingsStore {
this.updateEncryptionKeys(encryptionKeys);
};
getOAuthToken = (tokenGetterWin) => {
return new Promise((resolve, reject) => {
localStorage.removeItem("code");
let interval = null;
interval = setInterval(() => {
try {
const code = localStorage.getItem("code");
if (code) {
localStorage.removeItem("code");
clearInterval(interval);
resolve(code);
} else if (tokenGetterWin && tokenGetterWin.closed) {
clearInterval(interval);
reject();
}
} catch {
return;
}
}, 500);
});
};
getLoginLink = (token, code) => {
return combineUrl(proxyURL, `/login.ashx?p=${token}&code=${code}`);
};
setModuleInfo = (homepage, productId) => {
if (this.homepage == homepage) return;
this.homepage = homepage;

View File

@ -0,0 +1,48 @@
import React from "react";
import PropTypes from "prop-types";
import equal from "fast-deep-equal/react";
import Text from "../text";
import StyledFacebookButton from "./styled-facebook-button";
import { ReactSVG } from "react-svg";
// eslint-disable-next-line no-unused-vars
class FacebookButton extends React.Component {
shouldComponentUpdate(nextProps) {
return !equal(this.props, nextProps);
}
render() {
const { label, iconName, ...otherProps } = this.props;
return (
<StyledFacebookButton {...otherProps}>
<ReactSVG className="iconWrapper" src={iconName} />
{label && (
<Text as="span" className="social_button_text">
{label}
</Text>
)}
</StyledFacebookButton>
);
}
}
FacebookButton.propTypes = {
label: PropTypes.string,
iconName: PropTypes.string,
tabIndex: PropTypes.number,
isDisabled: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
onClick: PropTypes.func,
$iconOptions: PropTypes.object,
};
FacebookButton.defaultProps = {
tabIndex: -1,
isDisabled: false,
$iconOptions: {},
};
export default FacebookButton;

View File

@ -0,0 +1,77 @@
import React from "react";
import styled, { css } from "styled-components";
import Base from "../themes/base";
import PropTypes from "prop-types";
const ButtonWrapper = ({ label, iconName, isDisabled, noHover, ...props }) => (
<button type="button" {...props}></button>
);
ButtonWrapper.propTypes = {
label: PropTypes.string,
iconName: PropTypes.string,
tabIndex: PropTypes.number,
isDisabled: PropTypes.bool,
onClick: PropTypes.func,
$iconOptions: PropTypes.object,
};
const StyledFacebookButton = styled(ButtonWrapper).attrs((props) => ({
disabled: props.isDisabled ? "disabled" : "",
tabIndex: props.tabIndex,
}))`
border: none;
display: flex;
align-items: center;
background-color: #ffffff;
border: 1px solid #1877f2;
border-radius: 3px;
width: 100%;
${(props) => !props.noHover && "cursor: pointer;"}
padding: 0;
outline: none;
touch-callout: none;
-o-touch-callout: none;
-moz-touch-callout: none;
-webkit-touch-callout: none;
svg {
margin: 11px;
width: 18px;
height: 18px;
min-width: 18px;
min-height: 18px;
}
${(props) =>
props.$iconOptions &&
props.$iconOptions.color &&
css`
svg {
path {
fill: ${props.$iconOptions.color};
}
}
`}
.iconWrapper {
display: flex;
pointer-events: none;
}
.social_button_text {
pointer-events: none;
font-family: Roboto, "Open Sans", sans-serif, Arial;
font-style: normal;
font-weight: 600;
font-size: 14px;
line-height: 14px;
color: #1877f2;
margin: 0 11px;
}
`;
StyledFacebookButton.defaultProps = { theme: Base };
export default StyledFacebookButton;

View File

@ -13,10 +13,10 @@ class SocialButton extends React.Component {
}
render() {
const { label, iconName } = this.props;
const { label, iconName, ...otherProps } = this.props;
return (
<StyledSocialButton {...this.props}>
<ReactSVG src={iconName} />
<StyledSocialButton {...otherProps}>
<ReactSVG className="iconWrapper" src={iconName} />
{label && (
<Text as="span" className="social_button_text">
{label}
@ -42,6 +42,8 @@ SocialButton.propTypes = {
id: PropTypes.string,
/** Accepts css style */
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
onClick: PropTypes.func,
$iconOptions: PropTypes.object,
};
SocialButton.defaultProps = {
@ -49,6 +51,7 @@ SocialButton.defaultProps = {
iconName: "SocialButtonGoogleIcon",
tabIndex: -1,
isDisabled: false,
$iconOptions: {},
};
export default SocialButton;

View File

@ -1,8 +1,9 @@
import React from "react";
import styled, { css } from "styled-components";
import Base from "../themes/base";
import PropTypes from "prop-types";
const ButtonWrapper = ({ label, iconName, isDisabled, ...props }) => (
const ButtonWrapper = ({ label, iconName, isDisabled, noHover, ...props }) => (
<button type="button" {...props}></button>
);
@ -12,6 +13,7 @@ ButtonWrapper.propTypes = {
tabIndex: PropTypes.number,
isDisabled: PropTypes.bool,
onClick: PropTypes.func,
$iconOptions: PropTypes.object,
};
const StyledSocialButton = styled(ButtonWrapper).attrs((props) => ({
@ -20,7 +22,8 @@ const StyledSocialButton = styled(ButtonWrapper).attrs((props) => ({
}))`
font-family: ${(props) => props.theme.fontFamily};
border: none;
display: inline-block;
display: flex;
align-items: center;
font-weight: ${(props) => props.theme.socialButton.fontWeight};
text-decoration: ${(props) => props.theme.socialButton.textDecoration};
@ -42,6 +45,15 @@ const StyledSocialButton = styled(ButtonWrapper).attrs((props) => ({
outline: ${(props) => props.theme.socialButton.outline};
}
${(props) =>
props.$iconOptions &&
props.$iconOptions.color &&
css`
svg path {
fill: ${props.$iconOptions.color};
}
`}
${(props) =>
!props.isDisabled
? css`
@ -49,20 +61,27 @@ const StyledSocialButton = styled(ButtonWrapper).attrs((props) => ({
box-shadow: ${(props) => props.theme.socialButton.boxShadow};
color: ${(props) => props.theme.socialButton.color};
:hover,
:active {
cursor: pointer;
box-shadow: ${(props) => props.theme.socialButton.hoverBoxShadow};
}
${(props) =>
!props.noHover &&
css`
:hover,
:active {
cursor: pointer;
box-shadow: ${(props) =>
props.theme.socialButton.hoverBoxShadow};
}
:hover {
background: ${(props) => props.theme.socialButton.hoverBackground};
}
:hover {
background: ${(props) =>
props.theme.socialButton.hoverBackground};
}
:active {
background: ${(props) => props.theme.socialButton.activeBackground};
border: none;
}
:active {
background: ${(props) =>
props.theme.socialButton.activeBackground};
border: none;
}
`}
`
: css`
box-shadow: none;
@ -75,8 +94,14 @@ const StyledSocialButton = styled(ButtonWrapper).attrs((props) => ({
}
`};
.iconWrapper {
display: flex;
pointer-events: none;
}
.social_button_text {
position: absolute;
position: relative;
pointer-events: none;
width: ${(props) => props.theme.socialButton.text.width};
height: ${(props) => props.theme.socialButton.text.height};

View File

@ -213,14 +213,13 @@ const Base = {
socialButton: {
fontWeight: "600",
textDecoration: "none",
margin: "20px 0 0 20px",
padding: "0",
borderRadius: "2px",
width: "201px",
height: "40px",
textAlign: "left",
stroke: " none",
outline: "none",
width: "100%",
background: white,
disableBackgroundColor: "rgba(0, 0, 0, 0.08)",
@ -236,16 +235,17 @@ const Base = {
disableColor: "rgba(0, 0, 0, 0.4)",
text: {
width: "142px",
width: "100%",
height: "16px",
margin: "12px 9px 12px 10px",
fontWeight: "500",
margin: "0 11px",
fontWeight: "600",
fontSize: "14px",
lineHeight: "16px",
lineHeight: "14px",
letterSpacing: "0.21875px",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
color: "#757575",
},
svg: {

View File

@ -129,6 +129,7 @@ const PureThirdPartyListContainer = ({
openConnectWindow(data.title, authModal).then((modal) => {
redirectAction();
getOAuthToken(modal).then((token) => {
authModal.close();
const serviceData = {
title: data.title,
provider_key: data.title,
@ -216,6 +217,7 @@ const ThirdPartyList = withTranslation("Article")(
export default inject(
({
filesStore,
auth,
settingsStore,
treeFoldersStore,
selectedFolderStore,
@ -231,16 +233,16 @@ export default inject(
oneDriveConnectItem,
nextCloudConnectItem,
webDavConnectItem,
getOAuthToken,
openConnectWindow,
} = settingsStore.thirdPartyStore;
const { getOAuthToken } = auth.settingsStore;
const {
setConnectItem,
setConnectDialogVisible,
setThirdPartyDialogVisible,
} = dialogsStore;
return {
googleConnectItem,
boxConnectItem,

View File

@ -185,7 +185,10 @@ const PureConnectDialogContainer = (props) => {
const onReconnect = () => {
let authModal = window.open("", "Authorization", "height=600, width=1020");
openConnectWindow(title, authModal).then((modal) =>
getOAuthToken(modal).then((token) => setToken(token))
getOAuthToken(modal).then((token) => {
authModal.close();
setToken(token);
})
);
};
@ -321,6 +324,7 @@ const ConnectDialog = withTranslation("ConnectDialog")(
export default inject(
({
auth,
filesStore,
settingsStore,
treeFoldersStore,
@ -329,12 +333,12 @@ export default inject(
}) => {
const {
providers,
getOAuthToken,
saveThirdParty,
openConnectWindow,
fetchThirdPartyProviders,
} = settingsStore.thirdPartyStore;
const { fetchFiles } = filesStore;
const { getOAuthToken } = auth.settingsStore;
const {
treeFolders,

View File

@ -105,11 +105,13 @@ const ThirdPartyDialog = (props) => {
);
openConnectWindow(item.title, authModal).then((modal) =>
getOAuthToken(modal).then((token) => {
authModal.close();
showOAuthModal(token, item);
setConnectItem(item);
setConnectDialogVisible(true);
})
);
} else {
setConnectItem(item);
setConnectDialogVisible(true);
}
setThirdPartyDialogVisible(false);
@ -242,7 +244,6 @@ export default inject(({ auth, settingsStore, dialogsStore }) => {
ownCloudConnectItem,
webDavConnectItem,
sharePointConnectItem,
getOAuthToken,
openConnectWindow,
} = settingsStore.thirdPartyStore;
const {
@ -251,6 +252,7 @@ export default inject(({ auth, settingsStore, dialogsStore }) => {
setConnectDialogVisible,
setConnectItem,
} = dialogsStore;
const { getOAuthToken } = auth.settingsStore;
return {
visible,

View File

@ -25,7 +25,6 @@ class ThirdPartyStore {
setThirdPartyCapabilities: action,
fetchThirdPartyProviders: action,
deleteThirdParty: action,
getOAuthToken: action,
openConnectWindow: action,
});
}
@ -66,25 +65,6 @@ class ThirdPartyStore {
);
};
getOAuthToken = () => {
return new Promise((resolve) => {
localStorage.removeItem("code");
const interval = setInterval(() => {
try {
const code = localStorage.getItem("code");
if (code) {
localStorage.removeItem("code");
clearInterval(interval);
resolve(code);
}
} catch {
return;
}
}, 500);
});
};
convertServiceName = (serviceName) => {
//Docusign, OneDrive, Wordpress
switch (serviceName) {

View File

@ -1,3 +1,22 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 0H2C0.897 0 0 0.897 0 2V14C0 15.103 0.897 16 2 16H8V10.1848H5.81641V8H8V5.90666C8 4.24966 9.343 2.90666 11 2.90666H13.0877V5.09417H11.912C11.36 5.09417 10.912 5.26478 10.912 5.81678V8H13.5L12.5 10.1848H10.912V16H14C15.103 16 16 15.103 16 14V2C16 0.897 15.103 0 14 0Z" fill="#333333"/>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2255 5108 c-105 -12 -298 -51 -409 -83 -994 -288 -1724 -1155 -1835
-2177 -14 -125 -14 -451 0 -576 99 -919 703 -1723 1562 -2081 170 -71 487
-161 566 -161 l21 0 0 895 0 895 -325 0 -325 0 0 370 0 370 325 0 325 0 0 359
c0 397 6 462 61 617 107 305 331 497 659 564 94 20 128 21 325 17 184 -4 433
-26 483 -42 9 -3 12 -76 12 -318 l0 -314 -212 -6 c-263 -7 -317 -20 -402 -98
-108 -99 -126 -176 -126 -556 l0 -223 355 0 c312 0 355 -2 355 -15 0 -17 -96
-652 -106 -698 l-6 -27 -299 0 -299 0 0 -895 0 -895 21 0 c79 0 396 90 566
161 859 358 1463 1162 1562 2081 14 125 14 451 0 576 -128 1181 -1080 2133
-2261 2261 -115 13 -479 12 -593 -1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.64 9.20419C17.64 8.56601 17.5827 7.95237 17.4764 7.36328H9V10.8446H13.8436C13.635 11.9696 13.0009 12.9228 12.0477 13.561V15.8192H14.9564C16.6582 14.2524 17.64 11.9451 17.64 9.20419Z" fill="#4285F4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 18.0009C11.43 18.0009 13.4673 17.195 14.9564 15.8205L12.0477 13.5623C11.2418 14.1023 10.2109 14.4214 9 14.4214C6.65591 14.4214 4.67182 12.8382 3.96409 10.7109H0.957275V13.0428C2.43818 15.9841 5.48182 18.0009 9 18.0009Z" fill="#34A853"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.96409 10.7088C3.78409 10.1688 3.68182 9.59203 3.68182 8.99885C3.68182 8.40567 3.78409 7.82885 3.96409 7.28885V4.95703H0.957273C0.347727 6.17203 0 7.54658 0 8.99885C0 10.4511 0.347727 11.8257 0.957273 13.0407L3.96409 10.7088Z" fill="#FBBC05"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 3.57955C10.3214 3.57955 11.5077 4.03364 12.4405 4.92545L15.0218 2.34409C13.4632 0.891818 11.4259 0 9 0C5.48182 0 2.43818 2.01682 0.957275 4.95818L3.96409 7.29C4.67182 5.16273 6.65591 3.57955 9 3.57955Z" fill="#EA4335"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75036 8.05344C9.74586 8.06301 9.73967 8.07201 9.73292 8.08044H9.75036V8.05344Z" fill="#1276B3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.25 0C1.00736 0 0 1.00736 0 2.25V15.75C0 16.9926 1.00736 18 2.25 18H15.75C16.9926 18 18 16.9926 18 15.75V2.25C18 1.00736 16.9926 0 15.75 0H2.25ZM2.89855 6.91044H5.5828V14.9846H2.89855V6.91044ZM4.25755 3.01794C5.17611 3.01794 5.74142 3.62094 5.7583 4.41407C5.7583 5.18751 5.17611 5.80851 4.24011 5.80851H4.22267C3.32211 5.80851 2.73992 5.18751 2.73992 4.41407C2.73992 3.62094 3.34011 3.01794 4.25755 3.01794ZM12.1697 6.72144C13.9354 6.72144 15.26 7.87513 15.26 10.3546L15.2595 14.9846H12.5758V10.6646C12.5758 9.57782 12.1882 8.83869 11.2157 8.83869C10.4754 8.83869 10.0333 9.34044 9.83867 9.82082C9.7678 9.99463 9.75036 10.2314 9.75036 10.4756V14.9846H7.06723C7.06723 14.9846 7.10155 7.66757 7.06723 6.91044H9.75036V8.05344C10.1087 7.50388 10.746 6.72144 12.1697 6.72144Z" fill="#1276B3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -48,5 +48,16 @@
"selectNewPhotoLabel": "Select new photo",
"orDropFileHereLabel": "or drop file here",
"maxSizeFileError": "Maximum file size exceeded",
"editAvatar": "Edit"
"editAvatar": "Edit",
"LoginSettings": "Login settings",
"SignInWithGoogle": "Sign in with Google",
"SignInWithFacebook": "Sign in with Facebook",
"SignInWithTwitter": "Sign in with Twitter",
"SignInWithLinkedIn": "Sign in with LinkedIn",
"Connect": "Connect",
"Disconnect": "Disconnect",
"ProviderSuccessfullyConnected": "Provider successfully connected",
"ProviderSuccessfullyDisconnected": "Provider successfully disconnected"
}

View File

@ -60,4 +60,4 @@
"Calendar": "Calendar",
"ViewAccess": "View",
"ProfileAction": "Profile action"
}
}

View File

@ -48,5 +48,16 @@
"selectNewPhotoLabel": "Выбрать новое фото",
"orDropFileHereLabel": "или перетащите файл сюда",
"maxSizeFileError": "Превышен максимальный размер файла",
"editAvatar": "Изменить"
"editAvatar": "Изменить",
"LoginSettings": "Вход через социальные сети",
"SignInWithGoogle": "Вход через Google",
"SignInWithFacebook": "Вход через Facebook",
"SignInWithTwitter": "Вход через Twitter",
"SignInWithLinkedIn": "Вход через LinkedIn",
"Connect": "Подключить",
"Disconnect": "Отключить",
"ProviderSuccessfullyConnected": "Провайдер успешно подключен",
"ProviderSuccessfullyDisconnected": "Провайдер успешно отключен"
}

View File

@ -59,4 +59,4 @@
"Calendar": "Календарь",
"ViewAccess": "Просмотр",
"ProfileAction": "Работа с профилем"
}
}

View File

@ -2,9 +2,12 @@ import Avatar from "@appserver/components/avatar";
import Button from "@appserver/components/button";
import IconButton from "@appserver/components/icon-button";
import Text from "@appserver/components/text";
import SocialButton from "@appserver/components/social-button";
import FacebookButton from "@appserver/components/facebook-button";
import ToggleContent from "@appserver/components/toggle-content";
import Link from "@appserver/components/link";
import ProfileInfo from "./ProfileInfo/ProfileInfo";
import toastr from "studio/toastr";
import React from "react";
import { combineUrl, isMe } from "@appserver/common/utils";
import styled from "styled-components";
@ -17,7 +20,9 @@ import {
getUserRole,
} from "../../../../helpers/people-helpers";
import config from "../../../../../package.json";
import { AppServerConfig } from "@appserver/common/constants";
import { AppServerConfig, providersData } from "@appserver/common/constants";
import { unlinkOAuth, linkOAuth } from "@appserver/common/api/people";
import { getAuthProviders } from "@appserver/common/api/settings";
const ProfileWrapper = styled.div`
display: flex;
@ -59,6 +64,13 @@ const ContactWrapper = styled.div`
}
`;
const ProviderButtonsWrapper = styled.div`
align-items: center;
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 16px 22px;
`;
const createContacts = (contacts) => {
const styledContacts = contacts.map((contact, index) => {
let url = null;
@ -88,12 +100,30 @@ const stringFormat = (string, data) =>
string.replace(/\{(\d+)\}/g, (m, n) => data[n] || m);
class SectionBodyContent extends React.PureComponent {
componentDidMount() {
const { cultures, getPortalCultures, profile, viewer, isSelf } = this.props;
async componentDidMount() {
const {
cultures,
getPortalCultures,
profile,
viewer,
isSelf,
setProviders,
} = this.props;
//const isSelf = isMe(viewer, profile.userName);
if (isSelf && !cultures.length) {
getPortalCultures();
}
if (!isSelf) return;
try {
await getAuthProviders().then((providers) => {
setProviders(providers);
});
} catch (e) {
console.error(e);
}
window.loginCallback = this.loginCallback;
}
onEditSubscriptionsClick = () => console.log("Edit subscriptions onClick()");
@ -110,6 +140,113 @@ class SectionBodyContent extends React.PureComponent {
);
};
loginCallback = (profile) => {
const { setProviders, t } = this.props;
linkOAuth(profile.Serialized).then((resp) => {
getAuthProviders().then((providers) => {
setProviders(providers);
toastr.success(t("ProviderSuccessfullyConnected"));
});
});
};
unlinkAccount = (providerName) => {
const { setProviders, t } = this.props;
unlinkOAuth(providerName).then(() => {
getAuthProviders().then((providers) => {
setProviders(providers);
toastr.success(t("ProviderSuccessfullyDisconnected"));
});
});
};
linkAccount = (providerName, link, e) => {
const { getOAuthToken, getLoginLink } = this.props;
e.preventDefault();
try {
const tokenGetterWin = window.open(
link,
"login",
"width=800,height=500,status=no,toolbar=no,menubar=no,resizable=yes,scrollbars=no"
);
getOAuthToken(tokenGetterWin).then((code) => {
const token = window.btoa(
JSON.stringify({
auth: providerName,
mode: "popup",
callback: "loginCallback",
})
);
tokenGetterWin.location.href = getLoginLink(token, code);
});
} catch (err) {
console.log(err);
}
};
providerButtons = () => {
const { t, providers } = this.props;
const providerButtons =
providers &&
providers.map((item) => {
const { icon, label, iconOptions } = providersData[item.provider];
if (!icon || !label) return <React.Fragment></React.Fragment>;
return (
<React.Fragment key={`${item.provider}ProviderItem`}>
<div>
{item.provider === "Facebook" ? (
<FacebookButton
noHover={true}
iconName={icon}
label={t(label)}
className="socialButton"
$iconOptions={iconOptions}
/>
) : (
<SocialButton
noHover={true}
iconName={icon}
label={t(label)}
className="socialButton"
$iconOptions={iconOptions}
/>
)}
</div>
{item.linked ? (
<div>
<Link
type="action"
color="A3A9AE"
onClick={(e) => this.unlinkAccount(item.provider, e)}
isHovered={true}
>
{t("Disconnect")}
</Link>
</div>
) : (
<div>
<Link
type="action"
color="A3A9AE"
onClick={(e) => this.linkAccount(item.provider, item.url, e)}
isHovered={true}
>
{t("Connect")}
</Link>
</div>
)}
</React.Fragment>
);
});
return providerButtons;
};
render() {
const {
profile,
@ -119,6 +256,7 @@ class SectionBodyContent extends React.PureComponent {
viewer,
t,
isSelf,
providers,
} = this.props;
const contacts = profile.contacts && getUserContacts(profile.contacts);
@ -161,6 +299,16 @@ class SectionBodyContent extends React.PureComponent {
cultures={cultures}
culture={culture}
/>
{isSelf && providers && providers.length > 0 && (
<ToggleWrapper>
<ToggleContent label={t("LoginSettings")} isOpen={true}>
<ProviderButtonsWrapper>
{this.providerButtons()}
</ProviderButtonsWrapper>
</ToggleContent>
</ToggleWrapper>
)}
{isSelf && false && (
<ToggleWrapper isSelf={true}>
<ToggleContent label={t("Subscriptions")} isOpen={true}>
@ -213,5 +361,9 @@ export default withRouter(
isSelf: peopleStore.targetUserStore.isMe,
avatarMax: peopleStore.avatarEditorStore.avatarMax,
setAvatarMax: peopleStore.avatarEditorStore.setAvatarMax,
providers: peopleStore.usersStore.providers,
setProviders: peopleStore.usersStore.setProviders,
getOAuthToken: auth.settingsStore.getOAuthToken,
getLoginLink: auth.settingsStore.getLoginLink,
}))(observer(withTranslation("Profile")(SectionBodyContent)))
);

View File

@ -927,5 +927,6 @@ export default withRouter(
updateProfile: peopleStore.targetUserStore.updateProfile,
getUserPhoto: peopleStore.targetUserStore.getUserPhoto,
disableProfileType: peopleStore.targetUserStore.getDisableProfileType,
isSelf: peopleStore.targetUserStore.isMe,
}))(observer(withTranslation("ProfileAction")(UpdateUserForm)))
);

View File

@ -2,6 +2,9 @@ import React from "react";
import PropTypes from "prop-types";
import PageLayout from "@appserver/common/components/PageLayout";
import Loaders from "@appserver/common/components/Loaders";
import toastr from "studio/toastr";
import { linkOAuth } from "@appserver/common/api/people";
import { getAuthProviders } from "@appserver/common/api/settings";
import {
ArticleHeaderContent,
ArticleMainButtonContent,
@ -118,6 +121,7 @@ ProfileAction.propTypes = {
export default withRouter(
inject(({ auth, peopleStore }) => ({
setProviders: peopleStore.usersStore.setProviders,
setDocumentTitle: auth.setDocumentTitle,
isEdit: peopleStore.editingFormStore.isEdit,
setIsEditingForm: peopleStore.editingFormStore.setIsEditingForm,

View File

@ -10,13 +10,16 @@ import store from "studio/store";
const { auth: authStore } = store;
class UsersStore {
users = [];
providers = [];
constructor(peopleStore) {
this.peopleStore = peopleStore;
makeObservable(this, {
users: observable,
providers: observable,
getUsersList: action,
setUsers: action,
setProviders: action,
createUser: action,
removeUser: action,
updateUserStatus: action,
@ -52,6 +55,10 @@ class UsersStore {
this.users = users;
};
setProviders = (providers) => {
this.providers = providers;
};
employeeWrapperToMemberModel = (profile) => {
const comment = profile.notes;
const department = profile.groups

View File

@ -1528,17 +1528,17 @@ namespace ASC.Employee.Core.Controllers
[AllowAnonymous]
[Create("thirdparty/signup")]
public void SignupAccountFromBody([FromBody] LinkAccountModel model)
public void SignupAccountFromBody([FromBody] SignupAccountModel model)
{
LinkAccount(model);
SignupAccount(model);
}
[AllowAnonymous]
[Update("thirdparty/linkaccount")]
[Create("thirdparty/signup")]
[Consumes("application/x-www-form-urlencoded")]
public void SignupAccountFromForm([FromForm] LinkAccountModel model)
public void SignupAccountFromForm([FromForm] SignupAccountModel model)
{
LinkAccount(model);
SignupAccount(model);
}
public void SignupAccount(SignupAccountModel model)
@ -1554,18 +1554,41 @@ namespace ASC.Employee.Core.Controllers
}
var thirdPartyProfile = new LoginProfile(Signature, InstanceCrypto, model.SerializedProfile);
var newUser = CreateNewUser(GetFirstName(model, thirdPartyProfile), GetLastName(model, thirdPartyProfile), GetEmailAddress(model, thirdPartyProfile), passwordHash, employeeType, false);
var messageAction = employeeType == EmployeeType.User ? MessageAction.UserCreatedViaInvite : MessageAction.GuestCreatedViaInvite;
MessageService.Send(MessageInitiator.System, messageAction, MessageTarget.Create(newUser.ID), newUser.DisplayUserName(false, DisplayUserSettingsHelper));
var userID = newUser.ID;
if (!string.IsNullOrEmpty(thirdPartyProfile.Avatar))
if (!string.IsNullOrEmpty(thirdPartyProfile.AuthorizationError))
{
SaveContactImage(userID, thirdPartyProfile.Avatar);
// ignore cancellation
if (thirdPartyProfile.AuthorizationError != "Canceled at provider")
throw new Exception(thirdPartyProfile.AuthorizationError);
return;
}
GetLinker().AddLink(userID.ToString(), thirdPartyProfile);
if (string.IsNullOrEmpty(thirdPartyProfile.EMail))
{
throw new Exception(Resource.ErrorNotCorrectEmail);
}
var userID = Guid.Empty;
try
{
SecurityContext.AuthenticateMe(ASC.Core.Configuration.Constants.CoreSystem);
var newUser = CreateNewUser(GetFirstName(model, thirdPartyProfile), GetLastName(model, thirdPartyProfile), GetEmailAddress(model, thirdPartyProfile), passwordHash, employeeType, false);
var messageAction = employeeType == EmployeeType.User ? MessageAction.UserCreatedViaInvite : MessageAction.GuestCreatedViaInvite;
MessageService.Send(MessageInitiator.System, messageAction, MessageTarget.Create(newUser.ID), newUser.DisplayUserName(false, DisplayUserSettingsHelper));
userID = newUser.ID;
if (!string.IsNullOrEmpty(thirdPartyProfile.Avatar))
{
SaveContactImage(userID, thirdPartyProfile.Avatar);
}
GetLinker().AddLink(userID.ToString(), thirdPartyProfile);
}
finally
{
SecurityContext.Logout();
}
var user = UserManager.GetUsers(userID);
var cookiesKey = SecurityContext.AuthenticateMe(user.Email, passwordHash);
@ -1581,6 +1604,7 @@ namespace ASC.Employee.Core.Controllers
UserHelpTourHelper.IsNewUser = true;
if (CoreBaseSettings.Personal)
PersonalSettingsHelper.IsNewUser = true;
}
[Create("phone")]

BIN
public/images/clouds.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 0C6.61305 0 4.32387 0.948211 2.63604 2.63604C0.948211 4.32387 0 6.61305 0 9C0 11.3869 0.948211 13.6761 2.63604 15.364C4.32387 17.0518 6.61305 18 9 18C11.3869 18 13.6761 17.0518 15.364 15.364C17.0518 13.6761 18 11.3869 18 9C18 6.61305 17.0518 4.32387 15.364 2.63604C13.6761 0.948211 11.3869 0 9 0Z" fill="#1877F2"/>
<path d="M10.1047 11.5264H12.5862L12.9758 9.21077H10.1042V7.94516C10.1042 6.98321 10.4464 6.1302 11.4259 6.1302H13V4.10941C12.7234 4.0751 12.1385 4 11.0333 4C8.72546 4 7.37245 5.11957 7.37245 7.67025V9.21077H5V11.5264H7.37245V17.8911C7.84229 17.956 8.31819 18 8.80671 18C9.2483 18 9.67928 17.9629 10.1047 17.9101V11.5264Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 768 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4 0H1.6C0.72 0 0 0.72 0 1.6V14.4C0 15.28 0.72 16 1.6 16H14.4C15.28 16 16 15.28 16 14.4V1.6C16 0.72 15.28 0 14.4 0ZM12.56 5.84C12.48 9.52 10.16 12.08 6.64 12.24C5.2 12.32 4.16 11.84 3.2 11.28C4.24 11.44 5.6 11.04 6.32 10.4C5.28 10.32 4.64 9.76 4.32 8.88C4.64 8.96 4.96 8.88 5.2 8.88C4.24 8.56 3.6 8 3.52 6.72C3.76 6.88 4.08 6.96 4.4 6.96C3.68 6.56 3.2 5.04 3.76 4.08C4.8 5.2 6.08 6.16 8.16 6.32C7.6 4.08 10.64 2.88 11.84 4.4C12.4 4.32 12.8 4.08 13.2 3.92C13.04 4.48 12.72 4.8 12.32 5.12C12.72 5.04 13.12 4.96 13.44 4.8C13.36 5.2 12.96 5.52 12.56 5.84Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 725 B

85
public/thirdparty/third-party.html vendored Normal file
View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="favicon.ico" />
<title>Third party</title>
<style type="text/css">
#third-party-body {
cursor: default;
height: 100%;
width: 100%;
min-height: 100%;
overflow-x: hidden;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 36px 36px 0 36px;
border: 0;
box-sizing: border-box;
font-family: "Open Sans", sans-serif;
}
#container {
position: relative;
margin: 12px 0 60px 0;
}
.text {
font-size: 18px;
line-height: 22px;
text-align: center;
margin: auto;
max-width: 560px;
padding: 0;
}
img {
display: block;
width: 100%;
}
</style>
<script>
function getObjectByLocation(location) {
if (!location.search || !location.search.length) return null;
const searchUrl = location.search.substring(1);
const object = JSON.parse(
'{"' +
decodeURIComponent(searchUrl)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"') +
'"}'
);
return object;
}
function renderError(error) {
var container = document.getElementById("container");
container.innerHTML = `<img src="/static/images/clouds.png" /><p class="text">${error}</p>`;
}
const urlParams = getObjectByLocation(window.location);
const code = urlParams ? urlParams.code || null : null;
const error = urlParams ? urlParams.error || null : null;
</script>
</head>
<body id="third-party-body">
<div id="container"></div>
</body>
<script>
if (code) {
localStorage.setItem("code", code);
} else if (error) {
renderError(error);
}
</script>
</html>

View File

@ -26,5 +26,12 @@
"ConfirmOwnerPortalTitle": "Please confirm that you want to change portal owner to {{newOwner}}",
"SaveButton": "Save",
"CancelButton": "Cancel",
"ConfirmOwnerPortalSuccessMessage": "Portal owner has been successfully changed. {0}In 10 seconds you will be redirected {1}here{2}"
"ConfirmOwnerPortalSuccessMessage": "Portal owner has been successfully changed. {0}In 10 seconds you will be redirected {1}here{2}",
"SignInWithGoogle": "Sign in with Google",
"SignInWithFacebook": "Sign in with Facebook",
"SignInWithTwitter": "Sign in with Twitter",
"SignInWithLinkedIn": "Sign in with LinkedIn",
"ProviderLoginError": "Authorization error",
"ProviderNotConnected": "Provider is not connected to your account"
}

View File

@ -21,5 +21,12 @@
"ConfirmOwnerPortalTitle": "Пожалуйста, подтвердите, что Вы хотите изменить владельца портала на {{newOwner}}",
"SaveButton": "Сохранить",
"CancelButton": "Отмена",
"ConfirmOwnerPortalSuccessMessage": "Portal owner has been successfully changed. {0}In 10 seconds you will be redirected {1}here{2}"
"ConfirmOwnerPortalSuccessMessage": "Portal owner has been successfully changed. {0}In 10 seconds you will be redirected {1}here{2}",
"SignInWithGoogle": "Вход через Google",
"SignInWithFacebook": "Вход через Facebook",
"SignInWithTwitter": "Вход через Twitter",
"SignInWithLinkedIn": "Вход через LinkedIn",
"ProviderLoginError": "Ошибка авторизации",
"ProviderNotConnected": "Провайдер не подключен к вашему аккаунту"
}

View File

@ -49,10 +49,6 @@ const COMING_SOON_URLS = [
//combineUrl(PROXY_HOMEPAGE_URL, "/products/calendar"),
//combineUrl(PROXY_HOMEPAGE_URL, "/products/talk/"),
];
const THIRD_PARTY_RESPONSE_URL = combineUrl(
PROXY_HOMEPAGE_URL,
"/thirdparty/:provider"
);
const PAYMENTS_URL = combineUrl(PROXY_HOMEPAGE_URL, "/payments");
const SETTINGS_URL = combineUrl(PROXY_HOMEPAGE_URL, "/settings");
const ERROR_401_URL = combineUrl(PROXY_HOMEPAGE_URL, "/error401");
@ -66,9 +62,6 @@ const About = React.lazy(() => import("./components/pages/About"));
const Wizard = React.lazy(() => import("./components/pages/Wizard"));
const Settings = React.lazy(() => import("./components/pages/Settings"));
const ComingSoon = React.lazy(() => import("./components/pages/ComingSoon"));
const ThirdPartyResponse = React.lazy(() =>
import("./components/pages/ThirdParty")
);
const Confirm = React.lazy(() => import("./components/pages/Confirm"));
const SettingsRoute = (props) => (
@ -149,14 +142,6 @@ const ComingSoonRoute = (props) => (
</React.Suspense>
);
const ThirdPartyResponseRoute = (props) => (
<React.Suspense fallback={<AppLoader />}>
<ErrorBoundary>
<ThirdPartyResponse {...props} />
</ErrorBoundary>
</React.Suspense>
);
const Shell = ({ items = [], page = "home", ...rest }) => {
const { isLoaded, loadBaseInfo, modules } = rest;
@ -174,7 +159,6 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
LOGIN_URLS,
CONFIRM_URL,
COMING_SOON_URLS,
THIRD_PARTY_RESPONSE_URL,
PAYMENTS_URL,
SETTINGS_URL,
ERROR_401_URL,
@ -251,10 +235,6 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
path={COMING_SOON_URLS}
component={ComingSoonRoute}
/>
<PrivateRoute
path={THIRD_PARTY_RESPONSE_URL}
component={ThirdPartyResponseRoute}
/>
<PrivateRoute path={PAYMENTS_URL} component={PaymentsRoute} />
<PrivateRoute
restricted

View File

@ -4,28 +4,44 @@ import { withTranslation } from "react-i18next";
import styled from "styled-components";
import PropTypes from "prop-types";
import axios from "axios";
import { createUser } from "@appserver/common/api/people";
import { createUser, signupOAuth } from "@appserver/common/api/people";
import { inject, observer } from "mobx-react";
import Button from "@appserver/components/button";
import TextInput from "@appserver/components/text-input";
import Box from "@appserver/components/box";
import Text from "@appserver/components/text";
import PasswordInput from "@appserver/components/password-input";
import toastr from "@appserver/components/toast/toastr";
import Loader from "@appserver/components/loader";
import SocialButton from "@appserver/components/social-button";
import FacebookButton from "@appserver/components/facebook-button";
import EmailInput from "@appserver/components/email-input";
import { getAuthProviders } from "@appserver/common/api/settings";
import PageLayout from "@appserver/common/components/PageLayout";
import { combineUrl, createPasswordHash } from "@appserver/common/utils";
import { AppServerConfig } from "@appserver/common/constants";
import { AppServerConfig, providersData } from "@appserver/common/constants";
import { isMobile } from "react-device-detect";
const inputWidth = "400px";
const ButtonsWrapper = styled.div`
display: table;
margin: -6px;
margin-top: 17px;
margin-right: auto;
`;
const ConfirmContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-left: 200px;
.buttonWrapper {
margin: 6px;
min-width: 225px;
}
@media (max-width: 830px) {
margin-left: 40px;
}
@ -150,6 +166,99 @@ class Confirm extends React.PureComponent {
});
};
addFacebookToStart = (facebookIndex, providerButtons) => {
const { providers, t } = this.props;
const faceBookData = providers[facebookIndex];
const { icon, label, iconOptions } = providersData[faceBookData.provider];
providerButtons.unshift(
<div
className="buttonWrapper"
key={`${faceBookData.provider}ProviderItem`}
>
<FacebookButton
iconName={icon}
label={t(label)}
className="socialButton"
$iconOptions={iconOptions}
data-url={faceBookData.url}
data-providername={faceBookData.provider}
onClick={this.onSocialButtonClick}
/>
</div>
);
};
providerButtons = () => {
const { providers, t } = this.props;
let facebookIndex = null;
const providerButtons =
providers &&
providers.map((item, index) => {
const { icon, label, iconOptions, className } = providersData[
item.provider
];
if (!icon) return;
if (item.provider === "Facebook") {
facebookIndex = index;
return;
}
return (
<div className="buttonWrapper" key={`${item.provider}ProviderItem`}>
<SocialButton
iconName={icon}
label={t(label)}
className={`socialButton ${className ? className : ""}`}
$iconOptions={iconOptions}
data-url={item.url}
data-providername={item.provider}
onClick={this.onSocialButtonClick}
/>
</div>
);
});
if (facebookIndex) this.addFacebookToStart(facebookIndex, providerButtons);
return providerButtons;
};
authCallback = (profile) => {
const { t, defaultPage } = this.props;
const { FirstName, LastName, EMail, Serialized } = profile;
console.log(profile);
const signupAccount = {
EmployeeType: null,
FirstName: FirstName,
LastName: LastName,
Email: EMail,
PasswordHash: "",
SerializedProfile: Serialized,
};
signupOAuth(signupAccount)
.then(() => {
window.location.replace(defaultPage);
})
.catch((e) => {
toastr.error(e);
});
};
setProviders = async () => {
const { setProviders } = this.props;
try {
await getAuthProviders().then((providers) => {
setProviders(providers);
});
} catch (e) {
console.error(e);
}
};
createConfirmUser = async (registerData, loginData, key) => {
const data = Object.assign(
{ fromInviteLink: true },
@ -171,6 +280,35 @@ class Confirm extends React.PureComponent {
return user;
};
onSocialButtonClick = (e) => {
const providerName = e.target.dataset.providername;
const url = e.target.dataset.url;
const { getOAuthToken, getLoginLink } = this.props;
try {
const tokenGetterWin = window.open(
url,
"login",
"width=800,height=500,status=no,toolbar=no,menubar=no,resizable=yes,scrollbars=no"
);
getOAuthToken(tokenGetterWin).then((code) => {
const token = window.btoa(
JSON.stringify({
auth: providerName,
mode: "popup",
callback: "authCallback",
})
);
tokenGetterWin.location.href = getLoginLink(token, code);
});
} catch (err) {
console.log(err);
}
};
onKeyPress = (event) => {
if (event.key === "Enter") {
this.onSubmit();
@ -191,6 +329,9 @@ class Confirm extends React.PureComponent {
history.push(combineUrl(AppServerConfig.proxyURL, `/login/error=${e}`));
});
this.setProviders();
window.authCallback = this.authCallback;
window.addEventListener("keydown", this.onKeyPress);
window.addEventListener("keyup", this.onKeyPress);
}
@ -230,7 +371,7 @@ class Confirm extends React.PureComponent {
};
render() {
const { settings, t, greetingTitle } = this.props;
const { settings, t, greetingTitle, providers } = this.props;
//console.log("createUser render");
@ -352,6 +493,11 @@ class Confirm extends React.PureComponent {
onClick={this.onSubmit}
/>
</div>
{providers && providers.length > 0 && (
<Box>
<ButtonsWrapper>{this.providerButtons()}</ButtonsWrapper>
</Box>
)}
{/* <Row className='confirm-row'>
@ -381,7 +527,15 @@ const CreateUserForm = (props) => (
);
export default inject(({ auth }) => {
const { login, logout, isAuthenticated, settingsStore } = auth;
const {
login,
logout,
isAuthenticated,
settingsStore,
setProviders,
providers,
thirdPartyLogin,
} = auth;
const {
passwordSettings,
greetingSettings,
@ -389,6 +543,8 @@ export default inject(({ auth }) => {
defaultPage,
getSettings,
getPortalPasswordSettings,
getOAuthToken,
getLoginLink,
} = settingsStore;
return {
@ -401,5 +557,10 @@ export default inject(({ auth }) => {
logout,
getSettings,
getPortalPasswordSettings,
thirdPartyLogin,
getOAuthToken,
getLoginLink,
setProviders,
providers,
};
})(withRouter(withTranslation("Confirm")(observer(CreateUserForm))));

View File

@ -26,5 +26,12 @@
"CookieSettingsTitle": "Session Lifetime",
"Authorization": "Authorization",
"Or": "OR",
"Register": "Register"
"Register": "Register",
"SignInWithGoogle": "Sign in with Google",
"SignInWithFacebook": "Sign in with Facebook",
"SignInWithTwitter": "Sign in with Twitter",
"SignInWithLinkedIn": "Sign in with LinkedIn",
"ProviderLoginError": "Authorization error",
"ProviderNotConnected": "Provider is not connected to your account"
}

View File

@ -26,5 +26,12 @@
"CookieSettingsTitle": "Время жизни сессии",
"Authorization": "Авторизация",
"Or": "ИЛИ",
"Register": "Регистрация"
"Register": "Регистрация",
"SignInWithGoogle": "Вход через Google",
"SignInWithFacebook": "Вход через Facebook",
"SignInWithTwitter": "Вход через Twitter",
"SignInWithLinkedIn": "Вход через LinkedIn",
"ProviderLoginError": "Ошибка авторизации",
"ProviderNotConnected": "Провайдер не подключен к вашему аккаунту"
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import styled, { css } from "styled-components";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
@ -12,14 +12,24 @@ import Toast from "@appserver/components/toast";
import HelpButton from "@appserver/components/help-button";
import PasswordInput from "@appserver/components/password-input";
import FieldContainer from "@appserver/components/field-container";
import SocialButton from "@appserver/components/social-button";
import FacebookButton from "@appserver/components/facebook-button";
import PageLayout from "@appserver/common/components/PageLayout";
import ForgotPasswordModalDialog from "./sub-components/forgot-password-modal-dialog";
import Register from "./sub-components/register-container";
import { getAuthProviders } from "@appserver/common/api/settings";
import { checkPwd } from "@appserver/common/desktop";
import { createPasswordHash, tryRedirectTo } from "@appserver/common/utils";
import { createPasswordHash } from "@appserver/common/utils";
import { providersData } from "@appserver/common/constants";
import { inject, observer } from "mobx-react";
import i18n from "./i18n";
import { I18nextProvider, useTranslation } from "react-i18next";
import toastr from "@appserver/components/toast/toastr";
const ButtonsWrapper = styled.div`
display: table;
margin: auto;
`;
const LoginContainer = styled.div`
display: flex;
@ -33,6 +43,18 @@ const LoginContainer = styled.div`
display: inline-block;
}
.buttonWrapper {
margin: 6px;
min-width: 225px;
}
.line {
height: 1px;
background-color: #eceef1;
flex-basis: 100%;
margin: 0 8px;
}
@media (max-width: 768px) {
padding: 0 16px;
max-width: 475px;
@ -140,7 +162,6 @@ const Form = (props) => {
const [isDialogVisible, setIsDialogVisible] = useState(false);
const [errorText, setErrorText] = useState("");
const [socialButtons, setSocialButtons] = useState([]);
const { t } = useTranslation("Login");
@ -153,6 +174,8 @@ const Form = (props) => {
organizationName,
greetingTitle,
history,
thirdPartyLogin,
providers,
} = props;
const { error, confirmedEmail } = match.params;
@ -175,7 +198,30 @@ const Form = (props) => {
//const throttledKeyPress = throttle(onKeyPress, 500);
useEffect(() => {
const authCallback = (profile) => {
thirdPartyLogin(profile.Serialized)
.then(() => {
setIsLoading(true);
history.push(defaultPage);
})
.catch(() => {
toastr.error(t("ProviderNotConnected"), t("ProviderLoginError"));
});
};
const setProviders = async () => {
const { setProviders } = props;
try {
await getAuthProviders().then((providers) => {
setProviders(providers);
});
} catch (e) {
console.error(e);
}
};
useEffect(async () => {
document.title = `${t("Authorization")} ${organizationName}`; //TODO: implement the setDocumentTitle() utility in ASC.Web.Common
error && setErrorText(error);
@ -183,6 +229,9 @@ const Form = (props) => {
focusInput();
window.authCallback = authCallback;
await setProviders();
//window.addEventListener("keyup", throttledKeyPress, false);
/*return () => {
@ -245,9 +294,10 @@ const Form = (props) => {
const hash = createPasswordHash(pass, hashSettings);
isDesktop && checkPwd();
login(userName, hash)
.then(() => history.push(defaultPage))
.then(() => {
history.push(defaultPage);
})
.catch((error) => {
setErrorText(error);
setIdentifierValid(!error);
@ -257,6 +307,89 @@ const Form = (props) => {
});
};
const onSocialButtonClick = useCallback((e) => {
const providerName = e.target.dataset.providername;
const url = e.target.dataset.url;
const { getOAuthToken, getLoginLink } = props;
try {
const tokenGetterWin = window.open(
url,
"login",
"width=800,height=500,status=no,toolbar=no,menubar=no,resizable=yes,scrollbars=no"
);
getOAuthToken(tokenGetterWin).then((code) => {
const token = window.btoa(
JSON.stringify({
auth: providerName,
mode: "popup",
callback: "authCallback",
})
);
tokenGetterWin.location.href = getLoginLink(token, code);
});
} catch (err) {
console.log(err);
}
}, []);
const addFacebookToStart = (facebookIndex, providerButtons) => {
const faceBookData = providers[facebookIndex];
const { icon, label, iconOptions } = providersData[faceBookData.provider];
providerButtons.unshift(
<div
className="buttonWrapper"
key={`${faceBookData.provider}ProviderItem`}
>
<FacebookButton
iconName={icon}
label={t(label)}
className="socialButton"
$iconOptions={iconOptions}
data-url={faceBookData.url}
data-providername={faceBookData.provider}
onClick={onSocialButtonClick}
/>
</div>
);
};
const providerButtons = () => {
let facebookIndex = null;
const providerButtons =
providers &&
providers.map((item, index) => {
const { icon, label, iconOptions, className } = providersData[
item.provider
];
if (!icon) return;
if (item.provider === "Facebook") {
facebookIndex = index;
return;
}
return (
<div className="buttonWrapper" key={`${item.provider}ProviderItem`}>
<SocialButton
iconName={icon}
label={t(label)}
className={`socialButton ${className ? className : ""}`}
$iconOptions={iconOptions}
data-url={item.url}
data-providername={item.provider}
onClick={onSocialButtonClick}
/>
</div>
);
});
if (facebookIndex) addFacebookToStart(facebookIndex, providerButtons);
return providerButtons;
};
//console.log("Login render");
return (
@ -374,15 +507,19 @@ const Form = (props) => {
</Text>
)}
{socialButtons.length ? (
<Box displayProp="flex" alignItems="center">
<div className="login-bottom-border"></div>
<Text className="login-bottom-text" color="#A3A9AE">
{t("Or")}
</Text>
<div className="login-bottom-border"></div>
</Box>
) : null}
{providers && providers.length > 0 && (
<>
<Box displayProp="flex" alignItems="center" marginProp="0 0 16px 0">
<div className="login-bottom-border"></div>
<Text className="login-bottom-text" color="#A3A9AE">
{t("Or")}
</Text>
<div className="login-bottom-border"></div>
</Box>
<ButtonsWrapper>{providerButtons()}</ButtonsWrapper>
</>
)}
</form>
<Toast />
</LoginContainer>
@ -394,7 +531,6 @@ Form.propTypes = {
match: PropTypes.object.isRequired,
hashSettings: PropTypes.object,
greetingTitle: PropTypes.string.isRequired,
socialButtons: PropTypes.array,
organizationName: PropTypes.string,
homepage: PropTypes.string,
defaultPage: PropTypes.string,
@ -429,7 +565,15 @@ LoginForm.propTypes = {
};
const Login = inject(({ auth }) => {
const { settingsStore, isAuthenticated, isLoaded, login } = auth;
const {
settingsStore,
isAuthenticated,
isLoaded,
login,
thirdPartyLogin,
setProviders,
providers,
} = auth;
const {
greetingSettings: greetingTitle,
organizationName,
@ -437,6 +581,8 @@ const Login = inject(({ auth }) => {
enabledJoin,
defaultPage,
isDesktopClient: isDesktop,
getOAuthToken,
getLoginLink,
} = settingsStore;
return {
@ -449,6 +595,11 @@ const Login = inject(({ auth }) => {
defaultPage,
isDesktop,
login,
thirdPartyLogin,
getOAuthToken,
getLoginLink,
setProviders,
providers,
};
})(withRouter(observer(LoginForm)));