Merge branch 'master' of github.com:ONLYOFFICE/CommunityServer-AspNetCore

This commit is contained in:
Ilya Oleshko 2019-09-27 08:57:49 +03:00
commit 4ff81ac474
31 changed files with 772 additions and 285 deletions

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Net;
using System.Security.Authentication;
using System.Security.Claims;
@ -8,11 +7,9 @@ using System.Threading.Tasks;
using ASC.Core;
using ASC.Security.Cryptography;
using ASC.Web.Studio.Core;
using ASC.Web.Studio.Utility;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -26,52 +23,18 @@ namespace ASC.Api.Core.Auth
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var Request = QueryHelpers.ParseQuery(Context.Request.Headers["confirm"]);
_ = Request.TryGetValue("type", out var type);
var _type = typeof(ConfirmType).TryParseEnum(type, ConfirmType.EmpInvite);
var emailValidationKeyModel = EmailValidationKeyModel.FromRequest(Context.Request);
if (SecurityContext.IsAuthenticated && _type != ConfirmType.EmailChange)
if (SecurityContext.IsAuthenticated && emailValidationKeyModel.Type != ConfirmType.EmailChange)
{
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, new AuthenticationProperties(), Scheme.Name)));
}
_ = Request.TryGetValue("key", out var key);
_ = Request.TryGetValue("emplType", out var emplType);
_ = Request.TryGetValue("email", out var _email);
var validInterval = SetupInfo.ValidEmailKeyInterval;
EmailValidationKeyProvider.ValidationResult checkKeyResult;
switch (_type)
{
case ConfirmType.EmpInvite:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type + emplType, key, validInterval);
break;
case ConfirmType.LinkInvite:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_type + emplType, key, validInterval);
break;
case ConfirmType.EmailChange:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type + SecurityContext.CurrentAccount.ID, key, validInterval);
break;
case ConfirmType.PasswordChange:
var userHash = Request.TryGetValue("p", out var p) && p == "1";
var hash = string.Empty;
if (userHash)
{
var tenantId = CoreContext.TenantManager.GetCurrentTenant().TenantId;
hash = CoreContext.Authentication.GetUserPasswordHash(tenantId, CoreContext.UserManager.GetUserByEmail(tenantId, _email).ID);
}
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type + (string.IsNullOrEmpty(hash) ? string.Empty : Hasher.Base64Hash(hash)), key, validInterval);
break;
default:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type, key, validInterval);
break;
}
var checkKeyResult = emailValidationKeyModel.Validate();
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Role, _type.ToString())
new Claim(ClaimTypes.Role, emailValidationKeyModel.Type.ToString())
};
if (!SecurityContext.IsAuthenticated)

View File

@ -125,7 +125,14 @@ namespace ASC.Common.Caching
var cr = c.Consume(Cts[channelName].Token);
if (cr != null && cr.Value != null && !(new Guid(cr.Key.Id.ToByteArray())).Equals(Key) && Actions.TryGetValue(channelName, out var act))
{
act(cr.Value);
try
{
act(cr.Value);
}
catch (Exception e)
{
Log.Error("Kafka onmessage", e);
}
}
}
catch (ConsumeException e)

View File

@ -29,7 +29,12 @@ using System.Text;
using ASC.Common.Logging;
using ASC.Common.Utils;
using ASC.Core;
using ASC.Core.Users;
using ASC.Web.Studio.Utility;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using static ASC.Security.Cryptography.EmailValidationKeyProvider;
namespace ASC.Security.Cryptography
{
public class EmailValidationKeyProvider
@ -42,8 +47,18 @@ namespace ASC.Security.Cryptography
}
private static readonly ILog log = LogManager.GetLogger("ASC.KeyValidation.EmailSignature");
private static readonly DateTime _from = new DateTime(2010, 01, 01, 0, 0, 0, DateTimeKind.Utc);
private static readonly DateTime _from = new DateTime(2010, 01, 01, 0, 0, 0, DateTimeKind.Utc);
internal static readonly TimeSpan ValidInterval;
static EmailValidationKeyProvider()
{
if (!TimeSpan.TryParse(ConfigurationManager.AppSettings["email:validinterval"], out var validInterval))
{
validInterval = TimeSpan.FromDays(7);
}
ValidInterval = validInterval;
}
public static string GetEmailKey(string email)
{
return GetEmailKey(CoreContext.TenantManager.GetCurrentTenant().TenantId, email);
@ -72,7 +87,7 @@ namespace ASC.Security.Cryptography
log.Fatal("Failed to format tenant specific email", e);
return email.ToLowerInvariant();
}
}
}
public static ValidationResult ValidateEmailKey(string email, string key)
{
@ -120,5 +135,82 @@ namespace ASC.Security.Cryptography
Array.Copy(salt, 0, alldata, data.Length, salt.Length);
return Hasher.Hash(alldata, HashAlg.SHA256);
}
}
public class EmailValidationKeyModel
{
public string Key { get; set; }
public EmployeeType? EmplType { get; set; }
public string Email { get; set; }
public Guid? UiD { get; set; }
public ConfirmType Type { get; set; }
public int? P { get; set; }
public ValidationResult Validate()
{
ValidationResult checkKeyResult;
switch (Type)
{
case ConfirmType.EmpInvite:
checkKeyResult = ValidateEmailKey(Email + Type + EmplType, Key, ValidInterval);
break;
case ConfirmType.LinkInvite:
checkKeyResult = ValidateEmailKey(Type.ToString() + EmplType.ToString(), Key, ValidInterval);
break;
case ConfirmType.EmailChange:
checkKeyResult = ValidateEmailKey(Email + Type + SecurityContext.CurrentAccount.ID, Key, ValidInterval);
break;
case ConfirmType.PasswordChange:
var hash = string.Empty;
if (P == 1)
{
var tenantId = CoreContext.TenantManager.GetCurrentTenant().TenantId;
hash = CoreContext.Authentication.GetUserPasswordHash(tenantId, UiD.Value);
}
checkKeyResult = ValidateEmailKey(Email + Type + (string.IsNullOrEmpty(hash) ? string.Empty : Hasher.Base64Hash(hash)) + UiD, Key, ValidInterval);
break;
default:
checkKeyResult = ValidateEmailKey(Email + Type, Key, ValidInterval);
break;
}
return checkKeyResult;
}
public static EmailValidationKeyModel FromRequest(HttpRequest httpRequest)
{
var Request = QueryHelpers.ParseQuery(httpRequest.Headers["confirm"]);
_ = Request.TryGetValue("type", out var type);
_ = Enum.TryParse<ConfirmType>(type, out var confirmType);
_ = Request.TryGetValue("key", out var key);
_ = Request.TryGetValue("p", out var pkey);
_ = int.TryParse(pkey, out var p);
_ = Request.TryGetValue("emplType", out var emplType);
_ = Enum.TryParse<EmployeeType>(emplType, out var employeeType);
_ = Request.TryGetValue("email", out var _email);
_ = Request.TryGetValue("uid", out var userIdKey);
_ = Guid.TryParse(userIdKey, out var userId);
return new EmailValidationKeyModel
{
Key = key,
Type = confirmType,
Email = _email,
EmplType = employeeType,
UiD = userId,
P = p
};
}
public void Deconstruct(out string key, out string email, out EmployeeType? employeeType, out Guid? userId, out ConfirmType confirmType, out int? p)
=> (key, email, employeeType, userId, confirmType, p) = (Key, Email, EmplType, UiD, Type, P);
}
}

