diff --git a/build/install/docker/.dockerignore b/build/install/docker/.dockerignore new file mode 100644 index 0000000000..b870ce67d4 --- /dev/null +++ b/build/install/docker/.dockerignore @@ -0,0 +1,11 @@ +node_modules +bin +.yarn +.git +.vscode +.github +Logs +Data +TestsResults +i18next +*.bat \ No newline at end of file diff --git a/build/install/docker/Dockerfile.dev b/build/install/docker/Dockerfile.dev index f5fe882e1c..e0a0f3db08 100644 --- a/build/install/docker/Dockerfile.dev +++ b/build/install/docker/Dockerfile.dev @@ -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} diff --git a/build/start/stop.backend.docker.sh b/build/start/stop.backend.docker.sh index ea730023ba..ebc2e665ba 100755 --- a/build/start/stop.backend.docker.sh +++ b/build/start/stop.backend.docker.sh @@ -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}') \ No newline at end of file +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}') \ No newline at end of file diff --git a/common/ASC.Api.Core/Model/EmployeeFullDto.cs b/common/ASC.Api.Core/Model/EmployeeFullDto.cs index ab929e97e5..7117d94077 100644 --- a/common/ASC.Api.Core/Model/EmployeeFullDto.cs +++ b/common/ASC.Api.Core/Model/EmployeeFullDto.cs @@ -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 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 { "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() diff --git a/common/ASC.Api.Core/Security/DocSpaceLinksHelper.cs b/common/ASC.Api.Core/Security/DocSpaceLinksHelper.cs index 6b696e28d7..abab5040b3 100644 --- a/common/ASC.Api.Core/Security/DocSpaceLinksHelper.cs +++ b/common/ASC.Api.Core/Security/DocSpaceLinksHelper.cs @@ -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(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) diff --git a/common/ASC.Api.Core/Security/EmailValidationKeyModelHelper.cs b/common/ASC.Api.Core/Security/EmailValidationKeyModelHelper.cs index 21f3231af7..a8c101a89f 100644 --- a/common/ASC.Api.Core/Security/EmailValidationKeyModelHelper.cs +++ b/common/ASC.Api.Core/Security/EmailValidationKeyModelHelper.cs @@ -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; diff --git a/common/ASC.Core.Common/Caching/CachedUserService.cs b/common/ASC.Core.Common/Caching/CachedUserService.cs index c452d55a0c..a05012cae3 100644 --- a/common/ASC.Core.Common/Caching/CachedUserService.cs +++ b/common/ASC.Core.Common/Caching/CachedUserService.cs @@ -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); } } diff --git a/common/ASC.Core.Common/Core/EmployeeType.cs b/common/ASC.Core.Common/Core/EmployeeType.cs index c5242364d0..17d3372e53 100644 --- a/common/ASC.Core.Common/Core/EmployeeType.cs +++ b/common/ASC.Core.Common/Core/EmployeeType.cs @@ -36,5 +36,6 @@ public enum EmployeeType { All = 0, RoomAdmin = 1, - User = 2 + User = 2, + DocSpaceAdmin = 3, } diff --git a/common/ASC.Core.Common/Users/DisplayUserSettings.cs b/common/ASC.Core.Common/Users/DisplayUserSettings.cs index 5d1d1b49d8..dc290117c8 100644 --- a/common/ASC.Core.Common/Users/DisplayUserSettings.cs +++ b/common/ASC.Core.Common/Users/DisplayUserSettings.cs @@ -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) diff --git a/packages/client/public/locales/en/InviteDialog.json b/packages/client/public/locales/en/InviteDialog.json index 11c6a863ca..21fc962c4e 100644 --- a/packages/client/public/locales/en/InviteDialog.json +++ b/packages/client/public/locales/en/InviteDialog.json @@ -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" } diff --git a/packages/client/public/locales/en/Translations.json b/packages/client/public/locales/en/Translations.json index 275449ae26..f9c7326934 100644 --- a/packages/client/public/locales/en/Translations.json +++ b/packages/client/public/locales/en/Translations.json @@ -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", diff --git a/packages/client/src/components/Article/Body/Items.js b/packages/client/src/components/Article/Body/Items.js index e2ca75e4ec..b2d8512150 100644 --- a/packages/client/src/components/Article/Body/Items.js +++ b/packages/client/src/components/Article/Body/Items.js @@ -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))); diff --git a/packages/client/src/components/Article/MainButton/index.js b/packages/client/src/components/Article/MainButton/index.js index 3aa236cec2..1c4550b8ea 100644 --- a/packages/client/src/components/Article/MainButton/index.js +++ b/packages/client/src/components/Article/MainButton/index.js @@ -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, diff --git a/packages/client/src/components/EmptyContainer/RootFolderContainer.js b/packages/client/src/components/EmptyContainer/RootFolderContainer.js index d05366a891..6b3a08bc36 100644 --- a/packages/client/src/components/EmptyContainer/RootFolderContainer.js +++ b/packages/client/src/components/EmptyContainer/RootFolderContainer.js @@ -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"); diff --git a/packages/client/src/components/panels/InvitePanel/index.js b/packages/client/src/components/panels/InvitePanel/index.js index 59f052f712..dc634bfef5 100644 --- a/packages/client/src/components/panels/InvitePanel/index.js +++ b/packages/client/src/components/panels/InvitePanel/index.js @@ -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 ( @@ -127,7 +181,9 @@ const InvitePanel = ({ withoutBodyScroll > - {t("InviteUsersToRoom")} + + {roomId === -1 ? t("InviteUsers") : t("InviteUsersToRoom")} + @@ -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([ diff --git a/packages/client/src/components/panels/InvitePanel/sub-components/AccessSelector.js b/packages/client/src/components/panels/InvitePanel/sub-components/AccessSelector.js index a94be93153..95b0009795 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/AccessSelector.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/AccessSelector.js @@ -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 ( diff --git a/packages/client/src/components/panels/InvitePanel/sub-components/ExternalLinks.js b/packages/client/src/components/panels/InvitePanel/sub-components/ExternalLinks.js index 3a5b9cc16b..5ddcab63ff 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/ExternalLinks.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/ExternalLinks.js @@ -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 ( - - - copyLink(link.shareLink)} - hoverColor="#333333" - iconColor="#A3A9AE" - /> - - - {!hideSelector && ( - - )} - - ); - }); - return ( @@ -156,17 +146,41 @@ const ExternalLinks = ({ )} - {linksVisible && links} + {linksVisible && ( + + + copyLink(activeLink.shareLink)} + hoverColor="#333333" + iconColor="#A3A9AE" + /> + + + + )} ); }; 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)); diff --git a/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js b/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js index c09409391d..65aba4451a 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js @@ -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 = ({ <> {t("IndividualInvitation")} - - {t("СhooseFromList")} - + {!hideSelector && ( + + {t("СhooseFromList")} + + )} @@ -258,17 +265,15 @@ const InviteInput = ({ )} - {!hideSelector && ( - - )} + - {addUsersPanelVisible && ( + {!hideSelector && addUsersPanelVisible && ( option.access === access); + const defaultAccess = accesses.find((option) => option.access === +access); const errorsInList = () => { const hasErrors = inviteItems.some((item) => !!item.errors?.length); diff --git a/packages/client/src/components/panels/InvitePanel/utils/index.js b/packages/client/src/components/panels/InvitePanel/utils/index.js index 7a3fe91d9a..d9f7011b47 100644 --- a/packages/client/src/components/panels/InvitePanel/utils/index.js +++ b/packages/client/src/components/panels/InvitePanel/utils/index.js @@ -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 = [ diff --git a/packages/client/src/pages/AccountsHome/Section/Body/Dialogs.js b/packages/client/src/pages/AccountsHome/Section/Body/Dialogs.js index c60f8ca0ed..93b454fd8d 100644 --- a/packages/client/src/pages/AccountsHome/Section/Body/Dialogs.js +++ b/packages/client/src/pages/AccountsHome/Section/Body/Dialogs.js @@ -77,7 +77,6 @@ const Dialogs = ({ {...data} /> )} - {changeUserStatusDialogVisible && ( )} - {sendInviteDialogVisible && ( )} - {deleteDialogVisible && ( { 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([ diff --git a/packages/client/src/pages/Confirm/sub-components/createUser.js b/packages/client/src/pages/Confirm/sub-components/createUser.js index 1de780cca8..348a131087 100644 --- a/packages/client/src/pages/Confirm/sub-components/createUser.js +++ b/packages/client/src/pages/Confirm/sub-components/createUser.js @@ -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} diff --git a/packages/client/src/pages/Files.jsx b/packages/client/src/pages/Files.jsx index 25adea1bf1..ce741cb4f1 100644 --- a/packages/client/src/pages/Files.jsx +++ b/packages/client/src/pages/Files.jsx @@ -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", diff --git a/packages/client/src/pages/FormGallery/TilesView/StyledTileView.js b/packages/client/src/pages/FormGallery/TilesView/StyledTileView.js index 38c8417646..46478bfceb 100644 --- a/packages/client/src/pages/FormGallery/TilesView/StyledTileView.js +++ b/packages/client/src/pages/FormGallery/TilesView/StyledTileView.js @@ -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`}; `; diff --git a/packages/client/src/pages/FormGallery/TilesView/sub-components/InfiniteGrid.js b/packages/client/src/pages/FormGallery/TilesView/sub-components/InfiniteGrid.js index 3e149ea9cd..cf9407ce27 100644 --- a/packages/client/src/pages/FormGallery/TilesView/sub-components/InfiniteGrid.js +++ b/packages/client/src/pages/FormGallery/TilesView/sub-components/InfiniteGrid.js @@ -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 ( - + {children} ); @@ -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({child}); + cards.push( + + {child} + + ); } }); diff --git a/packages/client/src/pages/Home/InfoPanel/Body/index.js b/packages/client/src/pages/Home/InfoPanel/Body/index.js index 1d3061e7d7..c70e6a2732 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/index.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/index.js @@ -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, diff --git a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/index.js b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/index.js index 65732cc4ff..652c022691 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/index.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/index.js @@ -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 ; diff --git a/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/InfiniteGrid.js b/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/InfiniteGrid.js index 36b76a01ff..5a87ced076 100644 --- a/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/InfiniteGrid.js +++ b/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/InfiniteGrid.js @@ -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 ( - + {children} ); @@ -143,7 +148,11 @@ const InfiniteGrid = (props) => { } const cardKey = uniqueid("card-item_"); - cards.push({child}); + cards.push( + + {child} + + ); } } }); diff --git a/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/StyledInfiniteGrid.js b/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/StyledInfiniteGrid.js index 2976567cd4..1b0a5e2b95 100644 --- a/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/StyledInfiniteGrid.js +++ b/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/StyledInfiniteGrid.js @@ -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`}; `; diff --git a/packages/client/src/pages/Home/index.js b/packages/client/src/pages/Home/index.js index c1d823183c..c31e0c1a10 100644 --- a/packages/client/src/pages/Home/index.js +++ b/packages/client/src/pages/Home/index.js @@ -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); diff --git a/packages/client/src/store/ContextOptionsStore.js b/packages/client/src/store/ContextOptionsStore.js index 11749cf743..14a1e55196 100644 --- a/packages/client/src/store/ContextOptionsStore.js +++ b/packages/client/src/store/ContextOptionsStore.js @@ -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({ diff --git a/packages/client/src/store/FilesStore.js b/packages/client/src/store/FilesStore.js index c94ddd6fa8..157d36ece0 100644 --- a/packages/client/src/store/FilesStore.js +++ b/packages/client/src/store/FilesStore.js @@ -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) => { diff --git a/packages/client/src/store/InviteLinksStore.js b/packages/client/src/store/InviteLinksStore.js index 10351f80fe..7c59ffa62e 100644 --- a/packages/client/src/store/InviteLinksStore.js +++ b/packages/client/src/store/InviteLinksStore.js @@ -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) => { diff --git a/packages/client/src/store/UsersStore.js b/packages/client/src/store/UsersStore.js index f40c9d2b11..2bece3638a 100644 --- a/packages/client/src/store/UsersStore.js +++ b/packages/client/src/store/UsersStore.js @@ -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; diff --git a/packages/common/api/people/index.js b/packages/common/api/people/index.js index 7a11091627..49b0ddcd98 100644 --- a/packages/common/api/people/index.js +++ b/packages/common/api/people/index.js @@ -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", diff --git a/packages/common/api/portal/index.js b/packages/common/api/portal/index.js index e67f570182..16513400a1 100644 --- a/packages/common/api/portal/index.js +++ b/packages/common/api/portal/index.js @@ -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, }, - })}; + }); +} diff --git a/packages/common/api/rooms/index.js b/packages/common/api/rooms/index.js index a01f7cbd71..9895349d5d 100644 --- a/packages/common/api/rooms/index.js +++ b/packages/common/api/rooms/index.js @@ -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, diff --git a/packages/common/components/Article/styled-article.js b/packages/common/components/Article/styled-article.js index cb64484349..cbc107c283 100644 --- a/packages/common/components/Article/styled-article.js +++ b/packages/common/components/Article/styled-article.js @@ -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; diff --git a/packages/common/constants/index.js b/packages/common/constants/index.js index 3533c39a67..23ed5dd567 100644 --- a/packages/common/constants/index.js +++ b/packages/common/constants/index.js @@ -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", diff --git a/packages/components/access-right-select/index.js b/packages/components/access-right-select/index.js index 4a3127440b..a6a685ba81 100644 --- a/packages/components/access-right-select/index.js +++ b/packages/components/access-right-select/index.js @@ -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; diff --git a/packages/components/themes/base.js b/packages/components/themes/base.js index bbf7da5920..7e86b2e564 100644 --- a/packages/components/themes/base.js +++ b/packages/components/themes/base.js @@ -1858,6 +1858,8 @@ const Base = { headerBurgerColor: "#657077", + verticalLine: "1px solid #eceef1", + profile: { borderTop: "1px solid #eceef1", background: "#f3f4f4", diff --git a/packages/components/themes/dark.js b/packages/components/themes/dark.js index 48589a5172..2026a0a0ca 100644 --- a/packages/components/themes/dark.js +++ b/packages/components/themes/dark.js @@ -1854,6 +1854,8 @@ const Dark = { headerBurgerColor: "#606060", + verticalLine: "1px solid #474747", + profile: { borderTop: "1px solid #474747", background: "#3D3D3D", diff --git a/products/ASC.Files/Core/Core/FileStorageService.cs b/products/ASC.Files/Core/Core/FileStorageService.cs index 11ebf08939..11014fd00a 100644 --- a/products/ASC.Files/Core/Core/FileStorageService.cs +++ b/products/ASC.Files/Core/Core/FileStorageService.cs @@ -3138,7 +3138,7 @@ public class FileStorageService //: 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); } } diff --git a/products/ASC.Files/Core/Core/VirtualRooms/RoomLinkService.cs b/products/ASC.Files/Core/Core/VirtualRooms/RoomLinkService.cs index f263f79504..8e016f283b 100644 --- a/products/ASC.Files/Core/Core/VirtualRooms/RoomLinkService.cs +++ b/products/ASC.Files/Core/Core/VirtualRooms/RoomLinkService.cs @@ -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 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 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; } } diff --git a/products/ASC.Files/Core/Helpers/DocSpaceHelper.cs b/products/ASC.Files/Core/Helpers/DocSpaceHelper.cs index 6369f1ae8a..62c148bea4 100644 --- a/products/ASC.Files/Core/Helpers/DocSpaceHelper.cs +++ b/products/ASC.Files/Core/Helpers/DocSpaceHelper.cs @@ -28,6 +28,8 @@ namespace ASC.Files.Core.Helpers; public static class DocSpaceHelper { + public static HashSet PaidRights { get; } = new HashSet { FileShare.RoomAdmin }; + private static readonly HashSet _fillingFormRoomConstraints = new HashSet { FileShare.RoomAdmin, FileShare.FillForms, FileShare.Read }; private static readonly HashSet _collaborationRoomConstraints diff --git a/products/ASC.Files/Core/Utils/FileSharing.cs b/products/ASC.Files/Core/Utils/FileSharing.cs index 5eddba3279..fe9bfff418 100644 --- a/products/ASC.Files/Core/Utils/FileSharing.cs +++ b/products/ASC.Files/Core/Utils/FileSharing.cs @@ -40,11 +40,11 @@ public class FileSharingAceHelper 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 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 _fileSharingHelper = fileSharingHelper; _fileTracker = fileTracker; _filesSettingsHelper = filesSettingsHelper; - _fileSecurityCommon = fileSecurityCommon; _roomLinkService = roomLinkService; _studioNotifyService = studioNotifyService; _usersInRoomChecker = usersInRoomChecker; _logger = loggerProvider.CreateLogger("ASC.Files"); + _userManagerWrapper = userManagerWrapper; } public async Task SetAceObjectAsync(List aceWrappers, FileEntry entry, bool notify, string message, AceAdvancedSettingsWrapper advancedSettings) @@ -117,7 +117,7 @@ public class FileSharingAceHelper continue; } - if (!ProcessEmailAce(w)) + if (!await ProcessEmailAceAsync(w)) { continue; } @@ -159,7 +159,7 @@ public class FileSharingAceHelper 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 await _fileMarker.RemoveMarkAsNewAsync(entry); } - private bool ProcessEmailAce(AceWrapper ace) + private async Task 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; diff --git a/products/ASC.People/Server/Api/UserController.cs b/products/ASC.People/Server/Api/UserController.cs index 0b85f5d027..20e2e055e9 100644 --- a/products/ASC.People/Server/Api/UserController.cs +++ b/products/ASC.People/Server/Api/UserController.cs @@ -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 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 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 { diff --git a/products/ASC.People/Server/ApiModels/RequestDto/InviteUsersRequestDto.cs b/products/ASC.People/Server/ApiModels/RequestDto/InviteUsersRequestDto.cs new file mode 100644 index 0000000000..0dec548c4f --- /dev/null +++ b/products/ASC.People/Server/ApiModels/RequestDto/InviteUsersRequestDto.cs @@ -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 Invitations { get; set; } +} + +public class UserInvitation +{ + public string Email { get; set; } + public EmployeeType Type { get; set; } +} \ No newline at end of file diff --git a/products/ASC.People/Server/ApiModels/RequestDto/MemberRequestDto.cs b/products/ASC.People/Server/ApiModels/RequestDto/MemberRequestDto.cs index a6ce791827..856e40927c 100644 --- a/products/ASC.People/Server/ApiModels/RequestDto/MemberRequestDto.cs +++ b/products/ASC.People/Server/ApiModels/RequestDto/MemberRequestDto.cs @@ -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; } diff --git a/web/ASC.Web.Core/Notify/Actions.cs b/web/ASC.Web.Core/Notify/Actions.cs index 85a5e96481..d05ecf1251 100644 --- a/web/ASC.Web.Core/Notify/Actions.cs +++ b/web/ASC.Web.Core/Notify/Actions.cs @@ -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"); } diff --git a/web/ASC.Web.Core/Notify/StudioNotifyService.cs b/web/ASC.Web.Core/Notify/StudioNotifyService.cs index 0269fbe0ae..db61fca540 100644 --- a/web/ASC.Web.Core/Notify/StudioNotifyService.cs +++ b/web/ASC.Web.Core/Notify/StudioNotifyService.cs @@ -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 diff --git a/web/ASC.Web.Core/PublicResources/WebstudioNotifyPatternResource.Designer.cs b/web/ASC.Web.Core/PublicResources/WebstudioNotifyPatternResource.Designer.cs index aae4f35925..a37dbfe30e 100644 --- a/web/ASC.Web.Core/PublicResources/WebstudioNotifyPatternResource.Designer.cs +++ b/web/ASC.Web.Core/PublicResources/WebstudioNotifyPatternResource.Designer.cs @@ -303,6 +303,15 @@ namespace ASC.Web.Core.PublicResources { } } + /// + /// Looks up a localized string similar to Confirm DocSpace Invite. + /// + public static string ButtonConfirmDocSpaceInvite { + get { + return ResourceManager.GetString("ButtonConfirmDocSpaceInvite", resourceCulture); + } + } + /// /// Looks up a localized string similar to Confirm Portal Address Change. /// diff --git a/web/ASC.Web.Core/PublicResources/WebstudioNotifyPatternResource.resx b/web/ASC.Web.Core/PublicResources/WebstudioNotifyPatternResource.resx index 532d259897..0798309f5c 100644 --- a/web/ASC.Web.Core/PublicResources/WebstudioNotifyPatternResource.resx +++ b/web/ASC.Web.Core/PublicResources/WebstudioNotifyPatternResource.resx @@ -2121,4 +2121,7 @@ $Body Sales department request + + Confirm DocSpace Invite + \ No newline at end of file diff --git a/web/ASC.Web.Core/Users/UserManagerWrapper.cs b/web/ASC.Web.Core/Users/UserManagerWrapper.cs index 29335c7628..745a838295 100644 --- a/web/ASC.Web.Core/Users/UserManagerWrapper.cs +++ b/web/ASC.Web.Core/Users/UserManagerWrapper.cs @@ -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 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; }