Merge branch 'develop' into bugfix/rooms

This commit is contained in:
Maksim Chegulov 2022-10-31 11:34:50 +03:00
commit 0f72136ff0
55 changed files with 652 additions and 234 deletions

View File

@ -0,0 +1,11 @@
node_modules
bin
.yarn
.git
.vscode
.github
Logs
Data
TestsResults
i18next
*.bat

View File

@ -34,6 +34,7 @@ RUN apt-get -y update && \
apt-get install -y nodejs && \
rm -rf /var/lib/apt/lists/*
ADD https://api.github.com/repos/ONLYOFFICE/DocSpace/git/refs/heads/${GIT_BRANCH} version.json
RUN echo ${GIT_BRANCH} && \
git clone --recurse-submodules -b ${GIT_BRANCH} https://github.com/ONLYOFFICE/DocSpace.git ${SRC_PATH}

View File

@ -18,4 +18,6 @@ echo "Stop all backend containers"
# docker compose -f docspace.dev.yml down
docker stop $(docker ps -a | egrep "onlyoffice" | egrep -v "mysql|rabbitmq|redis|elasticsearch|documentserver" | awk 'NR>0 {print $1}')
echo "Remove all backend containers"
docker rm -f $(docker ps -a | egrep "onlyoffice" | egrep -v "mysql|rabbitmq|redis|elasticsearch|documentserver" | awk 'NR>0 {print $1}')
docker rm -f $(docker ps -a | egrep "onlyoffice" | egrep -v "mysql|rabbitmq|redis|elasticsearch|documentserver" | awk 'NR>0 {print $1}')
echo "Remove all backend images"
docker rmi -f $(docker images -a | egrep "onlyoffice" | egrep -v "mysql|rabbitmq|redis|elasticsearch|documentserver" | awk 'NR>0 {print $3}')

View File

@ -46,11 +46,11 @@ public class EmployeeFullDto : EmployeeDto
public string AvatarMax { get; set; }
public string AvatarMedium { get; set; }
public string Avatar { get; set; }
public bool IsDocSpaceAdmin { get; set; }
public bool IsAdmin { get; set; }
public bool IsLDAP { get; set; }
public List<string> ListAdminModules { get; set; }
public bool IsOwner { get; set; }
public bool IsUser { get; set; }
public bool IsVisitor { get; set; }
public string CultureName { get; set; }
public string MobilePhone { get; set; }
public MobilePhoneActivationStatus MobilePhoneActivationStatus { get; set; }
@ -70,7 +70,7 @@ public class EmployeeFullDto : EmployeeDto
Email = "my@gmail.com",
FirstName = "Mike",
Id = Guid.Empty,
IsDocSpaceAdmin = false,
IsAdmin = false,
ListAdminModules = new List<string> { "projects", "crm" },
UserName = "Mike.Zanyatski",
LastName = "Zanyatski",
@ -190,8 +190,8 @@ public class EmployeeFullDtoHelper : EmployeeDtoHelper
Terminated = _apiDateTimeHelper.Get(userInfo.TerminatedDate),
WorkFrom = _apiDateTimeHelper.Get(userInfo.WorkFromDate),
Email = userInfo.Email,
IsUser = _userManager.IsUser(userInfo),
IsDocSpaceAdmin = _userManager.IsDocSpaceAdmin(userInfo),
IsVisitor = _userManager.IsUser(userInfo),
IsAdmin = _userManager.IsDocSpaceAdmin(userInfo),
IsOwner = userInfo.IsOwner(_context.Tenant),
IsLDAP = userInfo.IsLDAP(),
IsSSO = userInfo.IsSSO()

View File

@ -62,9 +62,9 @@ public class DocSpaceLinkHelper
return _signature.Create(linkId);
}
public string MakeKey(string email)
public string MakeKey(string email, EmployeeType employeeType)
{
return email + ConfirmType.LinkInvite.ToStringFast() + EmployeeType.RoomAdmin.ToStringFast();
return email + ConfirmType.LinkInvite.ToStringFast() + employeeType.ToStringFast();
}
public Guid Parse(string key)
@ -72,14 +72,14 @@ public class DocSpaceLinkHelper
return _signature.Read<Guid>(key);
}
public ValidationResult Validate(string key, string email)
public ValidationResult Validate(string key, string email, EmployeeType employeeType)
{
return string.IsNullOrEmpty(email) ? ValidateExternalLink(key) : ValidateEmailLink(email, key);
return string.IsNullOrEmpty(email) ? ValidateRoomExternalLink(key) : ValidateEmailLink(email, key, employeeType);
}
private ValidationResult ValidateEmailLink(string email, string key)
public ValidationResult ValidateEmailLink(string email, string key, EmployeeType employeeType)
{
var result = _emailValidationKeyProvider.ValidateEmailKey(MakeKey(email), key, ExpirationInterval);
var result = _emailValidationKeyProvider.ValidateEmailKey(MakeKey(email, employeeType), key, ExpirationInterval);
if (result == ValidationResult.Ok)
{
@ -94,16 +94,16 @@ public class DocSpaceLinkHelper
return result;
}
private ValidationResult ValidateExternalLink(string key)
public ValidationResult ValidateRoomExternalLink(string key)
{
var payload = Parse(key);
if (payload == default)
{
return ValidationResult.Invalid;
}
return payload == default ? ValidationResult.Invalid : ValidationResult.Ok;
}
return ValidationResult.Ok;
public ValidationResult ValidateExtarnalLink(string key, EmployeeType employeeType)
{
return _emailValidationKeyProvider.ValidateEmailKey(ConfirmType.LinkInvite.ToStringFast() + (int)employeeType, key);
}
private bool CanUsed(string email, string key, TimeSpan interval)

View File

@ -110,11 +110,12 @@ public class EmailValidationKeyModelHelper
break;
case ConfirmType.LinkInvite:
checkKeyResult = _docSpaceLinkHelper.Validate(key, email);
checkKeyResult = string.IsNullOrEmpty(email) ? _docSpaceLinkHelper.ValidateRoomExternalLink(key)
: _docSpaceLinkHelper.ValidateEmailLink(email, key, emplType ?? default);
if (checkKeyResult == ValidationResult.Invalid)
{
checkKeyResult = _provider.ValidateEmailKey(type.ToString() + (int)emplType, key, _provider.ValidEmailKeyInterval);
checkKeyResult = _provider.ValidateEmailKey(type.ToString() + (int)(emplType ?? default), key, _provider.ValidEmailKeyInterval);
}
break;

View File

@ -60,7 +60,7 @@ public class UserServiceCache
CacheGroupCacheItem = cacheGroupCacheItem;
CacheUserGroupRefItem = cacheUserGroupRefItem;
cacheUserInfoItem.Subscribe(InvalidateCache, CacheNotifyAction.Any);
cacheUserInfoItem.Subscribe((u) => InvalidateCache(u), CacheNotifyAction.Any);
cacheUserPhotoItem.Subscribe((p) => Cache.Remove(p.Key), CacheNotifyAction.Remove);
cacheGroupCacheItem.Subscribe((g) => InvalidateCache(g), CacheNotifyAction.Any);
@ -72,7 +72,7 @@ public class UserServiceCache
{
if (userInfo != null)
{
var key = GetUserCacheKey(userInfo.Tenant, new Guid(userInfo.Id));
var key = GetUserCacheKey(userInfo.Tenant);
Cache.Remove(key);
}
}

View File

@ -36,5 +36,6 @@ public enum EmployeeType
{
All = 0,
RoomAdmin = 1,
User = 2
User = 2,
DocSpaceAdmin = 3,
}

View File

@ -90,6 +90,11 @@ public class DisplayUserSettingsHelper
}
var result = _userFormatter.GetUserName(userInfo, format);
if (string.IsNullOrWhiteSpace(result))
{
result = userInfo.Email;
}
return withHtmlEncode ? HtmlEncode(result) : result;
}
public string HtmlEncode(string str)

View File

@ -12,5 +12,6 @@
"Add": "Add",
"Invited": "Invited",
"EmailErrorMessage": "Email address not valid. You can edit the email by clicking on it.",
"СhooseFromList": "Сhoose from list"
"СhooseFromList": "Сhoose from list",
"InviteUsers": "Invite users"
}

View File

@ -46,16 +46,15 @@
"Remove": "Remove",
"RoleCommentator": "Commentator",
"RoleCommentatorDescription": "Operations with existing files: viewing, commenting.",
"RoleDocSpaceAdminDescription": "DocSpace admins can access DocSpace settings, manage and archive rooms, invite new users and assign roles below their level. All admins have access to the Personal section.",
"RoleEditor": "Editor",
"RoleEditorDescription": "Operations with existing files: viewing, editing, form filling, reviewing, commenting.",
"RoleFormFiller": "Form filler",
"RoleFormFillerDescription": "Operations with existing files: viewing, form filling, reviewing, commenting.",
"RoleReviewer": "Reviewer",
"RoleReviewerDescription": "Operations with existing files: viewing, reviewing, commenting.",
"RoleRoomAdmin": "Room admin",
"RoleRoomAdminDescription": "Room admins can create and manage the assigned rooms, invite new users and assign roles below their level. All admins have access to the Personal section.",
"RoleDocSpaceAdmin": "DocSpace admin",
"RoleDocSpaceAdminDescription": "DocSpace admins can access DocSpace settings, manage and archive rooms, invite new users and assign roles below their level. All admins have access to the Personal section.",
"RoleUserDescription": "Users can only access the rooms they are invited to by admins. They can't create own rooms, folders or files.",
"RoleViewer": "Viewer",
"RoleViewerDescription": "File viewing",
"Spreadsheets": "Spreadsheets",

View File

@ -6,9 +6,6 @@ import CatalogItem from "@docspace/components/catalog-item";
import { FolderType, ShareAccessRights } from "@docspace/common/constants";
import { withTranslation } from "react-i18next";
import DragAndDrop from "@docspace/components/drag-and-drop";
import withLoader from "../../../HOCs/withLoader";
import Loaders from "@docspace/common/components/Loaders";
import Loader from "@docspace/components/loader";
import { isMobile } from "react-device-detect";
const StyledDragAndDrop = styled(DragAndDrop)`
@ -34,7 +31,7 @@ const Item = ({
labelBadge,
iconBadge,
}) => {
const [isDragActive, setIsDragActive] = React.useState(false);
const [isDragActive, setIsDragActive] = useState(false);
const isDragging = dragging ? showDragItems(item) : false;
@ -125,6 +122,7 @@ const Items = ({
data,
showText,
pathParts,
rootFolderType,
selectedTreeNode,
onClick,
onBadgeClick,
@ -161,6 +159,13 @@ const Items = ({
if (selectedTreeNode.length > 0) {
const isMainFolder = dataMainTree.indexOf(selectedTreeNode[0]) !== -1;
if (
rootFolderType === FolderType.Rooms &&
item.rootFolderType === FolderType.Rooms
) {
return true;
}
if (pathParts && pathParts.includes(item.id) && !isMainFolder)
return true;
@ -173,7 +178,7 @@ const Items = ({
return `${item.id}` === selectedTreeNode[0];
}
},
[selectedTreeNode, pathParts, docSpace]
[selectedTreeNode, pathParts, docSpace, rootFolderType]
);
const getEndOfBlock = React.useCallback(
(item) => {
@ -348,7 +353,7 @@ const Items = ({
);
if (isVisitor) {
items.length > 1 && items.splice(1, 0, filesHeader);
items.length > 1 && items.splice(2, 0, filesHeader);
} else {
items.splice(3, 0, filesHeader);
}
@ -417,7 +422,7 @@ export default inject(
isPrivacyFolder,
} = treeFoldersStore;
const { id } = selectedFolderStore;
const { id, pathParts, rootFolderType } = selectedFolderStore;
const { moveDragItems, uploadEmptyFolders } = filesActionsStore;
const { setEmptyTrashDialogVisible } = dialogsStore;
@ -430,7 +435,7 @@ export default inject(
currentId: id,
showText: auth.settingsStore.showText,
docSpace: auth.settingsStore.docSpace,
pathParts: selectedFolderStore.pathParts,
pathParts,
data: treeFolders,
selectedTreeNode,
draggableItems: dragging ? selection : null,
@ -442,6 +447,7 @@ export default inject(
uploadEmptyFolders,
setEmptyTrashDialogVisible,
trashIsEmpty,
rootFolderType,
};
}
)(withTranslation(["Files", "Common", "Translations"])(observer(Items)));

View File

@ -14,7 +14,7 @@ import MobileView from "./MobileView";
import { combineUrl } from "@docspace/common/utils";
import config from "PACKAGE_FILE";
import withLoader from "../../../HOCs/withLoader";
import { Events } from "@docspace/common/constants";
import { Events, EmployeeType } from "@docspace/common/constants";
import { getMainButtonItems } from "SRC_DIR/helpers/plugins";
import toastr from "@docspace/components/toast/toastr";
@ -91,6 +91,8 @@ const ArticleMainButtonContent = (props) => {
isOwner,
isAdmin,
isVisitor,
setInvitePanelOptions,
} = props;
const isAccountsPage = selectedTreeNode[0] === "accounts";
@ -171,8 +173,13 @@ const ArticleMainButtonContent = (props) => {
const onInvite = React.useCallback((e) => {
const type = e.action;
toastr.warning("Work in progress " + type);
console.log("invite ", type);
setInvitePanelOptions({
visible: true,
roomId: -1,
hideSelector: true,
defaultAccess: type,
});
}, []);
const onInviteAgain = React.useCallback(() => {
@ -265,7 +272,7 @@ const ArticleMainButtonContent = (props) => {
icon: "/static/images/person.admin.react.svg",
label: t("Common:DocSpaceAdmin"),
onClick: onInvite,
action: "administrator",
action: EmployeeType.Admin,
key: "administrator",
},
{
@ -274,7 +281,7 @@ const ArticleMainButtonContent = (props) => {
icon: "/static/images/person.manager.react.svg",
label: t("Common:RoomAdmin"),
onClick: onInvite,
action: "manager",
action: EmployeeType.User,
key: "manager",
},
{
@ -283,7 +290,7 @@ const ArticleMainButtonContent = (props) => {
icon: "/static/images/person.user.react.svg",
label: t("Common:User"),
onClick: onInvite,
action: "user",
action: EmployeeType.Guest,
key: "user",
},
]
@ -492,7 +499,7 @@ export default inject(
selectedTreeNode,
} = treeFoldersStore;
const { startUpload } = uploadDataStore;
const { setSelectFileDialogVisible } = dialogsStore;
const { setSelectFileDialogVisible, setInvitePanelOptions } = dialogsStore;
const isArticleLoading = (!isLoaded || isLoading) && firstLoad;
@ -522,6 +529,7 @@ export default inject(
startUpload,
setSelectFileDialogVisible,
setInvitePanelOptions,
isLoading,
isLoaded,

View File

@ -59,9 +59,10 @@ const RootFolderContainer = (props) => {
const trashDescription = t("TrashEmptyDescription");
const favoritesDescription = t("FavoritesEmptyContainerDescription");
const recentDescription = t("RecentEmptyContainerDescription");
const roomsDescription = isVisitor
? t("RoomEmptyContainerDescription")
: t("RoomEmptyContainerDescriptionUser");
? t("RoomEmptyContainerDescriptionUser")
: t("RoomEmptyContainerDescription");
const archiveRoomsDescription = t("ArchiveEmptyScreen");
const privateRoomHeader = t("PrivateRoomHeader");

View File

@ -29,13 +29,19 @@ const InvitePanel = ({
visible,
setRoomSecurity,
getRoomSecurityInfo,
getPortalInviteLinks,
userLink,
guestLink,
adminLink,
defaultAccess,
inviteUsers,
}) => {
const [selectedRoom, setSelectedRoom] = useState(null);
const [hasErrors, setHasErrors] = useState(false);
const [shareLinks, setShareLinks] = useState([]);
const [roomUsers, setRoomUsers] = useState([]);
useEffect(() => {
const selectRoom = () => {
const room = folders.find((folder) => folder.id === roomId);
if (room) {
@ -45,7 +51,9 @@ const InvitePanel = ({
setSelectedRoom(info);
});
}
};
const getInfo = () => {
getRoomSecurityInfo(roomId).then((users) => {
let links = [];
@ -58,6 +66,7 @@ const InvitePanel = ({
title,
shareLink,
expirationDate,
access: defaultAccess,
});
}
});
@ -65,7 +74,39 @@ const InvitePanel = ({
setShareLinks(links);
setRoomUsers(users);
});
}, [roomId]);
};
useEffect(() => {
if (roomId === -1) {
if (!userLink || !guestLink || !adminLink) getPortalInviteLinks();
setShareLinks([
{
id: "user",
title: "User",
shareLink: userLink,
access: 1,
},
{
id: "guest",
title: "Guest",
shareLink: guestLink,
access: 2,
},
{
id: "admin",
title: "Admin",
shareLink: adminLink,
access: 3,
},
]);
return;
}
selectRoom();
getInfo();
}, [roomId, userLink, guestLink, adminLink]);
useEffect(() => {
const hasErrors = inviteItems.some((item) => !!item.errors?.length);
@ -74,7 +115,11 @@ const InvitePanel = ({
}, [inviteItems]);
const onClose = () => {
setInvitePanelOptions({ visible: false });
setInvitePanelOptions({
visible: false,
hideSelector: false,
defaultAccess: 1,
});
setInviteItems([]);
};
@ -88,7 +133,11 @@ const InvitePanel = ({
const onClickSend = async (e) => {
const invitations = inviteItems.map((item) => {
let newItem = { access: item.access };
let newItem = {};
roomId === -1
? (newItem.type = item.access)
: (newItem.access = item.access);
item.avatar ? (newItem.id = item.id) : (newItem.email = item.email);
@ -97,20 +146,25 @@ const InvitePanel = ({
const data = {
invitations,
notify: true,
message: "Invitation message",
};
if (roomId !== -1) {
data.notify = true;
data.message = "Invitation message";
}
try {
await setRoomSecurity(roomId, data);
roomId === -1
? await inviteUsers(data)
: await setRoomSecurity(roomId, data);
onClose();
toastr.success(`Users invited to ${selectedRoom.title}`);
toastr.success(`Users invited`);
} catch (err) {
toastr.error(err);
}
};
const roomType = selectedRoom ? selectedRoom.roomType : 5;
const roomType = selectedRoom ? selectedRoom.roomType : -1;
return (
<StyledInvitePanel>
@ -127,7 +181,9 @@ const InvitePanel = ({
withoutBodyScroll
>
<StyledBlock>
<StyledHeading>{t("InviteUsersToRoom")}</StyledHeading>
<StyledHeading>
{roomId === -1 ? t("InviteUsers") : t("InviteUsersToRoom")}
</StyledHeading>
</StyledBlock>
<ExternalLinks t={t} shareLinks={shareLinks} roomType={roomType} />
@ -168,7 +224,14 @@ const InvitePanel = ({
export default inject(({ auth, peopleStore, filesStore, dialogsStore }) => {
const { theme } = auth.settingsStore;
const { getUsersByQuery } = peopleStore.usersStore;
const { getUsersByQuery, inviteUsers } = peopleStore.usersStore;
const {
getPortalInviteLinks,
userLink,
guestLink,
adminLink,
} = peopleStore.inviteLinksStore;
const {
inviteItems,
@ -195,7 +258,13 @@ export default inject(({ auth, peopleStore, filesStore, dialogsStore }) => {
setRoomSecurity,
theme,
visible: invitePanelOptions.visible,
defaultAccess: invitePanelOptions.defaultAccess,
getFolderInfo,
getPortalInviteLinks,
userLink,
guestLink,
adminLink,
inviteUsers,
};
})(
withTranslation([

View File

@ -12,10 +12,11 @@ const AccessSelector = ({
defaultAccess,
}) => {
const width = containerRef?.current?.offsetWidth - 32;
const accessOptions = getAccessOptions(t, roomType, false, true);
const selectedOption = accessOptions.filter(
(access) => access.access === defaultAccess
(access) => access.access === +defaultAccess
)[0];
return (

View File

@ -22,25 +22,45 @@ import {
const ExternalLinks = ({
t,
hideSelector,
roomId,
roomType,
defaultAccess,
shareLinks,
setInvitationLinks,
}) => {
const [linksVisible, setLinksVisible] = useState(false);
const [actionLinksVisible, setActionLinksVisible] = useState(false);
const [activeLink, setActiveLink] = useState({});
const inputsRef = useRef();
const toggleLinks = (e) => {
if (roomId === -1) {
const link = shareLinks.find((l) => l.access === +defaultAccess);
setActiveLink(link);
} else {
setInvitationLinks(roomId, shareLinks[0].id, "Invite", +defaultAccess);
setActiveLink(shareLinks[0]);
}
setLinksVisible(!linksVisible);
if (!linksVisible) copyLink(shareLinks[0].shareLink);
if (!linksVisible) copyLink(activeLink.shareLink);
};
const onSelectAccess = (access) => {
console.log(access);
if (roomId === -1) {
const link = shareLinks.find((l) => l.access === access.access);
setActiveLink(link);
} else {
setInvitationLinks(roomId, shareLinks[0].id, "Invite", +access.access);
setActiveLink(shareLinks[0]);
}
copyLink(activeLink.shareLink);
};
const copyLink = (link) => {
@ -73,7 +93,7 @@ const ExternalLinks = ({
closeActionLinks();
},
[closeActionLinks, links, t]
[closeActionLinks, t]
);
const shareTwitter = useCallback(
@ -90,39 +110,9 @@ const ExternalLinks = ({
closeActionLinks();
},
[closeActionLinks, links]
[closeActionLinks]
);
const links =
!!shareLinks.length &&
shareLinks?.map((link) => {
return (
<StyledInviteInputContainer key={link.id}>
<StyledInviteInput>
<InputBlock
scale
value={link.shareLink}
isReadOnly
iconName="/static/images/copy.react.svg"
onIconClick={() => copyLink(link.shareLink)}
hoverColor="#333333"
iconColor="#A3A9AE"
/>
</StyledInviteInput>
{!hideSelector && (
<AccessSelector
t={t}
roomType={roomType}
defaultAccess={defaultAccess}
onSelectAccess={onSelectAccess}
containerRef={inputsRef}
/>
)}
</StyledInviteInputContainer>
);
});
return (
<StyledBlock noPadding ref={inputsRef}>
<StyledSubHeader inline>
@ -156,17 +146,41 @@ const ExternalLinks = ({
)}
<StyledToggleButton isChecked={linksVisible} onChange={toggleLinks} />
</StyledSubHeader>
{linksVisible && links}
{linksVisible && (
<StyledInviteInputContainer key={activeLink.id}>
<StyledInviteInput>
<InputBlock
scale
value={activeLink.shareLink}
isReadOnly
iconName="/static/images/copy.react.svg"
onIconClick={() => copyLink(activeLink.shareLink)}
hoverColor="#333333"
iconColor="#A3A9AE"
/>
</StyledInviteInput>
<AccessSelector
t={t}
roomType={roomType}
defaultAccess={activeLink.access}
onSelectAccess={onSelectAccess}
containerRef={inputsRef}
/>
</StyledInviteInputContainer>
)}
</StyledBlock>
);
};
export default inject(({ dialogsStore, filesStore }) => {
const { invitePanelOptions } = dialogsStore;
const { setInvitationLinks } = filesStore;
const { roomId, hideSelector, defaultAccess } = invitePanelOptions;
return {
roomId: invitePanelOptions.roomId,
hideSelector: invitePanelOptions.hideSelector,
defaultAccess: invitePanelOptions.defaultAccess,
setInvitationLinks,
roomId,
hideSelector,
defaultAccess,
};
})(observer(ExternalLinks));

View File

@ -82,6 +82,8 @@ const InviteInput = ({
setUsersList(users);
} else {
closeInviteInputPanel();
setInputValue("");
setUsersList([]);
}
};
@ -125,6 +127,8 @@ const InviteInput = ({
const items = removeExist([item, ...inviteItems]);
setInviteItems(items);
closeInviteInputPanel();
setInputValue("");
setUsersList([]);
};
return (
@ -157,6 +161,8 @@ const InviteInput = ({
setInviteItems(filtered);
closeInviteInputPanel();
setInputValue("");
setUsersList([]);
};
const addItems = (users) => {
@ -166,6 +172,8 @@ const InviteInput = ({
setInviteItems(filtered);
closeInviteInputPanel();
setInputValue("");
setUsersList([]);
};
const dropDownMaxHeight = usersList.length > 5 ? { maxHeight: 240 } : {};
@ -186,9 +194,6 @@ const InviteInput = ({
const closeInviteInputPanel = (e) => {
if (e?.target.tagName.toUpperCase() == "INPUT") return;
setInputValue("");
setUsersList([]);
setSearchPanelVisible(false);
};
@ -215,14 +220,16 @@ const InviteInput = ({
<>
<StyledSubHeader>
{t("IndividualInvitation")}
<StyledLink
fontWeight="600"
type="action"
isHovered
onClick={openUsersPanel}
>
{t("СhooseFromList")}
</StyledLink>
{!hideSelector && (
<StyledLink
fontWeight="600"
type="action"
isHovered
onClick={openUsersPanel}
>
{t("СhooseFromList")}
</StyledLink>
)}
</StyledSubHeader>
<StyledInviteInputContainer ref={inputsRef}>
@ -258,17 +265,15 @@ const InviteInput = ({
)}
</StyledDropDown>
{!hideSelector && (
<AccessSelector
t={t}
roomType={roomType}
defaultAccess={defaultAccess}
onSelectAccess={onSelectAccess}
containerRef={inputsRef}
/>
)}
<AccessSelector
t={t}
roomType={roomType}
defaultAccess={selectedAccess}
onSelectAccess={onSelectAccess}
containerRef={inputsRef}
/>
{addUsersPanelVisible && (
{!hideSelector && addUsersPanelVisible && (
<AddUsersPanel
onParentPanelClose={onClose}
onClose={closeUsersPanel}

View File

@ -36,7 +36,7 @@ const Item = ({
const accesses = getAccessOptions(t, roomType, true);
const defaultAccess = accesses.find((option) => option.access === access);
const defaultAccess = accesses.find((option) => option.access === +access);
const errorsInList = () => {
const hasErrors = inviteItems.some((item) => !!item.errors?.length);

View File

@ -1,4 +1,8 @@
import { ShareAccessRights, RoomsType } from "@docspace/common/constants";
import {
ShareAccessRights,
RoomsType,
EmployeeType,
} from "@docspace/common/constants";
export const getAccessOptions = (
t,
@ -10,19 +14,27 @@ export const getAccessOptions = (
const accesses = {
docSpaceAdmin: {
key: "docSpaceAdmin",
label: t("Translations:RoleDocSpaceAdmin"),
label: t("Common:DocSpaceAdmin"),
description: t("Translations:RoleDocSpaceAdminDescription"),
quota: t("Common:Paid"),
color: "#EDC409",
access: ShareAccessRights.FullAccess,
access:
roomType === -1 ? EmployeeType.Admin : ShareAccessRights.FullAccess,
},
roomAdmin: {
key: "roomAdmin",
label: t("Translations:RoleRoomAdmin"),
label: t("Common:RoomAdmin"),
description: t("Translations:RoleRoomAdminDescription"),
quota: t("Common:Paid"),
color: "#EDC409",
access: ShareAccessRights.RoomManager,
access:
roomType === -1 ? EmployeeType.User : ShareAccessRights.RoomManager,
},
user: {
key: "user",
label: t("Common:User"),
description: t("Translations:RoleUserDescription"),
access: EmployeeType.Guest,
},
editor: {
key: "editor",
@ -100,6 +112,14 @@ export const getAccessOptions = (
accesses.viewer,
];
break;
case -1:
options = [
accesses.docSpaceAdmin,
accesses.roomAdmin,
{ key: "s1", isSeparator: withSeparator },
accesses.user,
];
break;
}
const removeOption = [

View File

@ -77,7 +77,6 @@ const Dialogs = ({
{...data}
/>
)}
{changeUserStatusDialogVisible && (
<ChangeUserStatusDialog
visible={changeUserStatusDialogVisible}
@ -85,14 +84,12 @@ const Dialogs = ({
{...data}
/>
)}
{sendInviteDialogVisible && (
<SendInviteDialog
visible={sendInviteDialogVisible}
onClose={closeDialogs}
/>
)}
{deleteDialogVisible && (
<DeleteUsersDialog
visible={deleteDialogVisible}

View File

@ -20,6 +20,7 @@ import { Base } from "@docspace/components/themes";
import IconButton from "@docspace/components/icon-button";
import toastr from "@docspace/components/toast/toastr";
import withPeopleLoader from "SRC_DIR/HOCs/withPeopleLoader";
import { EmployeeType } from "@docspace/common/constants";
const StyledContainer = styled.div`
width: 100%;
@ -163,6 +164,7 @@ const SectionHeaderContent = (props) => {
isInfoPanelVisible,
isOwner,
isAdmin,
setInvitePanelOptions,
} = props;
//console.log("SectionHeaderContent render");
@ -202,9 +204,14 @@ const SectionHeaderContent = (props) => {
const headerMenu = getHeaderMenu(t);
const onInvite = React.useCallback((e) => {
const type = e.target.dataset.action;
toastr.warning("Work in progress " + type);
console.log("invite ", type);
const type = e.target.dataset.type;
setInvitePanelOptions({
visible: true,
roomId: -1,
hideSelector: true,
defaultAccess: type,
});
}, []);
const onInviteAgain = React.useCallback(() => {
@ -224,7 +231,7 @@ const SectionHeaderContent = (props) => {
icon: "/static/images/person.admin.react.svg",
label: t("Common:DocSpaceAdmin"),
onClick: onInvite,
"data-action": "administrator",
"data-type": EmployeeType.Admin,
key: "administrator",
},
{
@ -233,7 +240,7 @@ const SectionHeaderContent = (props) => {
icon: "/static/images/person.manager.react.svg",
label: t("Common:RoomAdmin"),
onClick: onInvite,
"data-action": "manager",
"data-type": EmployeeType.User,
key: "manager",
},
{
@ -242,7 +249,7 @@ const SectionHeaderContent = (props) => {
icon: "/static/images/person.user.react.svg",
label: t("Common:User"),
onClick: onInvite,
"data-action": "user",
"data-type": EmployeeType.Guest,
key: "user",
},
{
@ -323,12 +330,14 @@ const SectionHeaderContent = (props) => {
};
export default withRouter(
inject(({ auth, peopleStore }) => {
inject(({ auth, peopleStore, dialogsStore }) => {
const {
setIsVisible: setInfoPanelIsVisible,
isVisible: isInfoPanelVisible,
} = auth.infoPanelStore;
const { setInvitePanelOptions } = dialogsStore;
const { isOwner, isAdmin } = auth.userStore.user;
const { selectionStore, headerMenuStore, getHeaderMenu } = peopleStore;
@ -355,6 +364,7 @@ export default withRouter(
isInfoPanelVisible,
isOwner,
isAdmin,
setInvitePanelOptions,
};
})(
withTranslation([

View File

@ -218,13 +218,15 @@ const RegisterContainer = styled.div`
`;
const Confirm = (props) => {
const { settings, t, greetingTitle, providers, isDesktop } = props;
const { settings, t, greetingTitle, providers, isDesktop, linkData } = props;
const inputRef = React.useRef(null);
const emailFromLink = linkData.email ? linkData.email : "";
const [moreAuthVisible, setMoreAuthVisible] = useState(false);
const [ssoLabel, setSsoLabel] = useState("");
const [ssoUrl, setSsoUrl] = useState("");
const [email, setEmail] = useState("");
const [email, setEmail] = useState(emailFromLink);
const [emailValid, setEmailValid] = useState(true);
const [emailErrorText, setEmailErrorText] = useState("");
@ -286,7 +288,7 @@ const Confirm = (props) => {
const onSubmit = () => {
const { defaultPage, linkData, hashSettings } = props;
const isVisitor = parseInt(linkData.emplType) === 2;
const type = parseInt(linkData.emplType);
setIsLoading(true);
@ -335,13 +337,17 @@ const Confirm = (props) => {
email: email,
};
const registerData = Object.assign(personalData, {
isVisitor: isVisitor,
});
if (!!type) {
personalData.type = type;
}
const key = props.linkData.confirmHeader;
if (!!linkData.key) {
personalData.key = linkData.key;
}
createConfirmUser(registerData, loginData, key)
const headerKey = linkData.confirmHeader;
createConfirmUser(personalData, loginData, headerKey)
.then(() => window.location.replace(defaultPage))
.catch((error) => {
console.error("confirm error", error);
@ -637,7 +643,7 @@ const Confirm = (props) => {
scale={true}
isAutoFocussed={true}
tabIndex={1}
isDisabled={isLoading}
isDisabled={isLoading || !!emailFromLink}
autoComplete="username"
onChange={onChangeEmail}
onBlur={onBlurEmail}

View File

@ -97,11 +97,6 @@ const FilesSection = React.memo(() => {
"/rooms/personal",
"/rooms/personal/filter",
"/rooms/archived",
"/rooms/archived/filter",
"/rooms/archived/:room",
"/rooms/archived/:room/filter",
"/files/trash",
"/files/trash/filter",
]}
@ -115,6 +110,11 @@ const FilesSection = React.memo(() => {
"/rooms/shared/:room",
"/rooms/shared/:room/filter",
"/rooms/archived",
"/rooms/archived/filter",
"/rooms/archived/:room",
"/rooms/archived/:room/filter",
"/files/favorite",
"/files/favorite/filter",

View File

@ -388,7 +388,13 @@ const MainContainer = styled.div`
const StyledCard = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(216px, 1fr));
${({ isSingle }) =>
!isSingle &&
css`
grid-template-columns: repeat(auto-fill, minmax(216px, 1fr));
`};
height: ${({ cardHeight }) => `${cardHeight}px`};
`;

View File

@ -1,17 +1,22 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { inject, observer } from "mobx-react";
import InfiniteLoaderComponent from "@docspace/components/infinite-loader";
import { StyledCard, StyledItem } from "../StyledTileView";
import Loaders from "@docspace/common/components/Loaders";
import uniqueid from "lodash/uniqueId";
const Card = ({ children, ...rest }) => {
const Card = ({ children, countTilesInRow, ...rest }) => {
const horizontalGap = 16;
const fileHeight = 220 + horizontalGap;
const cardHeight = fileHeight;
return (
<StyledCard className="Card" cardHeight={cardHeight} {...rest}>
<StyledCard
className="Card"
cardHeight={cardHeight}
isSingle={countTilesInRow}
{...rest}
>
{children}
</StyledCard>
);
@ -37,7 +42,7 @@ const InfiniteGrid = (props) => {
...rest
} = props;
const countTilesInRow = getCountTilesInRow();
const [countTilesInRow, setCountTilesInRow] = useState(getCountTilesInRow());
let cards = [];
const list = [];
@ -51,6 +56,24 @@ const InfiniteGrid = (props) => {
if (clear) cards = [];
};
const setTilesCount = () => {
const newCount = getCountTilesInRow();
if (countTilesInRow !== newCount) setCountTilesInRow(newCount);
};
const onResize = () => {
setTilesCount();
};
useEffect(() => {
setTilesCount();
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
});
React.Children.map(children, (child) => {
if (child) {
if (cards.length && cards.length === countTilesInRow) {
@ -59,7 +82,11 @@ const InfiniteGrid = (props) => {
}
const cardKey = uniqueid("card-item_");
cards.push(<Card key={cardKey}>{child}</Card>);
cards.push(
<Card countTilesInRow={countTilesInRow} key={cardKey}>
{child}
</Card>
);
}
});

View File

@ -67,6 +67,7 @@ const InfoPanelBodyContent = ({
isAdmin: props.isAdmin,
getRoomMembers: props.getRoomMembers,
changeUserType: props.changeUserType,
setInvitePanelOptions: props.setInvitePanelOptions,
},
historyProps: {
selectedFolder: selectedFolder,
@ -234,7 +235,7 @@ export default inject(
const { getIcon, getFolderIcon } = settingsStore;
const { onSelectItem, openLocationAction } = filesActionsStore;
const { changeType: changeUserType } = peopleStore;
const { setSharingPanelVisible } = dialogsStore;
const { setSharingPanelVisible, setInvitePanelOptions } = dialogsStore;
const { isRootFolder } = selectedFolderStore;
const { gallerySelected } = oformsStore;
const {
@ -331,6 +332,7 @@ export default inject(
getRoomHistory,
getFileHistory,
setSharingPanelVisible,
setInvitePanelOptions,
getIcon,
getFolderIcon,

View File

@ -6,6 +6,8 @@ import Loaders from "@docspace/common/components/Loaders";
import { StyledUserList, StyledUserTypeHeader } from "../../styles/members";
import { ShareAccessRights } from "@docspace/common/constants";
import IconButton from "@docspace/components/icon-button";
import Text from "@docspace/components/text";
import User from "./User";
@ -23,6 +25,7 @@ const Members = ({
getRoomMembers,
changeUserType,
setInvitePanelOptions,
}) => {
const [members, setMembers] = useState(null);
const [showLoader, setShowLoader] = useState(false);
@ -65,7 +68,12 @@ const Members = ({
}, [selection]);
const onAddUsers = () => {
toastr.warning("Work in progress");
setInvitePanelOptions({
visible: true,
roomId: selection.id,
hideSelector: false,
defaultAccess: ShareAccessRights.ReadOnly,
});
};
if (showLoader) return <Loaders.InfoPanelViewLoader view="members" />;

View File

@ -13,7 +13,7 @@ const HeaderItem = ({ children, className, ...rest }) => {
);
};
const Card = ({ children, ...rest }) => {
const Card = ({ children, countTilesInRow, ...rest }) => {
const getItemSize = (child) => {
const isFile = child?.props?.className?.includes("file");
const isFolder = child?.props?.className?.includes("folder");
@ -37,7 +37,12 @@ const Card = ({ children, ...rest }) => {
const cardHeight = getItemSize(children);
return (
<StyledCard className="Card" cardHeight={cardHeight} {...rest}>
<StyledCard
isSingle={countTilesInRow}
className="Card"
cardHeight={cardHeight}
{...rest}
>
{children}
</StyledCard>
);
@ -143,7 +148,11 @@ const InfiniteGrid = (props) => {
}
const cardKey = uniqueid("card-item_");
cards.push(<Card key={cardKey}>{child}</Card>);
cards.push(
<Card countTilesInRow={countTilesInRow} key={cardKey}>
{child}
</Card>
);
}
}
});

View File

@ -14,7 +14,11 @@ const paddingCss = css`
const StyledCard = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(216px, 1fr));
${({ isSingle }) =>
!isSingle &&
css`
grid-template-columns: repeat(auto-fill, minmax(216px, 1fr));
`};
height: ${({ cardHeight }) => `${cardHeight}px`};
`;

View File

@ -10,6 +10,7 @@ import {
hideLoader,
frameCallbackData,
frameCallCommand,
getObjectByLocation,
} from "@docspace/common/utils";
import FilesFilter from "@docspace/common/api/files/filter";
import { getGroup } from "@docspace/common/api/groups";
@ -80,10 +81,13 @@ class PureHome extends React.Component {
return;
}
const isRoomFolder = getObjectByLocation(window.location)?.folder;
if (
categoryType == CategoryType.Shared ||
categoryType == CategoryType.SharedRoom ||
categoryType == CategoryType.Archive
(categoryType == CategoryType.Shared ||
categoryType == CategoryType.SharedRoom ||
categoryType == CategoryType.Archive) &&
!isRoomFolder
) {
filterObj = RoomsFilter.getFilter(window.location);

View File

@ -419,7 +419,7 @@ class ContextOptionsStore {
const { isGracePeriod } = this.authStore.currentTariffStatusStore;
const { isFreeTariff } = this.authStore.currentQuotaStore;
if (isGracePeriod || isFreeTariff) {
if (isGracePeriod) {
this.dialogsStore.setInviteUsersWarningDialogVisible(true);
} else {
this.dialogsStore.setInvitePanelOptions({

View File

@ -2694,8 +2694,8 @@ class FilesStore {
return Math.floor(sectionWidth / minTileWidth);
};
setInvitationLinks = async (id, linkId, title, access) => {
return await api.rooms.setInvitationLinks(id, linkId, title, access);
setInvitationLinks = async (roomId, linkId, title, access) => {
return await api.rooms.setInvitationLinks(roomId, linkId, title, access);
};
resendEmailInvitations = async (id, usersIds) => {

View File

@ -8,6 +8,7 @@ class InviteLinksStore {
peopleStore = null;
userLink = null;
guestLink = null;
adminLink = null;
constructor(peopleStore) {
this.peopleStore = peopleStore;
@ -17,18 +18,25 @@ class InviteLinksStore {
setUserLink = (link) => {
this.userLink = link;
};
setGuestLink = (link) => {
this.guestLink = link;
};
setAdminLink = (link) => {
this.adminLink = link;
};
getPortalInviteLinks = async () => {
const isViewerAdmin = this.peopleStore.authStore.isAdmin;
if (!isViewerAdmin) return Promise.resolve();
const links = await getInvitationLinks();
this.setUserLink(links.userLink);
this.setGuestLink(links.guestLink);
this.setAdminLink(links.adminLink);
};
getShortenedLink = async (link, forUser = false) => {

View File

@ -402,6 +402,12 @@ class UsersStore {
return list;
}
inviteUsers = async (data) => {
const result = await api.people.inviteUsers(data);
return Promise.resolve(result);
};
}
export default UsersStore;

View File

@ -195,6 +195,18 @@ export function getUserById(userId) {
});
}
export const inviteUsers = async (data) => {
const options = {
method: "post",
url: "/people/invite",
data,
};
const res = await request(options);
return res;
};
export function resendUserInvites(userIds) {
return request({
method: "put",

View File

@ -13,7 +13,7 @@ const USER_INVITE_LINK = "userInvitationLink";
const INVITE_LINK_TTL = "localStorageLinkTtl";
const LINKS_TTL = 6 * 3600 * 1000;
export function getInvitationLink(isGuest) {
export function getInvitationLink(type) {
const curLinksTtl = localStorage.getItem(INVITE_LINK_TTL);
const now = +new Date();
@ -26,30 +26,40 @@ export function getInvitationLink(isGuest) {
}
const link = localStorage.getItem(
isGuest ? GUEST_INVITE_LINK : USER_INVITE_LINK
type === 2 ? GUEST_INVITE_LINK : USER_INVITE_LINK
);
return link
return link && type !== 3
? Promise.resolve(link)
: request({
method: "get",
url: `/portal/users/invite/${isGuest ? 2 : 1}.json`,
url: `/portal/users/invite/${type}.json`,
}).then((link) => {
localStorage.setItem(
isGuest ? GUEST_INVITE_LINK : USER_INVITE_LINK,
link
);
if (type !== 3) {
localStorage.setItem(
type === 2 ? GUEST_INVITE_LINK : USER_INVITE_LINK,
link
);
}
return Promise.resolve(link);
});
}
export function getInvitationLinks() {
const isGuest = true;
return Promise.all([getInvitationLink(), getInvitationLink(isGuest)]).then(
([userInvitationLinkResp, guestInvitationLinkResp]) => {
return Promise.all([
getInvitationLink(1),
getInvitationLink(2),
getInvitationLink(3),
]).then(
([
userInvitationLinkResp,
guestInvitationLinkResp,
adminInvitationLinkResp,
]) => {
return Promise.resolve({
userLink: userInvitationLinkResp,
guestLink: guestInvitationLinkResp,
adminLink: adminInvitationLinkResp,
});
}
);
@ -266,4 +276,5 @@ export function sendPaymentRequest(email, userName, message) {
userName,
message,
},
})};
});
}

View File

@ -245,10 +245,10 @@ export function removeLogoFromRoom(id) {
});
}
export const setInvitationLinks = async (id, linkId, title, access) => {
export const setInvitationLinks = async (roomId, linkId, title, access) => {
const options = {
method: "put",
url: `/files/rooms/${id}/links`,
url: `/files/rooms/${roomId}/links`,
data: {
linkId,
title,

View File

@ -23,6 +23,8 @@ const StyledArticle = styled.article`
//padding: 0 20px;
border-right: ${(props) => props.theme.catalog.verticalLine};
@media ${tablet} {
min-width: ${(props) => (props.showText ? "243px" : "60px")};
max-width: ${(props) => (props.showText ? "243px" : "60px")};
@ -63,6 +65,8 @@ const StyledArticle = styled.article`
padding: 0;
top: ${(props) => (props.isBannerVisible ? "-16px" : "64px")} !important;
height: calc(100% - 64px) !important;
border-right: none;
`}
z-index: ${(props) =>
@ -77,7 +81,7 @@ const StyledArticle = styled.article`
.scroll-body {
overflow-x: hidden !important;
height: calc(100% - 200px);
padding: 0 20px;
padding: 0 20px !important;
@media ${tablet} {
height: calc(100% - 150px);
@ -278,16 +282,19 @@ const StyledArticleProfile = styled.div`
justify-content: center;
border-top: ${(props) => props.theme.catalog.profile.borderTop};
border-right: ${(props) => props.theme.catalog.verticalLine}
background-color: ${(props) => props.theme.catalog.profile.background};
@media ${tablet} {
padding: 16px 14px;
}
${isTablet &&
css`
padding: 16px 14px;
`}
${
isTablet &&
css`
padding: 16px 14px;
`
}
.profile-avatar {
cursor: pointer;

View File

@ -27,6 +27,7 @@ export const EmployeeStatus = Object.freeze({
export const EmployeeType = Object.freeze({
User: 1,
Guest: 2,
Admin: 3,
UserString: "user",
RoomAdmin: "manager",
DocSpaceAdmin: "admin",

View File

@ -21,6 +21,10 @@ const AccessRightSelect = ({
}) => {
const [currentItem, setCurrentItem] = useState(selectedOption);
useEffect(() => {
setCurrentItem(selectedOption);
}, [selectedOption]);
const onSelectCurrentItem = useCallback(
(e) => {
const key = e.currentTarget.dataset.key;

View File

@ -1858,6 +1858,8 @@ const Base = {
headerBurgerColor: "#657077",
verticalLine: "1px solid #eceef1",
profile: {
borderTop: "1px solid #eceef1",
background: "#f3f4f4",

View File

@ -1854,6 +1854,8 @@ const Dark = {
headerBurgerColor: "#606060",
verticalLine: "1px solid #474747",
profile: {
borderTop: "1px solid #474747",
background: "#3D3D3D",

View File

@ -3138,7 +3138,7 @@ public class FileStorageService<T> //: IFileStorageService
continue;
}
var link = _roomLinkService.GetInvitationLink(user.Email, _authContext.CurrentAccount.ID);
var link = _roomLinkService.GetInvitationLink(user.Email, share.Access, _authContext.CurrentAccount.ID);
_studioNotifyService.SendEmailRoomInvite(user.Email, link);
}
}

View File

@ -47,23 +47,32 @@ public class RoomLinkService
return _commonLinkUtility.GetConfirmationUrl(key, ConfirmType.LinkInvite, createdBy);
}
public string GetInvitationLink(string email, Guid createdBy)
public string GetInvitationLink(string email, FileShare share, Guid createdBy)
{
var link = _commonLinkUtility.GetConfirmationEmailUrl(email, ConfirmType.LinkInvite, EmployeeType.RoomAdmin, createdBy)
+ $"&emplType={EmployeeType.RoomAdmin:d}";
var type = DocSpaceHelper.PaidRights.Contains(share) ? EmployeeType.RoomAdmin : EmployeeType.User;
var link = _commonLinkUtility.GetConfirmationEmailUrl(email, ConfirmType.LinkInvite, type, createdBy)
+ $"&emplType={type:d}";
return link;
}
public string GetInvitationLink(string email, EmployeeType employeeType, Guid createdBy)
{
var link = _commonLinkUtility.GetConfirmationEmailUrl(email, ConfirmType.LinkInvite, employeeType, createdBy)
+ $"&emplType={employeeType:d}";
return link;
}
public async Task<LinkOptions> GetOptionsAsync(string key, string email)
{
var options = new LinkOptions();
return await GetOptionsAsync(key, email, EmployeeType.All);
}
if (string.IsNullOrEmpty(key))
{
options.Type = LinkType.DefaultInvintation;
options.IsCorrect = true;
}
public async Task<LinkOptions> GetOptionsAsync(string key, string email, EmployeeType employeeType)
{
var options = new LinkOptions();
var payload = _docSpaceLinksHelper.Parse(key);
@ -74,16 +83,24 @@ public class RoomLinkService
if (record != null)
{
options.IsCorrect = true;
options.Type = LinkType.InvintationToRoom;
options.LinkType = LinkType.InvintationToRoom;
options.RoomId = record.EntryId.ToString();
options.Share = record.Share;
options.Id = record.Subject;
options.EmployeeType = DocSpaceHelper.PaidRights.Contains(record.Share) ? EmployeeType.RoomAdmin : EmployeeType.User;
}
}
else if (_docSpaceLinksHelper.Validate(key, email) == EmailValidationKeyProvider.ValidationResult.Ok)
else if (_docSpaceLinksHelper.ValidateEmailLink(email, key, employeeType) == EmailValidationKeyProvider.ValidationResult.Ok)
{
options.IsCorrect = true;
options.Type = LinkType.InvintationByEmail;
options.LinkType = LinkType.InvintationByEmail;
options.EmployeeType = employeeType;
}
else if (_docSpaceLinksHelper.ValidateExtarnalLink(key, employeeType) == EmailValidationKeyProvider.ValidationResult.Ok)
{
options.LinkType = LinkType.DefaultInvintation;
options.IsCorrect = true;
options.EmployeeType = employeeType;
}
return options;
@ -105,7 +122,8 @@ public class LinkOptions
public Guid Id { get; set; }
public string RoomId { get; set; }
public FileShare Share { get; set; }
public LinkType Type { get; set; }
public LinkType LinkType { get; set; }
public EmployeeType EmployeeType { get; set; }
public bool IsCorrect { get; set; }
}

View File

@ -28,6 +28,8 @@ namespace ASC.Files.Core.Helpers;
public static class DocSpaceHelper
{
public static HashSet<FileShare> PaidRights { get; } = new HashSet<FileShare> { FileShare.RoomAdmin };
private static readonly HashSet<FileShare> _fillingFormRoomConstraints
= new HashSet<FileShare> { FileShare.RoomAdmin, FileShare.FillForms, FileShare.Read };
private static readonly HashSet<FileShare> _collaborationRoomConstraints

View File

@ -40,11 +40,11 @@ public class FileSharingAceHelper<T>
private readonly GlobalFolderHelper _globalFolderHelper;
private readonly FileSharingHelper _fileSharingHelper;
private readonly FileTrackerHelper _fileTracker;
private readonly FileSecurityCommon _fileSecurityCommon;
private readonly FilesSettingsHelper _filesSettingsHelper;
private readonly RoomLinkService _roomLinkService;
private readonly StudioNotifyService _studioNotifyService;
private readonly UsersInRoomChecker _usersInRoomChecker;
private readonly UserManagerWrapper _userManagerWrapper;
private readonly ILogger _logger;
public FileSharingAceHelper(
@ -59,12 +59,12 @@ public class FileSharingAceHelper<T>
GlobalFolderHelper globalFolderHelper,
FileSharingHelper fileSharingHelper,
FileTrackerHelper fileTracker,
FileSecurityCommon fileSecurityCommon,
FilesSettingsHelper filesSettingsHelper,
RoomLinkService roomLinkService,
StudioNotifyService studioNotifyService,
ILoggerProvider loggerProvider,
UsersInRoomChecker usersInRoomChecker)
UsersInRoomChecker usersInRoomChecker,
UserManagerWrapper userManagerWrapper)
{
_fileSecurity = fileSecurity;
_coreBaseSettings = coreBaseSettings;
@ -78,11 +78,11 @@ public class FileSharingAceHelper<T>
_fileSharingHelper = fileSharingHelper;
_fileTracker = fileTracker;
_filesSettingsHelper = filesSettingsHelper;
_fileSecurityCommon = fileSecurityCommon;
_roomLinkService = roomLinkService;
_studioNotifyService = studioNotifyService;
_usersInRoomChecker = usersInRoomChecker;
_logger = loggerProvider.CreateLogger("ASC.Files");
_userManagerWrapper = userManagerWrapper;
}
public async Task<bool> SetAceObjectAsync(List<AceWrapper> aceWrappers, FileEntry<T> entry, bool notify, string message, AceAdvancedSettingsWrapper advancedSettings)
@ -117,7 +117,7 @@ public class FileSharingAceHelper<T>
continue;
}
if (!ProcessEmailAce(w))
if (!await ProcessEmailAceAsync(w))
{
continue;
}
@ -159,7 +159,7 @@ public class FileSharingAceHelper<T>
if (!string.IsNullOrEmpty(w.Email))
{
var link = _roomLinkService.GetInvitationLink(w.Email, _authContext.CurrentAccount.ID);
var link = _roomLinkService.GetInvitationLink(w.Email, share, _authContext.CurrentAccount.ID);
_studioNotifyService.SendEmailRoomInvite(w.Email, link);
_logger.Debug(link);
}
@ -271,30 +271,25 @@ public class FileSharingAceHelper<T>
await _fileMarker.RemoveMarkAsNewAsync(entry);
}
private bool ProcessEmailAce(AceWrapper ace)
private async Task<bool> ProcessEmailAceAsync(AceWrapper ace)
{
if (string.IsNullOrEmpty(ace.Email))
{
return true;
}
if (!MailAddress.TryCreate(ace.Email, out var email) || _userManager.GetUserByEmail(ace.Email) != Constants.LostUser)
var type = DocSpaceHelper.PaidRights.Contains(ace.Access) ? EmployeeType.RoomAdmin : EmployeeType.User;
UserInfo user = null;
try
{
user = await _userManagerWrapper.AddInvitedUserAsync(ace.Email, type);
}
catch
{
return false;
}
var userInfo = new UserInfo
{
Email = email.Address,
UserName = email.User,
LastName = string.Empty,
FirstName = string.Empty,
ActivationStatus = EmployeeActivationStatus.Pending,
Status = EmployeeStatus.Active
};
var user = _userManager.SaveUserInfo(userInfo);
ace.Id = user.Id;
return true;

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using ASC.Common.Log;
namespace ASC.People.Api;
public class UserController : PeopleControllerBase
@ -203,16 +205,18 @@ public class UserController : PeopleControllerBase
_permissionContext.DemandPermissions(Constants.Action_AddRemoveUser);
var options = inDto.FromInviteLink ? await _roomLinkService.GetOptionsAsync(inDto.Key, inDto.Email) : null;
var options = inDto.FromInviteLink ? await _roomLinkService.GetOptionsAsync(inDto.Key, inDto.Email, inDto.Type) : null;
if (options != null && !options.IsCorrect)
{
throw new SecurityException(FilesCommonResource.ErrorMessage_InvintationLink);
}
inDto.Type = options != null ? options.EmployeeType : inDto.Type;
var user = new UserInfo();
var byEmail = options != null && options.Type == LinkType.InvintationByEmail;
var byEmail = options?.LinkType == LinkType.InvintationByEmail;
if (byEmail)
{
@ -259,7 +263,7 @@ public class UserController : PeopleControllerBase
UpdateContacts(inDto.Contacts, user);
_cache.Insert("REWRITE_URL" + _tenantManager.GetCurrentTenant().Id, HttpContext.Request.GetUrlRewriter().ToString(), TimeSpan.FromMinutes(5));
user = _userManagerWrapper.AddUser(user, inDto.PasswordHash, inDto.FromInviteLink, true, inDto.IsUser, inDto.FromInviteLink, true, true, byEmail);
user = _userManagerWrapper.AddUser(user, inDto.PasswordHash, inDto.FromInviteLink, true, inDto.Type == EmployeeType.User, inDto.FromInviteLink, true, true, byEmail, inDto.Type == EmployeeType.DocSpaceAdmin);
UpdateDepartments(inDto.Department, user);
@ -268,19 +272,19 @@ public class UserController : PeopleControllerBase
await UpdatePhotoUrl(inDto.Files, user);
}
if (options != null && options.Type == LinkType.InvintationToRoom)
if (options != null && options.LinkType == LinkType.InvintationToRoom)
{
var success = int.TryParse(options.RoomId, out var id);
if (success)
{
await _usersInRoomChecker.CheckAppend();
await _fileSecurity.ShareAsync(id, Files.Core.FileEntryType.Folder, user.Id, options.Share);
await _fileSecurity.ShareAsync(id, FileEntryType.Folder, user.Id, options.Share);
}
else
{
await _usersInRoomChecker.CheckAppend();
await _fileSecurity.ShareAsync(options.RoomId, Files.Core.FileEntryType.Folder, user.Id, options.Share);
await _fileSecurity.ShareAsync(options.RoomId, FileEntryType.Folder, user.Id, options.Share);
}
}
@ -290,6 +294,26 @@ public class UserController : PeopleControllerBase
return await _employeeFullDtoHelper.GetFull(user);
}
[HttpPost("invite")]
public async IAsyncEnumerable<EmployeeDto> InviteUsersAsync(InviteUsersRequestDto inDto)
{
foreach (var invite in inDto.Invitations)
{
var user = await _userManagerWrapper.AddInvitedUserAsync(invite.Email, invite.Type);
var link = _roomLinkService.GetInvitationLink(user.Email, invite.Type, _authContext.CurrentAccount.ID);
_studioNotifyService.SendDocSpaceInvite(user.Email, link);
_logger.Debug(link);
}
var users = _userManager.GetUsers().Where(u => u.ActivationStatus == EmployeeActivationStatus.Pending);
foreach (var user in users)
{
yield return await _employeeDtoHelper.Get(user);
}
}
[HttpPut("{userid}/password")]
[Authorize(AuthenticationSchemes = "confirm", Roles = "PasswordChange,EmailChange,Activation,EmailActivation,Everyone")]
public async Task<EmployeeFullDto> ChangeUserPassword(Guid userid, MemberRequestDto inDto)
@ -672,14 +696,10 @@ public class UserController : PeopleControllerBase
if (user.ActivationStatus == EmployeeActivationStatus.Pending)
{
if (_userManager.IsUser(user))
{
_studioNotifyService.GuestInfoActivation(user);
}
else
{
_studioNotifyService.UserInfoActivation(user);
}
var type = _userManager.IsDocSpaceAdmin(user) ? EmployeeType.DocSpaceAdmin :
_userManager.IsUser(user) ? EmployeeType.User : EmployeeType.RoomAdmin;
_studioNotifyService.SendDocSpaceInvite(user.Email, _roomLinkService.GetInvitationLink(user.Email, type, _authContext.CurrentAccount.ID));
}
else
{

View File

@ -0,0 +1,38 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.People.ApiModels.RequestDto;
public class InviteUsersRequestDto
{
public IEnumerable<UserInvitation> Invitations { get; set; }
}
public class UserInvitation
{
public string Email { get; set; }
public EmployeeType Type { get; set; }
}

View File

@ -28,6 +28,7 @@ namespace ASC.People.ApiModels.RequestDto;
public class MemberRequestDto
{
public EmployeeType Type { get; set; }
public bool IsUser { get; set; }
public string Email { get; set; }
public string Firstname { get; set; }

View File

@ -163,5 +163,6 @@ public static class Actions
public static readonly INotifyAction StorageDecryptionSuccess = new NotifyAction("storage_decryption_success");
public static readonly INotifyAction StorageDecryptionError = new NotifyAction("storage_decryption_error");
public static readonly INotifyAction RoomInvite = new NotifyAction("room_invite");
public static readonly INotifyAction RoomInvite = new NotifyAction("room_invite", "room_invite");
public static readonly INotifyAction DocSpaceInvite = new NotifyAction("docspace_invite", "docspace_invite");
}

View File

@ -237,6 +237,18 @@ public class StudioNotifyService
TagValues.GreenButton(greenButtonText, confirmationUrl));
}
public void SendDocSpaceInvite(string email, string confirmationUrl)
{
static string greenButtonText() => WebstudioNotifyPatternResource.ButtonConfirmDocSpaceInvite;
_client.SendNoticeToAsync(
Actions.DocSpaceInvite,
_studioNotifyHelper.RecipientFromEmail(email, false),
new[] { EMailSenderName },
new TagValue(Tags.InviteLink, confirmationUrl),
TagValues.GreenButton(greenButtonText, confirmationUrl));
}
#endregion
#region MailServer

View File

@ -303,6 +303,15 @@ namespace ASC.Web.Core.PublicResources {
}
}
/// <summary>
/// Looks up a localized string similar to Confirm DocSpace Invite.
/// </summary>
public static string ButtonConfirmDocSpaceInvite {
get {
return ResourceManager.GetString("ButtonConfirmDocSpaceInvite", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Confirm Portal Address Change.
/// </summary>

View File

@ -2121,4 +2121,7 @@ $Body</value>
<data name="subject_for_sales_notify" xml:space="preserve">
<value>Sales department request</value>
</data>
<data name="ButtonConfirmDocSpaceInvite" xml:space="preserve">
<value>Confirm DocSpace Invite</value>
</data>
</root>

View File

@ -48,6 +48,7 @@ public sealed class UserManagerWrapper
private readonly DisplayUserSettingsHelper _displayUserSettingsHelper;
private readonly SettingsManager _settingsManager;
private readonly UserFormatter _userFormatter;
private readonly CountRoomAdminChecker _countManagerChecker;
public UserManagerWrapper(
StudioNotifyService studioNotifyService,
@ -60,7 +61,8 @@ public sealed class UserManagerWrapper
IPSecurity.IPSecurity iPSecurity,
DisplayUserSettingsHelper displayUserSettingsHelper,
SettingsManager settingsManager,
UserFormatter userFormatter)
UserFormatter userFormatter,
CountRoomAdminChecker countManagerChecker)
{
_studioNotifyService = studioNotifyService;
_userManager = userManager;
@ -73,6 +75,7 @@ public sealed class UserManagerWrapper
_displayUserSettingsHelper = displayUserSettingsHelper;
_settingsManager = settingsManager;
_userFormatter = userFormatter;
_countManagerChecker = countManagerChecker;
}
private bool TestUniqueUserName(string uniqueName)
@ -108,8 +111,49 @@ public sealed class UserManagerWrapper
return Equals(foundUser, Constants.LostUser) || foundUser.Id == userId;
}
public async Task<UserInfo> AddInvitedUserAsync(string email, EmployeeType type)
{
var mail = new MailAddress(email);
if (_userManager.GetUserByEmail(mail.Address).Id != Constants.LostUser.Id)
{
throw new InvalidOperationException($"User with email {mail.Address} already exists or is invited");
}
if (type is EmployeeType.RoomAdmin or EmployeeType.DocSpaceAdmin)
{
await _countManagerChecker.CheckAppend();
}
var user = new UserInfo
{
Email = mail.Address,
UserName = mail.User,
LastName = string.Empty,
FirstName = string.Empty,
ActivationStatus = EmployeeActivationStatus.Pending,
Status = EmployeeStatus.Active,
};
var newUser = _userManager.SaveUserInfo(user);
var groupId = type switch
{
EmployeeType.User => Constants.GroupUser.ID,
EmployeeType.DocSpaceAdmin => Constants.GroupAdmin.ID,
_ => Guid.Empty,
};
if (groupId != Guid.Empty)
{
_userManager.AddUserIntoGroup(newUser.Id, groupId);
}
return newUser;
}
public UserInfo AddUser(UserInfo userInfo, string passwordHash, bool afterInvite = false, bool notify = true, bool isUser = false, bool fromInviteLink = false, bool makeUniqueName = true, bool isCardDav = false,
bool updateExising = false)
bool updateExising = false, bool isAdmin = false)
{
ArgumentNullException.ThrowIfNull(userInfo);
@ -184,6 +228,10 @@ public sealed class UserManagerWrapper
{
_userManager.AddUserIntoGroup(newUserInfo.Id, Constants.GroupUser.ID);
}
else if (isAdmin)
{
_userManager.AddUserIntoGroup(newUserInfo.Id, Constants.GroupAdmin.ID);
}
return newUserInfo;
}