Fix Bug 69046 - Rooms. Added password dialog for public files

This commit is contained in:
Nikita Gopienko 2024-08-02 13:17:03 +03:00
parent f23c55c98e
commit 35edf993ca
14 changed files with 514 additions and 20 deletions

View File

@ -54,14 +54,7 @@ export const ConfirmType = Object.freeze({
* Enum for result of validation public room keys. * Enum for result of validation public room keys.
* @readonly * @readonly
*/ */
export const ValidationStatus = Object.freeze({
Ok: 0,
Invalid: 1,
Expired: 2,
Password: 3,
InvalidPassword: 4,
ExternalAccessDenied: 5,
});
export const GUID_EMPTY = "00000000-0000-0000-0000-000000000000"; export const GUID_EMPTY = "00000000-0000-0000-0000-000000000000";
export const ID_NO_GROUP_MANAGER = "4a515a15-d4d6-4b8e-828e-e0586f18f3a3"; export const ID_NO_GROUP_MANAGER = "4a515a15-d4d6-4b8e-828e-e0586f18f3a3";

View File

@ -4,7 +4,7 @@ import { observer, inject } from "mobx-react";
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import api from "@docspace/shared/api"; import api from "@docspace/shared/api";
import { UrlActionType } from "@docspace/shared/enums"; import { UrlActionType, ValidationStatus } from "@docspace/shared/enums";
import { toastr } from "@docspace/shared/components/toast"; import { toastr } from "@docspace/shared/components/toast";
import MediaViewer from "@docspace/shared/components/media-viewer/MediaViewer"; import MediaViewer from "@docspace/shared/components/media-viewer/MediaViewer";
import { ViewerLoader } from "@docspace/shared/components/media-viewer/sub-components/ViewerLoader"; import { ViewerLoader } from "@docspace/shared/components/media-viewer/sub-components/ViewerLoader";
@ -17,8 +17,6 @@ import type {
PlaylistType, PlaylistType,
} from "@docspace/shared/components/media-viewer/MediaViewer.types"; } from "@docspace/shared/components/media-viewer/MediaViewer.types";
import { ValidationStatus } from "SRC_DIR/helpers/constants";
import type { PublicPreviewProps } from "./PublicPreview.types"; import type { PublicPreviewProps } from "./PublicPreview.types";
import { DEFAULT_EXTS_IMAGE } from "./PublicPreview.constants"; import { DEFAULT_EXTS_IMAGE } from "./PublicPreview.constants";
import { isAxiosError, useDeviceType } from "./PublicPreview.helpers"; import { isAxiosError, useDeviceType } from "./PublicPreview.helpers";

View File

@ -29,7 +29,7 @@ import { observer, inject } from "mobx-react";
import { useNavigate, useLocation, useSearchParams } from "react-router-dom"; import { useNavigate, useLocation, useSearchParams } from "react-router-dom";
import Section from "@docspace/shared/components/section"; import Section from "@docspace/shared/components/section";
import { Loader } from "@docspace/shared/components/loader"; import { Loader } from "@docspace/shared/components/loader";
import { ValidationStatus } from "../../helpers/constants"; import { ValidationStatus } from "@docspace/shared/enums";
import SectionWrapper from "SRC_DIR/components/Section"; import SectionWrapper from "SRC_DIR/components/Section";
import RoomPassword from "./sub-components/RoomPassword"; import RoomPassword from "./sub-components/RoomPassword";
import RoomErrors from "./sub-components/RoomErrors"; import RoomErrors from "./sub-components/RoomErrors";

View File

@ -37,7 +37,7 @@ import { frameCallCommand } from "@docspace/shared/utils/common";
import { toastr } from "@docspace/shared/components/toast"; import { toastr } from "@docspace/shared/components/toast";
import { FormWrapper } from "@docspace/shared/components/form-wrapper"; import { FormWrapper } from "@docspace/shared/components/form-wrapper";
import PortalLogo from "@docspace/shared/components/portal-logo/PortalLogo"; import PortalLogo from "@docspace/shared/components/portal-logo/PortalLogo";
import { ValidationStatus } from "../../../helpers/constants"; import { ValidationStatus } from "@docspace/shared/enums";
import PublicRoomIcon from "PUBLIC_DIR/images/icons/32/room/public.svg"; import PublicRoomIcon from "PUBLIC_DIR/images/icons/32/room/public.svg";
@ -167,6 +167,7 @@ const RoomPassword = (props) => {
isDisabled={isLoading} isDisabled={isLoading}
isDisableTooltip isDisableTooltip
forwardedRef={inputRef} forwardedRef={inputRef}
isAutoFocussed
/> />
</FieldContainer> </FieldContainer>
</div> </div>

View File

@ -38,7 +38,8 @@ import {
import { CategoryType } from "SRC_DIR/helpers/constants"; import { CategoryType } from "SRC_DIR/helpers/constants";
import { getCategoryUrl } from "SRC_DIR/helpers/utils"; import { getCategoryUrl } from "SRC_DIR/helpers/utils";
import { LinkType, ValidationStatus } from "../helpers/constants"; import { LinkType } from "../helpers/constants";
import { ValidationStatus } from "@docspace/shared/enums";
class PublicRoomStore { class PublicRoomStore {
externalLinks = []; externalLinks = [];

View File

@ -29,9 +29,11 @@ import { headers } from "next/headers";
import { getSelectorsByUserAgent } from "react-device-detect"; import { getSelectorsByUserAgent } from "react-device-detect";
import { getData } from "@/utils/actions"; import { getData, validatePublicRoomKey } from "@/utils/actions";
import { RootPageProps } from "@/types"; import { RootPageProps } from "@/types";
import Root from "@/components/Root"; import Root from "@/components/Root";
import FilePassword from "@/components/file-password";
import { ValidationStatus } from "@docspace/shared/enums";
const initialSearchParams: RootPageProps["searchParams"] = { const initialSearchParams: RootPageProps["searchParams"] = {
fileId: undefined, fileId: undefined,
@ -58,6 +60,17 @@ async function Page({ searchParams }: RootPageProps) {
if (isMobile) type = "mobile"; if (isMobile) type = "mobile";
} }
if (share) {
const roomData = await validatePublicRoomKey(share, fileId ?? fileid ?? "");
if (!roomData) return;
const { status } = roomData.response;
if (status === ValidationStatus.Password) {
return <FilePassword {...roomData.response} shareKey={share} />;
}
}
const startDate = new Date(); const startDate = new Date();
const data = await getData( const data = await getData(

View File

@ -0,0 +1,198 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// 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
import styled, { css } from "styled-components";
import { mobile, tablet } from "@docspace/shared/utils";
import { isIOS, isFirefox } from "react-device-detect";
import BackgroundPatternReactSvgUrl from "PUBLIC_DIR/images/background.pattern.react.svg?url";
export const StyledPage = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin: 0 auto;
box-sizing: border-box;
background-image: url("${BackgroundPatternReactSvgUrl}");
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
.logo-wrapper {
display: block;
}
@media ${mobile} {
background-image: none;
height: 0;
.logo-wrapper {
display: none;
}
}
height: ${isIOS && !isFirefox ? "calc(var(--vh, 1vh) * 100)" : "100vh"};
width: 100vw;
@media ${tablet} {
padding: 0 16px;
}
@media ${mobile} {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
padding: 0 16px 0 8px;
`
: css`
padding: 0 8px 0 16px;
`}
}
.subtitle {
margin-bottom: 32px;
}
.password-form {
width: 100%;
margin-bottom: 8px;
}
.subtitle {
margin-bottom: 32px;
}
.public-room-content {
padding-top: 9%;
justify-content: unset;
min-height: unset;
.public-room-text {
margin: 8px 0;
white-space: wrap;
}
.public-room-name {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 32px;
}
.public-room-icon {
min-width: 32px;
min-height: 32px;
}
}
`;
export const StyledContent = styled.div`
min-height: 100vh;
flex: 1 0 auto;
flex-direction: column;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
@media ${mobile} {
justify-content: start;
min-height: 100%;
}
`;
export const StyledBody = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin: 56px auto;
@media ${mobile} {
width: 100%;
margin: 0 auto;
}
.title {
margin-bottom: 32px;
text-align: center;
}
.subtitle {
margin-bottom: 32px;
}
.portal-logo {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 64px;
}
.password-field-wrapper {
width: 100%;
}
.password-change-form {
margin-top: 32px;
margin-bottom: 16px;
}
.phone-input {
margin-bottom: 24px;
}
.delete-profile-confirm {
margin-bottom: 8px;
}
.phone-title {
margin-bottom: 8px;
}
`;
export const StyledSimpleNav = styled.div`
display: none;
height: 48px;
align-items: center;
justify-content: center;
background-color: ${(props) => props.theme?.login?.navBackground};
.logo {
height: 24px;
}
@media ${mobile} {
display: flex;
.language-combo-box {
position: absolute;
top: 7px;
right: 8px;
}
}
`;

View File

@ -0,0 +1,34 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// 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
export interface FilePasswordProps {
shareKey: string;
title: string;
id: string;
status: string;
roomType: string;
entryTitle: string;
}

View File

@ -0,0 +1,212 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// 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
"use client";
import React, { useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Text } from "@docspace/shared/components/text";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { PasswordInput } from "@docspace/shared/components/password-input";
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
import PortalLogo from "@docspace/shared/components/portal-logo/PortalLogo";
import {
StyledPage,
StyledContent,
StyledBody,
StyledSimpleNav,
} from "./FilePassword.styled";
import { FilePasswordProps } from "./FilePassword.types";
import PublicRoomIcon from "PUBLIC_DIR/images/icons/32/room/public.svg";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { toastr } from "@docspace/shared/components/toast";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { getLogoUrl } from "@docspace/shared/utils";
import { useTheme } from "styled-components";
import { ValidationStatus, WhiteLabelLogoType } from "@docspace/shared/enums";
import { validatePublicRoomPassword } from "@docspace/shared/api/rooms";
import Image from "next/image";
const FilesPassword = ({ shareKey, title, entryTitle }: FilePasswordProps) => {
const { t } = useTranslation(["Common"]);
const theme = useTheme();
const [password, setPassword] = useState("");
const [passwordValid, setPasswordValid] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const onChangePassword = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
!passwordValid && setPasswordValid(true);
};
const onKeyPress = (event: React.KeyboardEvent) => {
if (event.key === "Enter") {
onSubmit();
}
};
const onSubmit = async () => {
if (!password.trim()) {
setPasswordValid(false);
setErrorMessage(t("Common:RequiredField"));
} else {
setErrorMessage("");
}
if (!passwordValid || !password.trim()) {
setIsLoading(false);
return;
}
setIsLoading(true);
try {
const res = await validatePublicRoomPassword(shareKey, password);
if (res?.status === ValidationStatus.Ok) {
return window.location.reload();
}
setIsLoading(false);
if (res?.status === ValidationStatus.InvalidPassword) {
setErrorMessage(t("Common:IncorrectPassword"));
return;
}
} catch (error) {
toastr.error(error as TData);
setIsLoading(false);
}
};
const logoUrl = getLogoUrl(WhiteLabelLogoType.LoginPage, !theme.isBase);
return (
<>
<StyledSimpleNav id="login-header">
<Image
className="logo"
src={logoUrl}
priority
alt="mobile-icon"
width={211}
height={24}
/>
</StyledSimpleNav>
<StyledPage>
<StyledContent className="public-room-content">
<StyledBody>
<Image
priority
src={logoUrl}
className="logo-wrapper"
alt="icon"
width={416}
height={200}
/>
<FormWrapper>
<div className="password-form">
<Text fontSize="16px" fontWeight="600">
{t("Common:PasswordRequired")}
</Text>
<Text
fontSize="13px"
fontWeight="400"
className="public-room-text"
>
<Trans
t={t}
ns="Common"
i18nKey="EnterPasswordDescription"
values={{ fileName: entryTitle }}
components={{ 1: <span className="bold" /> }}
/>
</Text>
<div className="public-room-name">
<PublicRoomIcon className="public-room-icon" />
<Text
className="public-room-text"
fontSize="15px"
fontWeight="600"
>
{title}
</Text>
</div>
<FieldContainer
isVertical={true}
labelVisible={false}
hasError={!!errorMessage}
errorMessage={errorMessage}
>
<PasswordInput
simpleView
id="password"
inputName="password"
placeholder={t("Common:Password")}
type={InputType.password}
inputValue={password}
hasError={!!errorMessage}
size={InputSize.large}
scale
tabIndex={1}
autoComplete="current-password"
onChange={onChangePassword}
onKeyDown={onKeyPress}
isDisabled={isLoading}
isDisableTooltip
isAutoFocussed
// forwardedRef={inputRef}
/>
</FieldContainer>
</div>
<Button
primary
size={ButtonSize.medium}
scale
label={t("Common:ContinueButton")}
tabIndex={5}
onClick={onSubmit}
isDisabled={isLoading}
/>
</FormWrapper>
</StyledBody>
</StyledContent>
</StyledPage>
</>
);
};
export default FilesPassword;

View File

@ -393,6 +393,22 @@ export const checkIsAuthenticated = async () => {
return isAuth.response as boolean; return isAuth.response as boolean;
}; };
export async function validatePublicRoomKey(key: string, fileId?: string) {
const [validatePublicRoomKey] = createRequest(
[`/files/share/${key}?fileid=${fileId}`],
[key ? ["Request-Token", key] : ["", ""]],
"GET",
);
const res = await fetch(validatePublicRoomKey);
if (res.status === 401) return undefined;
if (!res.ok) return;
const room = await res.json();
return room;
}
// export async function checkFillFromDraft( // export async function checkFillFromDraft(
// templateFileId: number, // templateFileId: number,
// share?: string, // share?: string,

View File

@ -35,7 +35,7 @@ import {
toUrlParams, toUrlParams,
} from "../../utils/common"; } from "../../utils/common";
import RoomsFilter from "./filter"; import RoomsFilter from "./filter";
import { TGetRooms } from "./types"; import { TGetRooms, TPublicRoomPassword } from "./types";
export async function getRooms(filter: RoomsFilter, signal?: AbortSignal) { export async function getRooms(filter: RoomsFilter, signal?: AbortSignal) {
let params; let params;
@ -452,12 +452,17 @@ export function validatePublicRoomKey(key) {
}); });
} }
export function validatePublicRoomPassword(key, passwordHash) { export async function validatePublicRoomPassword(
return request({ key: string,
passwordHash: string,
) {
const res = (await request({
method: "post", method: "post",
url: `files/share/${key}/password`, url: `files/share/${key}/password`,
data: { password: passwordHash }, data: { password: passwordHash },
}); })) as TPublicRoomPassword;
return res;
} }
export function setCustomRoomQuota(roomIds, quota) { export function setCustomRoomQuota(roomIds, quota) {

View File

@ -25,7 +25,12 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { TFile, TFolder } from "../files/types"; import { TFile, TFolder } from "../files/types";
import { FolderType, RoomsType, ShareAccessRights } from "../../enums"; import {
FolderType,
RoomsType,
ShareAccessRights,
ValidationStatus,
} from "../../enums";
import { TCreatedBy, TPathParts } from "../../types"; import { TCreatedBy, TPathParts } from "../../types";
export type TLogo = { export type TLogo = {
@ -91,3 +96,10 @@ export type TGetRooms = {
total: number; total: number;
new: number; new: number;
}; };
export type TPublicRoomPassword = {
linkId: string;
shared: boolean;
status: ValidationStatus;
tenantId: string | number;
};

View File

@ -593,3 +593,12 @@ export enum FileExtensions {
XLSX = "xlsx", XLSX = "xlsx",
PPTX = "pptx", PPTX = "pptx",
} }
export enum ValidationStatus {
Ok = 0,
Invalid = 1,
Expired = 2,
Password = 3,
InvalidPassword = 4,
ExternalAccessDenied = 5,
}

View File

@ -277,6 +277,8 @@
"MyDocuments": "My documents", "MyDocuments": "My documents",
"Name": "Name", "Name": "Name",
"NeedPassword": "You need a password to access the room", "NeedPassword": "You need a password to access the room",
"EnterPasswordDescription": "File «<1>{{fileName}}</1>» is located in the password-protected room. Please enter a password for the room",
"PasswordRequired": "Password required",
"NewDocument": "New document", "NewDocument": "New document",
"NewFolder": "New folder", "NewFolder": "New folder",
"NewMasterForm": "New form template", "NewMasterForm": "New form template",