View File

@ -0,0 +1,28 @@
namespace ASC.Web.Studio.Utility
{
// emp-invite - confirm ivite by email
// portal-suspend - confirm portal suspending - Tenant.SetStatus(TenantStatus.Suspended)
// portal-continue - confirm portal continuation - Tenant.SetStatus(TenantStatus.Active)
// portal-remove - confirm portal deletation - Tenant.SetStatus(TenantStatus.RemovePending)
// DnsChange - change Portal Address and/or Custom domain name
public enum ConfirmType
{
EmpInvite,
LinkInvite,
PortalSuspend,
PortalContinue,
PortalRemove,
DnsChange,
PortalOwnerChange,
Activation,
EmailChange,
EmailActivation,
PasswordChange,
ProfileRemove,
PhoneActivation,
PhoneAuth,
Auth,
TfaActivation,
TfaAuth
}
}

View File

@ -1,6 +1,6 @@
import React, { Suspense } from "react";
import { connect } from "react-redux";
import { BrowserRouter, Switch } from "react-router-dom";
import { Router, Switch } from "react-router-dom";
import { Loader } from "asc-web-components";
import PeopleLayout from "./components/Layout/index";
import Home from "./components/pages/Home";
@ -10,6 +10,7 @@ import ProfileAction from './components/pages/ProfileAction';
import GroupAction from './components/pages/GroupAction';
import { Error404 } from "./components/pages/Error";
import Reassign from './components/pages/Reassign';
import history from './history';
/*const Profile = lazy(() => import("./components/pages/Profile"));
const ProfileAction = lazy(() => import("./components/pages/ProfileAction"));
@ -18,13 +19,13 @@ const GroupAction = lazy(() => import("./components/pages/GroupAction"));*/
const App = ({ settings }) => {
const { homepage } = settings;
return (
<BrowserRouter>
<Router history={history}>
<PeopleLayout>
<Suspense
fallback={<Loader className="pageLoader" type="rombs" size={40} />}
>
<Switch>
<PrivateRoute exact path={homepage} component={Home} />
<PrivateRoute exact path={[homepage, `${homepage}/filter`]} component={Home} />
<PrivateRoute
path={`${homepage}/view/:userId`}
component={Profile}
@ -59,7 +60,7 @@ const App = ({ settings }) => {
</Switch>
</Suspense>
</PeopleLayout>
</BrowserRouter>
</Router>
);
};

View File

@ -1,19 +1,30 @@
import React, { useCallback } from "react";
import React from "react";
import { connect } from "react-redux";
import { FilterInput } from "asc-web-components";
import { fetchPeople } from "../../../../../store/people/actions";
import find from "lodash/find";
import result from "lodash/result";
import { isAdmin } from "../../../../../store/auth/selectors";
import { useTranslation } from "react-i18next";
import { typeGuest, typeUser, department } from './../../../../../helpers/customNames';
const getSortData = ( t ) => {
return [
{ key: "firstname", label: t('ByFirstNameSorting') },
{ key: "lastname", label: t('ByLastNameSorting') }
];
};
import { withTranslation } from "react-i18next";
import {
typeGuest,
typeUser,
department
} from "./../../../../../helpers/customNames";
import { withRouter } from "react-router";
import Filter from "../../../../../store/people/filter";
import {
EMPLOYEE_STATUS,
ACTIVATION_STATUS,
ROLE,
GROUP,
SEARCH,
SORT_BY,
SORT_ORDER,
PAGE,
PAGE_COUNT
} from "../../../../../helpers/constants";
import { getFilterByLocation } from "../../../../../helpers/converters";
const getEmployeeStatus = filterValues => {
const employeeStatus = result(
@ -59,51 +70,47 @@ const getGroup = filterValues => {
return groupId || null;
};
const SectionFilterContent = ({
fetchPeople,
filter,
onLoading,
user,
groups
}) => {
const { t } = useTranslation();
const selectedFilterData = {
filterValues: [],
sortDirection: filter.sortOrder === "ascending" ? "asc" : "desc",
sortId: filter.sortBy
class SectionFilterContent extends React.Component {
componentDidMount() {
const { location, filter, onLoading, fetchPeople } = this.props;
const newFilter = getFilterByLocation(location);
if(!newFilter || newFilter.equals(filter)) return;
onLoading(true);
fetchPeople(newFilter).finally(() => onLoading(false));
}
onFilter = data => {
const { onLoading, fetchPeople, filter } = this.props;
const employeeStatus = getEmployeeStatus(data.filterValues);
const activationStatus = getActivationStatus(data.filterValues);
const role = getRole(data.filterValues);
const group = getGroup(data.filterValues);
const search = data.inputValue || null;
const sortBy = data.sortId;
const sortOrder =
data.sortDirection === "desc" ? "descending" : "ascending";
const newFilter = filter.clone();
newFilter.page = 0;
newFilter.sortBy = sortBy;
newFilter.sortOrder = sortOrder;
newFilter.employeeStatus = employeeStatus;
newFilter.activationStatus = activationStatus;
newFilter.role = role;
newFilter.search = search;
newFilter.group = group;
onLoading(true);
fetchPeople(newFilter).finally(() => onLoading(false));
};
selectedFilterData.inputValue = filter.search;
getData = () => {
const { user, groups, t } = this.props;
if (filter.employeeStatus) {
selectedFilterData.filterValues.push({
key: `${filter.employeeStatus}`,
group: "filter-status"
});
}
if (filter.activationStatus) {
selectedFilterData.filterValues.push({
key: `${filter.activationStatus}`,
group: "filter-email"
});
}
if (filter.role) {
selectedFilterData.filterValues.push({
key: filter.role,
group: "filter-type"
});
}
if (filter.group) {
selectedFilterData.filterValues.push({
key: filter.group,
group: "filter-group"
});
}
const getData = useCallback(() => {
const options = !isAdmin(user)
? []
: [
@ -126,7 +133,12 @@ const SectionFilterContent = ({
];
const groupOptions = groups.map(group => {
return { key: group.id, inSubgroup: true, group: "filter-group", label: group.name };
return {
key: group.id,
inSubgroup: true,
group: "filter-group",
label: group.name
};
});
const filterOptions = [
@ -137,10 +149,10 @@ const SectionFilterContent = ({
label: t("Email"),
isHeader: true
},
{
key: "1",
group: "filter-email",
label: t("LblActive")
{
key: "1",
group: "filter-email",
label: t("LblActive")
},
{
key: "2",
@ -153,66 +165,114 @@ const SectionFilterContent = ({
label: t("UserType"),
isHeader: true
},
{ key: "admin", group: "filter-type", label: t("Administrator")},
{ key: "user", group: "filter-type", label: t('CustomTypeUser', { typeUser })},
{ key: "guest", group: "filter-type", label: t('CustomTypeGuest', { typeGuest }) },
{ key: "admin", group: "filter-type", label: t("Administrator") },
{
key: "user",
group: "filter-type",
label: t("CustomTypeUser", { typeUser })
},
{
key: "guest",
group: "filter-type",
label: t("CustomTypeGuest", { typeGuest })
},
{
key: "filter-other",
group: "filter-other",
label: t("LblOther"),
isHeader: true
},
{ key: "filter-type-group", group: "filter-other", subgroup: 'filter-group', label: t('CustomDepartment', { department }), defaultSelectLabel: t("DefaultSelectLabel") },
{
key: "filter-type-group",
group: "filter-other",
subgroup: "filter-group",
label: t("CustomDepartment", { department }),
defaultSelectLabel: t("DefaultSelectLabel")
},
...groupOptions
];
//console.log("getData (filterOptions)", filterOptions);
return filterOptions;
};
}, [user, groups, t]);
getSortData = () => {
const { t } = this.props;
const onFilter = useCallback(
data => {
console.log(data);
return [
{ key: "firstname", label: t("ByFirstNameSorting") },
{ key: "lastname", label: t("ByLastNameSorting") }
];
};
const newFilter = filter.clone();
newFilter.page = 0;
newFilter.sortBy = data.sortId;
newFilter.sortOrder =
data.sortDirection === "desc" ? "descending" : "ascending";
newFilter.employeeStatus = getEmployeeStatus(data.filterValues);
newFilter.activationStatus = getActivationStatus(data.filterValues);
newFilter.role = getRole(data.filterValues);
newFilter.search = data.inputValue || null;
newFilter.group = getGroup(data.filterValues);
getSelectedFilterData = () => {
const { filter } = this.props;
const selectedFilterData = {
filterValues: [],
sortDirection: filter.sortOrder === "ascending" ? "asc" : "desc",
sortId: filter.sortBy
};
onLoading(true);
fetchPeople(newFilter).finally(() => onLoading(false));
},
[onLoading, fetchPeople, filter]
);
return (
<FilterInput
getFilterData={getData}
getSortData={getSortData.bind(this, t)}
selectedFilterData={selectedFilterData}
onFilter={onFilter}
directionAscLabel={t("DirectionAscLabel")}
directionDescLabel={t("DirectionDescLabel")}
/>
);
};
selectedFilterData.inputValue = filter.search;
if (filter.employeeStatus) {
selectedFilterData.filterValues.push({
key: `${filter.employeeStatus}`,
group: "filter-status"
});
}
if (filter.activationStatus) {
selectedFilterData.filterValues.push({
key: `${filter.activationStatus}`,
group: "filter-email"
});
}
if (filter.role) {
selectedFilterData.filterValues.push({
key: filter.role,
group: "filter-type"
});
}
if (filter.group) {
selectedFilterData.filterValues.push({
key: filter.group,
group: "filter-group"
});
}
return selectedFilterData;
};
render() {
const selectedFilterData = this.getSelectedFilterData();
const { t } = this.props;
return (
<FilterInput
getFilterData={this.getData}
getSortData={this.getSortData}
selectedFilterData={selectedFilterData}
onFilter={this.onFilter}
directionAscLabel={t("DirectionAscLabel")}
directionDescLabel={t("DirectionDescLabel")}
/>
);
}
}
function mapStateToProps(state) {
return {
user: state.auth.user,
groups: state.people.groups,
filter: state.people.filter
filter: state.people.filter,
settings: state.auth.settings
};
}
export default connect(
mapStateToProps,
{ fetchPeople }
)(SectionFilterContent);
)(withRouter(withTranslation()(SectionFilterContent)));

View File

@ -1,18 +1,31 @@
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { PageLayout, Loader } from "asc-web-components";
import { PageLayout, Loader, toastr } from "asc-web-components";
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
import { SectionHeaderContent, SectionBodyContent } from './Section';
import { fetchProfile } from '../../../store/profile/actions';
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
import { I18nextProvider, withTranslation } from "react-i18next";
class Profile extends React.Component {
class PureProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
queryString: `${props.location.search.slice(1)}`
};
}
componentDidMount() {
const { match, fetchProfile } = this.props;
const { match, fetchProfile, t } = this.props;
const { userId } = match.params;
const queryParams = this.state.queryString.split('&');
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
const linkParams = Object.fromEntries(arrayOfQueryParams);
if (linkParams.email_change && linkParams.email_change === "success"){
toastr.success(t('ChangeEmailSuccess'));
}
fetchProfile(userId);
}
@ -32,8 +45,7 @@ class Profile extends React.Component {
const { profile } = this.props;
return (
<I18nextProvider i18n={i18n}>
{profile
profile
?
<PageLayout
articleHeaderContent={<ArticleHeaderContent />}
@ -53,12 +65,15 @@ class Profile extends React.Component {
sectionBodyContent={
<Loader className="pageLoader" type="rombs" size={40} />
}
/>}
</I18nextProvider>
/>
);
};
};
const ProfileContainer = withTranslation()(PureProfile);
const Profile = (props) => <I18nextProvider i18n={i18n}><ProfileContainer {...props} /></I18nextProvider>;
Profile.propTypes = {
history: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,

View File

@ -21,6 +21,7 @@
"DeleteSelfProfile": "Delete profile",
"EditButton": "Edit",
"Actions": "Actions",
"ChangeEmailSuccess": "Mail has been successfully changed",
"PhoneChange": "Change phone",
"PhoneLbl": "Phone",

View File

@ -1,5 +1,14 @@
export const AUTH_KEY = "asc_auth_key";
export const GUID_EMPTY = "00000000-0000-0000-0000-000000000000";
export const EMPLOYEE_STATUS = "employeestatus";
export const ACTIVATION_STATUS = "activationstatus";
export const ROLE = "role";
export const GROUP = "group";
export const SEARCH = "search";
export const SORT_BY = "sortby";
export const SORT_ORDER = "sortorder";
export const PAGE = "page";
export const PAGE_COUNT = "pagecount";
/**
* Enum for employee activation status.

View File

@ -0,0 +1,67 @@
import {
EMPLOYEE_STATUS,
ACTIVATION_STATUS,
ROLE,
GROUP,
SEARCH,
SORT_BY,
SORT_ORDER,
PAGE,
PAGE_COUNT
} from "./constants";
import Filter from "../store/people/filter";
export function getObjectByLocation(location) {
if (!location.search || !location.search.length) return null;
const searchUrl = location.search.substring(1);
const object = JSON.parse(
'{"' +
decodeURI(searchUrl)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"') +
'"}'
);
return object;
}
export function getFilterByLocation(location) {
const urlFilter = getObjectByLocation(location);
if(!urlFilter) return null;
const defaultFilter = Filter.getDefault();
const employeeStatus =
(urlFilter[EMPLOYEE_STATUS] && +urlFilter[EMPLOYEE_STATUS]) ||
defaultFilter.employeeStatus;
const activationStatus =
(urlFilter[ACTIVATION_STATUS] && +urlFilter[ACTIVATION_STATUS]) ||
defaultFilter.activationStatus;
const role = urlFilter[ROLE] || defaultFilter.role;
const group = urlFilter[GROUP] || defaultFilter.group;
const search = urlFilter[SEARCH] || defaultFilter.search;
const sortBy = urlFilter[SORT_BY] || defaultFilter.sortBy;
const sortOrder = urlFilter[SORT_ORDER] || defaultFilter.sortOrder;
const page = (urlFilter[PAGE] && +urlFilter[PAGE]) || defaultFilter.page;
const pageCount =
(urlFilter[PAGE_COUNT] && +urlFilter[PAGE_COUNT]) ||
defaultFilter.pageCount;
const newFilter = new Filter(
page,
pageCount,
defaultFilter.total,
sortBy,
sortOrder,
employeeStatus,
activationStatus,
role,
search,
group
);
return newFilter;
}

View File

@ -0,0 +1,3 @@
import { createBrowserHistory } from 'history';
export default createBrowserHistory();

View File

@ -50,6 +50,7 @@
"RemoveData",
"DeleteSelfProfile",
"EditButton",
"ChangeEmailSuccess",
"Actions"
]
},

View File

@ -1,6 +1,7 @@
import * as api from "../services/api";
import { setGroups, fetchPeopleAsync } from "../people/actions";
import setAuthorizationToken from "../../store/services/setAuthorizationToken";
import { getFilterByLocation } from "../../helpers/converters";
export const LOGIN_POST = "LOGIN_POST";
export const SET_CURRENT_USER = "SET_CURRENT_USER";
@ -59,7 +60,9 @@ export async function getUserInfo(dispatch) {
dispatch(setGroups(groupResp.data.response));
await fetchPeopleAsync(dispatch);
const newFilter = getFilterByLocation(window.location);
await fetchPeopleAsync(dispatch, newFilter);
return dispatch(setIsLoaded(true));
}

View File

@ -1,5 +1,18 @@
import * as api from "../services/api";
import Filter from "./filter";
import history from "../../history";
import config from "../../../package.json";
import {
EMPLOYEE_STATUS,
ACTIVATION_STATUS,
ROLE,
GROUP,
SEARCH,
SORT_BY,
SORT_ORDER,
PAGE,
PAGE_COUNT
} from "../../helpers/constants";
export const SET_GROUPS = "SET_GROUPS";
export const SET_USERS = "SET_USERS";
@ -74,6 +87,49 @@ export function deselectUser(user) {
}
export function setFilter(filter) {
const defaultFilter = Filter.getDefault();
const params = [];
if (filter.employeeStatus) {
params.push(`${EMPLOYEE_STATUS}=${filter.employeeStatus}`);
}
if (filter.activationStatus) {
params.push(`${ACTIVATION_STATUS}=${filter.activationStatus}`);
}
if (filter.role) {
params.push(`${ROLE}=${filter.role}`);
}
if (filter.group) {
params.push(`${GROUP}=${filter.group}`);
}
if (filter.search) {
params.push(`${SEARCH}=${filter.search}`);
}
if (filter.page > 0) {
params.push(`${PAGE}=${filter.page + 1}`);
}
if (filter.pageCount !== defaultFilter.pageCount) {
params.push(`${PAGE_COUNT}=${filter.pageCount}`);
}
if (
params.length > 0 ||
(filter.sortBy !== defaultFilter.sortBy ||
filter.sortOrder !== defaultFilter.sortOrder)
) {
params.push(`${SORT_BY}=${filter.sortBy}`);
params.push(`${SORT_ORDER}=${filter.sortOrder}`);
}
if (params.length > 0) {
history.push(`${config.homepage}/filter?${params.join("&")}`);
}
return {
type: SET_FILTER,
filter
@ -89,8 +145,9 @@ export function setSelectorUsers(users) {
export function fetchSelectorUsers() {
return dispatch => {
api.getSelectorUserList()
.then(res => dispatch(setSelectorUsers(res.data.response)));
api
.getSelectorUserList()
.then(res => dispatch(setSelectorUsers(res.data.response)));
};
}
@ -102,6 +159,7 @@ export function fetchPeople(filter) {
export function fetchPeopleByFilter(dispatch, filter) {
let filterData = (filter && filter.clone()) || Filter.getDefault();
return api.getUserList(filterData).then(res => {
filterData.total = res.data.total;
dispatch(setFilter(filterData));

View File

@ -61,7 +61,7 @@ class Filter {
activationStatus,
role,
search,
group,
group
} = this;
let dtoFilter = {
@ -122,6 +122,21 @@ class Filter {
this.group
);
}
equals(filter) {
const equals =
this.employeeStatus === filter.employeeStatus &&
this.activationStatus === filter.activationStatus &&
this.role === filter.role &&
this.group === filter.group &&
this.search === filter.search &&
this.sortBy === filter.sortBy &&
this.sortOrder === filter.sortOrder &&
this.page === filter.page &&
this.pageCount === filter.pageCount;
return equals;
}
}
export default Filter;

View File

@ -7,6 +7,7 @@ using ASC.Web.Api.Models;
using ASC.Web.Api.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using static ASC.Security.Cryptography.EmailValidationKeyProvider;
namespace ASC.Web.Api.Controllers
{
@ -38,6 +39,13 @@ namespace ASC.Web.Api.Controllers
}
}
[AllowAnonymous]
[Create("confirm", false)]
public ValidationResult CheckConfirm([FromBody]EmailValidationKeyModel model)
{
return model.Validate();
}
private static UserInfo GetUser(int tenantId, string userName, string password)
{
var user = CoreContext.UserManager.GetUsers(

View File

@ -1,48 +1,67 @@
import React, { Suspense, lazy } from "react";
import { Switch, Redirect, Route } from "react-router-dom";
import { Redirect, Route } from "react-router-dom";
import { Loader } from "asc-web-components";
import PublicRoute from "../../../helpers/publicRoute";
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
// import ChangeEmailForm from "./sub-components/changeEmail";
const CreateUserForm = lazy(() => import("./sub-components/createUser"));
const ChangePasswordForm = lazy(() => import("./sub-components/changePassword"));
const ActivateEmailForm = lazy(() => import("./sub-components/activateEmail"));
const ChangeEmailForm = lazy(() => import("./sub-components/changeEmail"));
const ChangePhoneForm = lazy(() => import("./sub-components/changePhone"));
const Confirm = ({ match }) => {
//console.log("Confirm render");
const ConfirmType = (props) => {
switch (props.type) {
case 'LinkInvite':
return <PublicRoute
component={CreateUserForm}
/>;
case 'EmailActivation':
return <Route
component={ActivateEmailForm}
/>;
case 'EmailChange':
return <Route
component={ChangeEmailForm}
/>;
case 'PasswordChange':
return <Route
component={ChangePasswordForm}
/>;
case 'PhoneActivation':
return <Route
component={ChangePhoneForm}
/>;
default:
return <Redirect to={{ pathname: "/" }} />;
}
}
class Confirm extends React.Component {
return (
constructor(props) {
const queryParams = props.location.search.slice(1).split('&');
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
const linkParams = Object.fromEntries(arrayOfQueryParams);
super(props);
this.state = {
type: linkParams.type
};
}
render() {
//console.log("Confirm render");
return (
<I18nextProvider i18n={i18n}>
<Suspense
fallback={<Loader className="pageLoader" type="rombs" size={40} />}
>
<Switch>
<PublicRoute
path={`${match.path}/type=LinkInvite`}
component={CreateUserForm}
/>
<Route
exact
path={`${match.path}/type=EmailActivation`}
component={ActivateEmailForm}
/>
<Route
exact
path={`${match.path}/type=PasswordChange`}
component={ChangePasswordForm}
/>
<Route
exact
path={`${match.path}/type=PhoneActivation`}
component={ChangePhoneForm}
/>
<Redirect to={{ pathname: "/" }} />
</Switch>
<ConfirmType type={this.state.type} />
</Suspense>
</I18nextProvider>
);
};
</I18nextProvider >
);
};
}
export default Confirm;

View File

@ -16,6 +16,7 @@
"PassworResetTitle": "Now you can create a new password.", "_comment":"SYNTAX ERROR 'Passwor' Reset Title",
"PasswordCustomMode": "Password",
"ImportContactsOkButton": "OK",
"LoadingProcessing": "Loading...",
"CustomWelcomePageTitle": "{{welcomePageTitle}}"

View File

@ -0,0 +1,61 @@
import React from 'react';
import { withRouter } from "react-router";
import { withTranslation } from 'react-i18next';
import { PageLayout, Loader } from 'asc-web-components';
import { connect } from 'react-redux';
import { logout, changeEmail } from '../../../../store/auth/actions';
import PropTypes from 'prop-types';
class ChangeEmail extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
queryString: props.location.search.slice(1)
};
}
componentDidUpdate(){
const { logout, changeEmail, userId, isLoaded } = this.props;
if (isLoaded){
const queryParams = this.state.queryString.split('&');
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
const linkParams = Object.fromEntries(arrayOfQueryParams);
// logout();
const email = decodeURIComponent(linkParams.email);
changeEmail(userId, {email}, this.state.queryString)
.then((res) => {
console.log('change client email success', res)
window.location.href = `${window.location.origin}/products/people/view/@self?email_change=success`;
})
.catch((e) => {
console.log('change client email error', e)
});
}
}
render() {
console.log('Change email render');
return (
<Loader className="pageLoader" type="rombs" size={40} />
);
}
}
ChangeEmail.propTypes = {
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
const ChangeEmailForm = (props) => (<PageLayout sectionBodyContent={<ChangeEmail {...props} />} />);
function mapStateToProps(state) {
return {
isLoaded: state.auth.isLoaded,
userId: state.auth.user.id
};
}
export default connect(mapStateToProps, { logout, changeEmail })(withRouter(withTranslation()(ChangeEmailForm)));

View File

@ -64,10 +64,11 @@ const Form = props => {
setIsLoading(true);
console.log("changePassword onSubmit", match, location, history);
const str = location.search.split("&");
const userId = str[1].slice(4);
const key = `type=PasswordChange&${location.search.slice(1)}`;
const userId = "f305ea37-da05-11e9-89de-e0cb4e43b8c0"; //TODO: Find real userId by key
changePassword(userId, password, key)
changePassword(userId, {password}, key)
.then(() => {
console.log("UPDATE PASSWORD");
history.push("/");
@ -149,6 +150,7 @@ const Form = props => {
tooltipPasswordTitle="Password must contain:"
tooltipPasswordLength={tooltipPasswordLength}
placeholder={t("PasswordCustomMode")}
maxLength={30}
//isAutoFocussed={true}
//autocomple="current-password"

View File

@ -10,6 +10,7 @@ export const LOGOUT = 'LOGOUT';
export const SET_PASSWORD_SETTINGS = 'SET_PASSWORD_SETTINGS';
export const SET_IS_CONFIRM_LOADED = 'SET_IS_CONFIRM_LOADED';
export const SET_NEW_PASSWORD = 'SET_NEW_PASSWORD';
export const SET_NEW_EMAIL = 'SET_NEW_EMAIL';
export function setCurrentUser(user) {
return {
@ -66,6 +67,13 @@ export function setNewPasswordSettings(password) {
};
};
export function setNewEmail(email) {
return {
type: SET_NEW_EMAIL,
email
};
};
export function getUserInfo(dispatch) {
return api.getUser()
@ -140,9 +148,18 @@ export function checkResponseError(res) {
export function changePassword(userId, password, key) {
return dispatch => {
return api.changePassword(userId, password, key)
.then(res => {
//checkResponseError(res);
dispatch(setNewPasswordSettings(res.data.response));
})
.then(res => {
//checkResponseError(res);
dispatch(setNewPasswordSettings(res.data.response));
})
}
}
export function changeEmail(userId, email, key) {
return dispatch => {
return api.changePassword(userId, email, key)
.then(res => {
dispatch(setNewEmail(res.data.response.email));
})
}
}

View File

@ -1,4 +1,4 @@
import { SET_CURRENT_USER, SET_MODULES, SET_SETTINGS, SET_IS_LOADED, LOGOUT, SET_PASSWORD_SETTINGS, SET_IS_CONFIRM_LOADED, SET_NEW_PASSWORD } from './actions';
import { SET_CURRENT_USER, SET_MODULES, SET_SETTINGS, SET_IS_LOADED, LOGOUT, SET_PASSWORD_SETTINGS, SET_IS_CONFIRM_LOADED, SET_NEW_PASSWORD, SET_NEW_EMAIL } from './actions';
import isEmpty from 'lodash/isEmpty';
import config from "../../../package.json";
@ -60,6 +60,10 @@ const authReducer = (state = initialState, action) => {
return Object.assign({}, state, {
password: action.password
});
case SET_NEW_EMAIL:
return Object.assign({}, state, {
email: action.email
});
case LOGOUT:
return initialState;
default:

View File

@ -15,17 +15,17 @@ export function getModulesList() {
return IS_FAKE
? fakeApi.getModulesList()
: axios
.get(`${API_URL}/modules`)
.then(res => {
const modules = res.data.response;
return axios.all(
modules.map(m => axios.get(`${window.location.origin}/${m}`))
);
})
.then(res => {
const response = res.map(d => d.data.response);
return Promise.resolve({ data: { response } });
});
.get(`${API_URL}/modules`)
.then(res => {
const modules = res.data.response;
return axios.all(
modules.map(m => axios.get(`${window.location.origin}/${m}`))
);
})
.then(res => {
const response = res.map(d => d.data.response);
return Promise.resolve({ data: { response } });
});
}
export function getUser() {
@ -44,8 +44,8 @@ export function getPasswordSettings(key) {
return IS_FAKE
? fakeApi.getPasswordSettings()
: axios.get(`${API_URL}/settings/security/password`, {
headers: { confirm: key }
});
headers: { confirm: key }
});
}
export function createUser(data, key) {
@ -63,10 +63,9 @@ export function validateConfirmLink(link) {
}
export function changePassword(userId, password, key) {
const IS_FAKE = true;
return IS_FAKE
? fakeApi.changePassword()
: axios.put(`${API_URL}/${userId}/password`, {password}, {
headers: { confirm: key }
: axios.put(`${API_URL}/people/${userId}/password`, password, {
headers: { confirm: key }
});
}
}

View File

@ -150,12 +150,7 @@ export function validateConfirmLink(link) {
}
export function changePassword() {
const data = {
//minLength: 12,
//upperCase: true,
//digits: true,
//specSymbols: true
};
const data = { password: "password" };
return fakeResponse(data);
}

View File

@ -1,6 +1,6 @@
{
"name": "asc-web-components",
"version": "1.0.97",
"version": "1.0.98",
"description": "Ascensio System SIA component library",
"license": "AGPL-3.0",
"main": "dist/asc-web-components.js",

View File

@ -1,8 +1,25 @@
import React, { memo } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import ModalDialog from '../modal-dialog'
import Button from '../button'
import AvatarEditorBody from './sub-components/avatar-editor-body'
import Aside from "../layout/sub-components/aside";
import IconButton from '../icon-button'
import styled from 'styled-components'
import { desktop } from '../../utils/device';
import throttle from 'lodash/throttle';
const Header = styled.div`
margin-bottom: 10px;
`;
const StyledAside = styled(Aside)`
padding: 10px;
.aside-save-button{
margin-top: 10px;
}
`;
class AvatarEditor extends React.Component {
constructor(props) {
@ -14,7 +31,9 @@ class AvatarEditor extends React.Component {
x: 0,
y: 0,
width: 0,
height:0
height: 0,
displayType: this.props.displayType !== 'auto' ? this.props.displayType : window.innerWidth <= desktop.match(/\d+/)[0] ? 'aside' : 'modal'
}
this.onClose = this.onClose.bind(this);
@ -24,19 +43,29 @@ class AvatarEditor extends React.Component {
this.onLoadFile = this.onLoadFile.bind(this);
this.onPositionChange = this.onPositionChange.bind(this);
this.onDeleteImage = this.onDeleteImage.bind(this)
this.onDeleteImage = this.onDeleteImage.bind(this);
this.throttledResize = throttle(this.resize, 300);
}
onImageChange(file){
if(typeof this.props.onImageChange === 'function') this.props.onImageChange(file);
resize = () => {
if (this.props.displayType === "auto") {
const type = window.innerWidth <= desktop.match(/\d+/)[0] ? 'aside' : 'modal';
if (type !== this.state.displayType)
this.setState({
displayType: type
});
}
}
onDeleteImage(){
onImageChange(file) {
if (typeof this.props.onImageChange === 'function') this.props.onImageChange(file);
}
onDeleteImage() {
this.setState({
isContainsFile: false
})
if(typeof this.props.onDeleteImage === 'function') this.props.onDeleteImage();
if (typeof this.props.onDeleteImage === 'function') this.props.onDeleteImage();
}
onPositionChange(data){
onPositionChange(data) {
this.setState(data);
}
onLoadFileError(error) {
@ -53,10 +82,10 @@ class AvatarEditor extends React.Component {
}
}
onLoadFile(file) {
if(typeof this.props.onLoadFile === 'function') this.props.onLoadFile(file);
if (typeof this.props.onLoadFile === 'function') this.props.onLoadFile(file);
this.setState({ isContainsFile: true });
}
onSaveButtonClick() {
this.props.onSave(this.state.isContainsFile, {
x: this.state.x,
@ -76,12 +105,59 @@ class AvatarEditor extends React.Component {
this.setState({ visible: this.props.visible });
}
}
componentDidMount() {
window.addEventListener('resize', this.throttledResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.throttledResize);
}
render() {
return (
<ModalDialog
visible={this.state.visible}
headerContent={this.props.headerLabel}
bodyContent={
this.state.displayType === "modal" ?
<ModalDialog
visible={this.state.visible}
headerContent={this.props.headerLabel}
bodyContent={
<AvatarEditorBody
onImageChange={this.onImageChange}
onPositionChange={this.onPositionChange}
onLoadFileError={this.onLoadFileError}
onLoadFile={this.onLoadFile}
deleteImage={this.onDeleteImage}
maxSize={this.props.maxSize * 1000000} // megabytes to bytes
accept={this.props.accept}
image={this.props.image}
chooseFileLabel={this.props.chooseFileLabel}
unknownTypeError={this.props.unknownTypeError}
maxSizeFileError={this.props.maxSizeFileError}
unknownError={this.props.unknownError}
/>
}
footerContent={[
<Button
key="SaveBtn"
label={this.props.saveButtonLabel}
primary={true}
onClick={this.onSaveButtonClick}
/>
]}
onClose={this.props.onClose}
/>
:
<StyledAside
visible={this.state.visible}
scale={true}
>
<Header>
<IconButton
iconName={"ArrowPathIcon"}
color="#A3A9AE"
size="16"
onClick={this.onClose}
/>
</Header>
<AvatarEditorBody
onImageChange={this.onImageChange}
onPositionChange={this.onPositionChange}
@ -96,23 +172,17 @@ class AvatarEditor extends React.Component {
maxSizeFileError={this.props.maxSizeFileError}
unknownError={this.props.unknownError}
/>
}
footerContent={[
<Button
key="SaveBtn"
className="aside-save-button"
label={this.props.saveButtonLabel}
primary={true}
onClick={this.onSaveButtonClick}
/>,
<Button
key="CancelBtn"
label={this.props.cancelButtonLabel}
onClick={this.onClose}
style={{ marginLeft: "8px" }}
/>
]}
onClose={this.props.onClose}
/>
</StyledAside>
);
}
}
@ -134,7 +204,9 @@ AvatarEditor.propTypes = {
onImageChange: PropTypes.func,
unknownTypeError: PropTypes.string,
maxSizeFileError: PropTypes.string,
unknownError: PropTypes.string
unknownError: PropTypes.string,
displayType: PropTypes.oneOf(['auto', 'modal', 'aside']),
};
AvatarEditor.defaultProps = {
@ -145,6 +217,7 @@ AvatarEditor.defaultProps = {
cancelButtonLabel: 'Cancel',
maxSizeErrorLabel: 'File is too big',
accept: ['image/png', 'image/jpeg'],
displayType: 'auto'
};
export default AvatarEditor;

View File

@ -5,7 +5,7 @@ import ReactAvatarEditor from 'react-avatar-editor'
import PropTypes from 'prop-types'
import { default as ASCAvatar } from '../../avatar/index'
import accepts from 'attr-accept'
import {Text} from '../../text'
import { Text } from '../../text'
import { tablet } from '../../../utils/device';
const StyledErrorContainer = styled.div`
@ -58,7 +58,8 @@ const StyledAvatarContainer = styled.div`
text-align: center;
.custom-range{
width: 300px;
width: 100%;
display: block
}
.avatar-container{
display: inline-block;
@ -66,10 +67,11 @@ const StyledAvatarContainer = styled.div`
}
.editor-container{
display: inline-block;
width: calc(100% - 170px);
width: auto;
padding: 0 30px;
position: relative;
@media ${tablet} {
width: 100%
padding: 0;
}
}
`;
@ -109,7 +111,7 @@ class AvatarEditorBody extends React.Component {
this.onDropAccepted = this.onDropAccepted.bind(this);
this.onDropRejected = this.onDropRejected.bind(this);
this.onPositionChange = this.onPositionChange.bind(this);

View File

@ -11,18 +11,18 @@ const StyledAside = styled.aside`
position: fixed;
right: 0;
top: 0;
transform: translateX(${props => (props.visible ? "0" : "240px")});
transform: translateX(${props => (props.visible ? "0" : props.scale ? "100%" : "240px")});
transition: transform 0.3s ease-in-out;
width: 240px;
width: ${props => (props.scale ? "100%" : "240px")};
z-index: 400;
`;
const Aside = React.memo(props => {
//console.log("Aside render");
const { visible, children } = props;
const { visible, children, scale, className} = props;
return (
<StyledAside visible={visible}>
<StyledAside visible={visible} scale={scale} className={className}>
<Scrollbar>{children}</Scrollbar>
</StyledAside>
);
@ -32,10 +32,15 @@ Aside.displayName = "Aside";
Aside.propTypes = {
visible: PropTypes.bool,
scale: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
};
Aside.defaultProps = {
scale: false,
};
export default Aside;

View File

@ -124,7 +124,7 @@ namespace ASC.Web.Studio.Core.Notify
public void UserPasswordChange(int tenantId, UserInfo userInfo)
{
var hash = Hasher.Base64Hash(CoreContext.Authentication.GetUserPasswordHash(tenantId, userInfo.ID));
var confirmationUrl = CommonLinkUtility.GetConfirmationUrl(tenantId, userInfo.Email, ConfirmType.PasswordChange, hash);
var confirmationUrl = CommonLinkUtility.GetConfirmationUrl(tenantId, userInfo.Email, ConfirmType.PasswordChange, hash + userInfo.ID, userInfo.ID);
static string greenButtonText() => WebstudioNotifyPatternResource.ButtonChangePassword;

View File

@ -31,6 +31,7 @@ using System.Threading;
using ASC.Common.Caching;
using ASC.Common.Utils;
using ASC.Core;
using ASC.Core.Configuration;
using ASC.Notify;
using ASC.Notify.Model;
using ASC.Notify.Patterns;
@ -44,7 +45,7 @@ namespace ASC.Web.Studio.Core.Notify
private readonly INotifyClient client;
private readonly ICacheNotify<NotifyItem> cache;
private static string EMailSenderName { get { return ASC.Core.Configuration.Constants.NotifyEMailSenderSysName; } }
private static string EMailSenderName { get { return Constants.NotifyEMailSenderSysName; } }
public StudioNotifyServiceSender()
{
@ -56,7 +57,6 @@ namespace ASC.Web.Studio.Core.Notify
public void OnMessage(NotifyItem item)
{
CoreContext.TenantManager.SetCurrentTenant(item.TenantId);
SecurityContext.AuthenticateMe(item.TenantId, Guid.Parse(item.UserId));
CultureInfo culture = null;
var tenant = CoreContext.TenantManager.GetCurrentTenant(false);
@ -65,10 +65,14 @@ namespace ASC.Web.Studio.Core.Notify
culture = tenant.GetCulture();
}
var user = CoreContext.UserManager.GetUsers(item.TenantId, SecurityContext.CurrentAccount.ID);
if (!string.IsNullOrEmpty(user.CultureName))
if (Guid.TryParse(item.UserId, out var userId) && !userId.Equals(Constants.Guest.ID) && !userId.Equals(Guid.Empty))
{
culture = CultureInfo.GetCultureInfo(user.CultureName);
SecurityContext.AuthenticateMe(item.TenantId, userId);
var user = CoreContext.UserManager.GetUsers(item.TenantId, userId);
if (!string.IsNullOrEmpty(user.CultureName))
{
culture = CultureInfo.GetCultureInfo(user.CultureName);
}
}
if (culture != null && !Equals(Thread.CurrentThread.CurrentCulture, culture))

View File

@ -66,32 +66,6 @@ namespace ASC.Web.Studio.Utility
Storage = 21
}
// emp-invite - confirm ivite by email
// portal-suspend - confirm portal suspending - Tenant.SetStatus(TenantStatus.Suspended)
// portal-continue - confirm portal continuation - Tenant.SetStatus(TenantStatus.Active)
// portal-remove - confirm portal deletation - Tenant.SetStatus(TenantStatus.RemovePending)
// DnsChange - change Portal Address and/or Custom domain name
public enum ConfirmType
{
EmpInvite,
LinkInvite,
PortalSuspend,
PortalContinue,
PortalRemove,
DnsChange,
PortalOwnerChange,
Activation,
EmailChange,
EmailActivation,
PasswordChange,
ProfileRemove,
PhoneActivation,
PhoneAuth,
Auth,
TfaActivation,
TfaAuth
}
public static class CommonLinkUtility
{
private static readonly Regex RegFilePathTrim = new Regex("/[^/]*\\.aspx", RegexOptions.IgnoreCase | RegexOptions.Compiled);
@ -515,7 +489,7 @@ namespace ASC.Web.Studio.Utility
{
var validationKey = EmailValidationKeyProvider.GetEmailKey(tenantId, email + confirmType + (postfix ?? ""));
var link = $"confirm/type={confirmType}?key={validationKey}";
var link = $"confirm?type={confirmType}&key={validationKey}";
if (!string.IsNullOrEmpty(email))
{