From a72cd10447be769dd742e53766d582f3c28c72a3 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Fri, 15 Dec 2023 09:50:26 -0400 Subject: [PATCH 01/14] feat: add password protection dialogs kickoff --- .../components/ShareDialog/ShareDialog.tsx | 82 ++++++++++++++++++- .../SharePasswordDisableDialog.tsx | 41 ++++++++++ .../SharePasswordInputDialog.tsx | 46 +++++++++++ src/app/share/services/share.service.ts | 23 ++++++ src/app/share/types/index.ts | 1 + 5 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/app/share/components/SharePasswordDisableDialog/SharePasswordDisableDialog.tsx create mode 100644 src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx diff --git a/src/app/drive/components/ShareDialog/ShareDialog.tsx b/src/app/drive/components/ShareDialog/ShareDialog.tsx index ccb4d3007..6b57e0a43 100644 --- a/src/app/drive/components/ShareDialog/ShareDialog.tsx +++ b/src/app/drive/components/ShareDialog/ShareDialog.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Popover } from '@headlessui/react'; import { connect } from 'react-redux'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; @@ -23,6 +23,11 @@ import { AdvancedSharedItem } from '../../../share/types'; import { DriveItemData } from '../../types'; import { TrackingPlan } from '../../../analytics/TrackingPlan'; import { trackPublicShared } from '../../../analytics/services/analytics.service'; +import PasswordInput from 'app/share/components/ShareItemDialog/components/PasswordInput'; +import BaseCheckbox from 'app/shared/components/forms/BaseCheckbox/BaseCheckbox'; +import { SharePasswordDisableDialog } from 'app/share/components/SharePasswordDisableDialog/SharePasswordDisableDialog'; +import { SharingMeta } from '@internxt/sdk/dist/drive/share/types'; +import { SharePasswordInputDialog } from 'app/share/components/SharePasswordInputDialog/SharePasswordInputDialog'; type AccessMode = 'public' | 'restricted'; type UserRole = 'owner' | 'editor' | 'reader'; @@ -95,6 +100,10 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { const [isLoading, setIsLoading] = useState(false); const [invitedUsers, setInvitedUsers] = useState([]); const [currentUserFolderRole, setCurrentUserFolderRole] = useState(''); + const [isPasswordProtected, setIsPasswordProtected] = useState(false); + const [openPasswordInput, setOpenPasswordInput] = useState(false); + const [openPasswordDisableDialog, setOpenPasswordDisableDialog] = useState(false); + const [sharingMeta, setSharingMeta] = useState(); const [accessRequests, setAccessRequests] = useState([]); const [userOptionsEmail, setUserOptionsEmail] = useState(); @@ -179,6 +188,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { // Change object type of itemToShare to AdvancedSharedItem let shareAccessMode: AccessMode = 'public'; let sharingType = 'public'; + let isAlreadyPasswordProtected = false; if (props.isDriveItem) { sharingType = (itemToShare?.item as DriveItemData & { sharings: { type: string; id: string }[] }).sharings?.[0] @@ -189,6 +199,8 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { try { const sharingData = await shareService.getSharingType(itemId, itemType); sharingType = sharingData.type; + isAlreadyPasswordProtected = sharingData.encryptedPassword ? true : false; + setSharingMeta(sharingData); } catch (error) { errorService.reportError(error); } @@ -198,7 +210,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { shareAccessMode = 'restricted'; } setAccessMode(shareAccessMode); - + setIsPasswordProtected(isAlreadyPasswordProtected); if (!itemToShare?.item) return; try { @@ -308,6 +320,45 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { closeSelectedUserPopover(); }; + const onPasswordCheckboxChange = useCallback(() => { + if (!isPasswordProtected) { + setOpenPasswordInput(true); + } else { + setOpenPasswordDisableDialog(true); + } + }, [isPasswordProtected]); + + const onSavePublicSharePassword = useCallback( + async (passwordValue: string) => { + try { + if (sharingMeta) { + const code = shareService.decryptPublicSharingCodeWithOwner(sharingMeta.encryptedCode); + + await shareService.saveSharingPassword(sharingMeta.id, code, passwordValue); + setIsPasswordProtected(true); + } + } catch (error) { + errorService.castError(error); + } finally { + setOpenPasswordInput(false); + } + }, + [sharingMeta], + ); + + const onDisablePassword = useCallback(async () => { + try { + if (sharingMeta) { + await shareService.removeSharingPassword(sharingMeta.id); + setIsPasswordProtected(false); + } + } catch (error) { + errorService.castError(error); + } finally { + setOpenPasswordDisableDialog(false); + } + }, [sharingMeta]); + const changeAccess = async (mode: AccessMode) => { closeSelectedUserPopover(); if (mode != accessMode) { @@ -494,6 +545,21 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => {
+ {accessMode === 'public' && ( +
+
+
+ +

{translate('shareItemDialog.protect')}

+
+
+ {isPasswordProtected && ( + + )} +
+ )}

{translate('modals.shareModal.general.generalAccess')}

@@ -603,6 +669,18 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => {
+ setOpenPasswordInput(false)} + isOpen={openPasswordInput} + onSavePassword={onSavePublicSharePassword} + > + + setOpenPasswordDisableDialog(false)} + onConfirmHandler={onDisablePassword} + /> + {/* Stop sharing confirmation dialog */} void; + onConfirmHandler: () => Promise | void; +}; + +export const SharePasswordDisableDialog = ({ + isOpen, + onClose, + onConfirmHandler, +}: SharePasswordDisableWarningDialogProps) => { + const [isLoading, setIsLoading] = useState(false); + + const handleConfirm = async () => { + setIsLoading(true); + await onConfirmHandler(); + setIsLoading(false); + }; + + return ( + +
+

Disable password protection?

+

When disabled, people with the link will be able to access the content.

+ +
+ + +
+
+
+ ); +}; diff --git a/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx b/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx new file mode 100644 index 000000000..d7edb82dc --- /dev/null +++ b/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx @@ -0,0 +1,46 @@ +import Button from 'app/shared/components/Button/Button'; +import Modal from 'app/shared/components/Modal'; +import { useState } from 'react'; +import { Spinner } from '@phosphor-icons/react'; +import Input from 'app/shared/components/Input'; + +type SharePasswordInputDialogProps = { + isOpen: boolean; + onClose: () => void; + onSavePassword: (password: string) => Promise | void; +}; + +export const SharePasswordInputDialog = ({ isOpen, onClose, onSavePassword }: SharePasswordInputDialogProps) => { + const [isLoading, setIsLoading] = useState(false); + const [password, setPassword] = useState(''); + + const handleConfirm = async () => { + setIsLoading(true); + await onSavePassword(password); + setIsLoading(false); + }; + + return ( + +

Edit password

+ { + if (value.length <= 50) { + setPassword(value); + } + }} + value={password} + variant="password" + /> +
+ + +
+
+ ); +}; diff --git a/src/app/share/services/share.service.ts b/src/app/share/services/share.service.ts index da94dc1da..e52e572c5 100644 --- a/src/app/share/services/share.service.ts +++ b/src/app/share/services/share.service.ts @@ -390,6 +390,12 @@ export const createPublicShareFromOwnerUser = async ( }); }; +export const decryptPublicSharingCodeWithOwner = (encryptedCode: string) => { + const user = localStorageService.getUser() as UserSettings; + const { mnemonic } = user; + return aes.decrypt(encryptedCode, mnemonic); +}; + export const getPublicShareLink = async ( uuid: string, itemType: 'folder' | 'file', @@ -821,6 +827,20 @@ export function getSharingType(itemId: string, itemType: 'file' | 'folder'): Pro }); } +export function saveSharingPassword(sharingId: string, code: string, password: string): Promise { + const shareClient = SdkFactory.getNewApiInstance().createShareClient(); + return shareClient.saveSharingPassword(sharingId, code, password).catch((error) => { + throw errorService.castError(error); + }); +} + +export function removeSharingPassword(sharingId: string): Promise { + const shareClient = SdkFactory.getNewApiInstance().createShareClient(); + return shareClient.removeSharingPassword(sharingId).catch((error) => { + throw errorService.castError(error); + }); +} + const shareService = { createShare, createShareLink, @@ -856,6 +876,9 @@ const shareService = { getPublicSharingMeta, getPublicSharedFolderContent, getPublicShareLink, + saveSharingPassword, + removeSharingPassword, + decryptPublicSharingCodeWithOwner, }; export default shareService; diff --git a/src/app/share/types/index.ts b/src/app/share/types/index.ts index df49d65eb..58976d0fc 100644 --- a/src/app/share/types/index.ts +++ b/src/app/share/types/index.ts @@ -9,6 +9,7 @@ export type AdvancedSharedItem = SharedFolders & credentials: SharedNetworkCredentials; sharingId?: string; sharingType: 'public' | 'private'; + encryptedPassword: string | null; }; export type SharedNetworkCredentials = { From 671c0fa14eb63745fc6a4a7edda58ad12448df23 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Sun, 17 Dec 2023 23:37:16 -0400 Subject: [PATCH 02/14] feat: added password protection to shareInvite modal --- .../components/ShareDialog/ShareDialog.tsx | 81 ++++++++++++------- src/app/i18n/locales/cn.json | 16 +++- src/app/i18n/locales/de.json | 16 +++- src/app/i18n/locales/en.json | 17 +++- src/app/i18n/locales/es.json | 16 +++- src/app/i18n/locales/fr.json | 19 ++++- src/app/i18n/locales/it.json | 16 +++- src/app/i18n/locales/ru.json | 16 +++- .../SharePasswordDisableDialog.tsx | 12 ++- .../SharePasswordInputDialog.tsx | 23 ++++-- 10 files changed, 185 insertions(+), 47 deletions(-) diff --git a/src/app/drive/components/ShareDialog/ShareDialog.tsx b/src/app/drive/components/ShareDialog/ShareDialog.tsx index 6b57e0a43..4312902c2 100644 --- a/src/app/drive/components/ShareDialog/ShareDialog.tsx +++ b/src/app/drive/components/ShareDialog/ShareDialog.tsx @@ -8,7 +8,18 @@ import Button from 'app/shared/components/Button/Button'; import Modal from 'app/shared/components/Modal'; import ShareInviteDialog from '../ShareInviteDialog/ShareInviteDialog'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; -import { ArrowLeft, CaretDown, Check, CheckCircle, Globe, Link, UserPlus, Users, X } from '@phosphor-icons/react'; +import { + ArrowLeft, + CaretDown, + Check, + CheckCircle, + Globe, + Link, + Question, + UserPlus, + Users, + X, +} from '@phosphor-icons/react'; import Avatar from 'app/shared/components/Avatar'; import Spinner from 'app/shared/components/Spinner/Spinner'; import { sharedThunks } from '../../../store/slices/sharedLinks'; @@ -23,11 +34,12 @@ import { AdvancedSharedItem } from '../../../share/types'; import { DriveItemData } from '../../types'; import { TrackingPlan } from '../../../analytics/TrackingPlan'; import { trackPublicShared } from '../../../analytics/services/analytics.service'; -import PasswordInput from 'app/share/components/ShareItemDialog/components/PasswordInput'; import BaseCheckbox from 'app/shared/components/forms/BaseCheckbox/BaseCheckbox'; import { SharePasswordDisableDialog } from 'app/share/components/SharePasswordDisableDialog/SharePasswordDisableDialog'; import { SharingMeta } from '@internxt/sdk/dist/drive/share/types'; import { SharePasswordInputDialog } from 'app/share/components/SharePasswordInputDialog/SharePasswordInputDialog'; +import { Tooltip } from 'react-tooltip'; +import { DELAY_SHOW_MS } from 'app/shared/components/Tooltip/Tooltip'; type AccessMode = 'public' | 'restricted'; type UserRole = 'owner' | 'editor' | 'reader'; @@ -145,7 +157,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { if (roles.length === 0) dispatch(sharedThunks.getSharedFolderRoles()); if (roles.length > 0) loadShareInfo(); - }, [roles]); + }, [roles, isOpen]); useEffect(() => { const removeDeniedRequests = () => { @@ -190,20 +202,15 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { let sharingType = 'public'; let isAlreadyPasswordProtected = false; - if (props.isDriveItem) { - sharingType = (itemToShare?.item as DriveItemData & { sharings: { type: string; id: string }[] }).sharings?.[0] - ?.type; - } else { - const itemType = itemToShare?.item.isFolder ? 'folder' : 'file'; - const itemId = itemToShare?.item.uuid ?? ''; - try { - const sharingData = await shareService.getSharingType(itemId, itemType); - sharingType = sharingData.type; - isAlreadyPasswordProtected = sharingData.encryptedPassword ? true : false; - setSharingMeta(sharingData); - } catch (error) { - errorService.reportError(error); - } + const itemType = itemToShare?.item.isFolder ? 'folder' : 'file'; + const itemId = itemToShare?.item.uuid ?? ''; + try { + const sharingData = await shareService.getSharingType(itemId, itemType); + sharingType = sharingData.type; + isAlreadyPasswordProtected = sharingData.encryptedPassword ? true : false; + setSharingMeta(sharingData); + } catch (error) { + errorService.reportError(error); } if (sharingType === 'private') { @@ -331,19 +338,25 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { const onSavePublicSharePassword = useCallback( async (passwordValue: string) => { try { - if (sharingMeta) { - const code = shareService.decryptPublicSharingCodeWithOwner(sharingMeta.encryptedCode); + let sharingInfo = sharingMeta; - await shareService.saveSharingPassword(sharingMeta.id, code, passwordValue); - setIsPasswordProtected(true); + if (!sharingInfo) { + const itemType = itemToShare?.item.isFolder ? 'folder' : 'file'; + const itemId = itemToShare?.item.uuid ?? ''; + sharingInfo = await shareService.createPublicShareFromOwnerUser(itemId, itemType); + setSharingMeta(sharingInfo); } + + const code = shareService.decryptPublicSharingCodeWithOwner(sharingInfo.encryptedCode); + await shareService.saveSharingPassword(sharingInfo.id, code, passwordValue); + setIsPasswordProtected(true); } catch (error) { errorService.castError(error); } finally { setOpenPasswordInput(false); } }, - [sharingMeta], + [sharingMeta, itemToShare], ); const onDisablePassword = useCallback(async () => { @@ -372,6 +385,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { if (sharingType === 'public') { await shareService.createPublicShareFromOwnerUser(itemId, itemType); } + await loadShareInfo(); setAccessMode(mode); } catch (error) { errorService.reportError(error); @@ -545,17 +559,30 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => {
- {accessMode === 'public' && ( + {accessMode === 'public' && !isLoading && isUserOwner && (
-

{translate('shareItemDialog.protect')}

+

+ {translate('modals.shareModal.protectSharingModal.buttons.changePassword')} +

+ + +

+ {translate('modals.shareModal.protectSharingModal.protectTooltipText')} +

+
{isPasswordProtected && ( )}
@@ -673,8 +700,8 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { onClose={() => setOpenPasswordInput(false)} isOpen={openPasswordInput} onSavePassword={onSavePublicSharePassword} - > - + isAlreadyProtected={isPasswordProtected} + /> setOpenPasswordDisableDialog(false)} diff --git a/src/app/i18n/locales/cn.json b/src/app/i18n/locales/cn.json index 6f6c7a829..9e49269e2 100644 --- a/src/app/i18n/locales/cn.json +++ b/src/app/i18n/locales/cn.json @@ -746,6 +746,20 @@ "change": "改变", "requestSent": "请求已发送", "confirmation": "如果与您共享文件/文件夹 您将收到一封电子邮件" + }, + "protectSharingModal": { + "protectTooltipText": "仅对没有查看或编辑访问权限的用户需要密码。", + "protect": "使用密码保护", + "disablePasswordTitle": "禁用密码保护?", + "disablePasswordBody": "禁用后,拥有链接的人将能够访问内容。", + "createPasswordTitle": "创建密码", + "editPasswordTitle": "编辑密码", + "buttons": { + "cancel": "取消", + "save": "保存", + "disable": "禁用", + "changePassword": "更改密码" + } } }, "sharedInvitationsModal": { @@ -1323,4 +1337,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 2a56e3a2e..12b344969 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -642,6 +642,20 @@ "change": "Ändern", "requestSent": "Anfrage gesendet", "confirmation": "Sie erhalten eine E-Mail, wenn die Datei/der Ordner für Sie freigegeben wurde" + }, + "protectSharingModal": { + "protectTooltipText": "Ein Passwort ist nur für Benutzer erforderlich, die keine Viewer- oder Bearbeitungsberechtigungen für das freigegebene Element haben.", + "protect": "Mit Passwort schützen", + "disablePasswordTitle": "Passwortschutz deaktivieren?", + "disablePasswordBody": "Wenn deaktiviert, können Personen mit dem Link auf den Inhalt zugreifen.", + "createPasswordTitle": "Passwort erstellen", + "editPasswordTitle": "Passwort bearbeiten", + "buttons": { + "cancel": "Abbrechen", + "save": "Speichern", + "disable": "Deaktivieren", + "changePassword": "Passwort ändern" + } } }, "deletePhotosModal": { @@ -1196,4 +1210,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 3f9289992..a9d678a6c 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -759,6 +759,20 @@ "change": "Change", "requestSent": "Request Sent", "confirmation": "You will get an email if the file/folder is shared with you" + }, + "protectSharingModal": { + "protectTooltipText": "Password is only required for users without viewer or editor access to the shared item.", + "protect": "Protect with password", + "disablePasswordTitle": "Disable password protection?", + "disablePasswordBody": "When disabled, people with the link will be able to access the content.", + "createPasswordTitle": "Create passsword", + "editPasswordTitle": "Edit password", + "buttons": { + "cancel": "Cancel", + "save": "Save", + "disable": "Disable", + "changePassword": "Change password" + } } }, "sharedInvitationsModal": { @@ -1127,7 +1141,6 @@ "shareItemDialog": { "title": "Link settings", "access": "Access", - "protect": "Protect with password", "addSecurePassword": "Secure this link by setting up a password", "password": "Password", "views": "Views", @@ -1337,4 +1350,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index b19e6e608..3a1797958 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -741,6 +741,20 @@ "change": "Cambiar", "requestSent": "Solicitud enviada", "confirmation": "Recibirás un correo electrónico si el archivo/carpeta se comparte" + }, + "protectSharingModal": { + "protectTooltipText": "La contraseña solo es necesaria para los usuarios sin acceso de visor o editor al elemento compartido.", + "protect": "Proteger con contraseña", + "disablePasswordTitle": "¿Desactivar la protección con contraseña?", + "disablePasswordBody": "Cuando está desactivada, las personas con el enlace podrán acceder al contenido.", + "createPasswordTitle": "Crear contraseña", + "editPasswordTitle": "Editar contraseña", + "buttons": { + "cancel": "Cancelar", + "save": "Guardar", + "disable": "Desactivar", + "changePassword": "Cambiar contraseña" + } } }, "sharedInvitationsModal": { @@ -1318,4 +1332,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index 335d6551d..0da7af08c 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -136,7 +136,6 @@ "terms": "Termes et conditions", "help": "Besoin d'aide ?", "searchBar": "Rechercher dans ce dossier", - "stage": { "development": "Développement", "production": "Production" @@ -359,7 +358,6 @@ "business": "Changement de plan pour les entreprises" }, "viewAllFeatures": "Voir toutes les caractéristiques", - "card": { "annually": "€{{amount}} Facturation annuellement", "monthly": "€{{amount}} par mois", @@ -676,6 +674,20 @@ "change": "Changer", "requestSent": "Demande envoyée", "confirmation": "Vous recevrez un e-mail si le fichier/dossier est partagé avec vous" + }, + "protectSharingModal": { + "protectTooltipText": "Le mot de passe est uniquement requis pour les utilisateurs sans accès en lecture ou en modification à l'élément partagé.", + "protect": "Protéger par mot de passe", + "disablePasswordTitle": "Désactiver la protection par mot de passe ?", + "disablePasswordBody": "Lorsqu'elle est désactivée, les personnes disposant du lien pourront accéder au contenu.", + "createPasswordTitle": "Créer un mot de passe", + "editPasswordTitle": "Modifier le mot de passe", + "buttons": { + "cancel": "Annuler", + "save": "Enregistrer", + "disable": "Désactiver", + "changePassword": "Changer le mot de passe" + } } }, "sharedInvitationsModal": { @@ -882,7 +894,6 @@ "error-deleting-links": "Une erreur s'est produite lors de la suppression des liens partagés" } }, - "trash": { "trash": "Corbeille", "clearTrash": "La corbeille est vide", @@ -1248,4 +1259,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 24864d308..294dd3645 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -720,6 +720,20 @@ "change": "Cambia", "requestSent": "Richiesta inviata", "confirmation": "Riceverai un'e-mail se il file/cartella è condiviso con te" + }, + "protectSharingModal": { + "protectTooltipText": "La password è richiesta solo per gli utenti senza accesso in lettura o modifica all'elemento condiviso.", + "protect": "Proteggi con password", + "disablePasswordTitle": "Disabilitare la protezione con password?", + "disablePasswordBody": "Quando disabilitato, le persone con il link potranno accedere al contenuto.", + "createPasswordTitle": "Crea password", + "editPasswordTitle": "Modifica password", + "buttons": { + "cancel": "Annulla", + "save": "Salva", + "disable": "Disabilita", + "changePassword": "Cambia password" + } } }, "sharedInvitationsModal": { @@ -1292,4 +1306,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index 7764fb20f..2ac092f27 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -701,6 +701,20 @@ "change": "изменять", "requestSent": "Запрос отправлен", "confirmation": "Вы получите электронное письмо, если файл/папка открыта для вас" + }, + "protectSharingModal": { + "protectTooltipText": "Пароль требуется только для пользователей без доступа на просмотр или редактирование к общему элементу.", + "protect": "Защитить паролем", + "disablePasswordTitle": "Отключить защиту паролем?", + "disablePasswordBody": "При отключении люди с ссылкой смогут получить доступ к содержимому.", + "createPasswordTitle": "Создать пароль", + "editPasswordTitle": "Изменить пароль", + "buttons": { + "cancel": "Отмена", + "save": "Сохранить", + "disable": "Отключить", + "changePassword": "Изменить пароль" + } } }, "sharedInvitationsModal": { @@ -1221,4 +1235,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/share/components/SharePasswordDisableDialog/SharePasswordDisableDialog.tsx b/src/app/share/components/SharePasswordDisableDialog/SharePasswordDisableDialog.tsx index 9cefa4ef9..32d18f932 100644 --- a/src/app/share/components/SharePasswordDisableDialog/SharePasswordDisableDialog.tsx +++ b/src/app/share/components/SharePasswordDisableDialog/SharePasswordDisableDialog.tsx @@ -1,3 +1,4 @@ +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import Button from 'app/shared/components/Button/Button'; import Modal from 'app/shared/components/Modal'; import { useState } from 'react'; @@ -13,6 +14,7 @@ export const SharePasswordDisableDialog = ({ onClose, onConfirmHandler, }: SharePasswordDisableWarningDialogProps) => { + const { translate } = useTranslationContext(); const [isLoading, setIsLoading] = useState(false); const handleConfirm = async () => { @@ -24,15 +26,17 @@ export const SharePasswordDisableDialog = ({ return (
-

Disable password protection?

-

When disabled, people with the link will be able to access the content.

+

+ {translate('modals.shareModal.protectSharingModal.disablePasswordTitle')} +

+

{translate('modals.shareModal.protectSharingModal.disablePasswordBody')}

diff --git a/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx b/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx index d7edb82dc..c5ece8a1f 100644 --- a/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx +++ b/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx @@ -3,14 +3,22 @@ import Modal from 'app/shared/components/Modal'; import { useState } from 'react'; import { Spinner } from '@phosphor-icons/react'; import Input from 'app/shared/components/Input'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; type SharePasswordInputDialogProps = { isOpen: boolean; onClose: () => void; onSavePassword: (password: string) => Promise | void; + isAlreadyProtected?: boolean; }; -export const SharePasswordInputDialog = ({ isOpen, onClose, onSavePassword }: SharePasswordInputDialogProps) => { +export const SharePasswordInputDialog = ({ + isOpen, + onClose, + onSavePassword, + isAlreadyProtected = true, +}: SharePasswordInputDialogProps) => { + const { translate } = useTranslationContext(); const [isLoading, setIsLoading] = useState(false); const [password, setPassword] = useState(''); @@ -22,7 +30,11 @@ export const SharePasswordInputDialog = ({ isOpen, onClose, onSavePassword }: Sh return ( -

Edit password

+

+ {!isAlreadyProtected + ? translate('modals.shareModal.protectSharingModal.createPasswordTitle') + : translate('modals.shareModal.protectSharingModal.editPasswordTitle')} +

{ if (value.length <= 50) { @@ -31,14 +43,15 @@ export const SharePasswordInputDialog = ({ isOpen, onClose, onSavePassword }: Sh }} value={password} variant="password" + autoComplete="off" />
-
From b4e0c7b631845cdb30eea98894ce0e5a78fd98da Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Mon, 18 Dec 2023 01:00:17 -0400 Subject: [PATCH 03/14] fix: code smells and prevent loading when item is not shared --- .../components/ShareDialog/ShareDialog.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/app/drive/components/ShareDialog/ShareDialog.tsx b/src/app/drive/components/ShareDialog/ShareDialog.tsx index 4312902c2..baeb71fd0 100644 --- a/src/app/drive/components/ShareDialog/ShareDialog.tsx +++ b/src/app/drive/components/ShareDialog/ShareDialog.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; import { Popover } from '@headlessui/react'; import { connect } from 'react-redux'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; @@ -115,7 +115,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { const [isPasswordProtected, setIsPasswordProtected] = useState(false); const [openPasswordInput, setOpenPasswordInput] = useState(false); const [openPasswordDisableDialog, setOpenPasswordDisableDialog] = useState(false); - const [sharingMeta, setSharingMeta] = useState(); + const [sharingMeta, setSharingMeta] = useState(null); const [accessRequests, setAccessRequests] = useState([]); const [userOptionsEmail, setUserOptionsEmail] = useState(); @@ -139,6 +139,8 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { setUserOptionsEmail(undefined); setUserOptionsY(0); setView('general'); + setIsPasswordProtected(false); + setSharingMeta(null); }; useEffect(() => { @@ -196,6 +198,11 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { }, [itemToShare, roles]); const loadShareInfo = async () => { + if (!itemToShare?.item) return; + + const isItemNotSharedYet = !isAdvanchedShareItem(itemToShare?.item) && !itemToShare.item.sharings?.length; + if (isItemNotSharedYet) return; + setIsLoading(true); // Change object type of itemToShare to AdvancedSharedItem let shareAccessMode: AccessMode = 'public'; @@ -207,7 +214,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { try { const sharingData = await shareService.getSharingType(itemId, itemType); sharingType = sharingData.type; - isAlreadyPasswordProtected = sharingData.encryptedPassword ? true : false; + isAlreadyPasswordProtected = sharingData.encryptedPassword !== null; setSharingMeta(sharingData); } catch (error) { errorService.reportError(error); @@ -218,7 +225,6 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { } setAccessMode(shareAccessMode); setIsPasswordProtected(isAlreadyPasswordProtected); - if (!itemToShare?.item) return; try { await getAndUpdateInvitedUsers(); @@ -562,8 +568,8 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { {accessMode === 'public' && !isLoading && isUserOwner && (
-
- +
+

{translate('modals.shareModal.protectSharingModal.buttons.changePassword')}

From b86445c394f81765b71984fb54ec9cb71d2d648d Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Mon, 18 Dec 2023 01:06:36 -0400 Subject: [PATCH 04/14] mend --- src/app/i18n/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 2661d4121..7c952f780 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -1150,6 +1150,7 @@ "shareItemDialog": { "title": "Link settings", "access": "Access", + "protect": "Protect with password", "addSecurePassword": "Secure this link by setting up a password", "password": "Password", "views": "Views", From a2b756748f55b8e431b1d8416dd4a233f952882a Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Mon, 18 Dec 2023 11:44:40 -0400 Subject: [PATCH 05/14] fix: some improvements to the flow --- .../components/ShareDialog/ShareDialog.tsx | 43 +++++++++++-------- src/app/share/services/share.service.ts | 17 ++++++-- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/app/drive/components/ShareDialog/ShareDialog.tsx b/src/app/drive/components/ShareDialog/ShareDialog.tsx index baeb71fd0..9ce3e3315 100644 --- a/src/app/drive/components/ShareDialog/ShareDialog.tsx +++ b/src/app/drive/components/ShareDialog/ShareDialog.tsx @@ -200,9 +200,6 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { const loadShareInfo = async () => { if (!itemToShare?.item) return; - const isItemNotSharedYet = !isAdvanchedShareItem(itemToShare?.item) && !itemToShare.item.sharings?.length; - if (isItemNotSharedYet) return; - setIsLoading(true); // Change object type of itemToShare to AdvancedSharedItem let shareAccessMode: AccessMode = 'public'; @@ -211,13 +208,19 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { const itemType = itemToShare?.item.isFolder ? 'folder' : 'file'; const itemId = itemToShare?.item.uuid ?? ''; - try { - const sharingData = await shareService.getSharingType(itemId, itemType); - sharingType = sharingData.type; - isAlreadyPasswordProtected = sharingData.encryptedPassword !== null; - setSharingMeta(sharingData); - } catch (error) { - errorService.reportError(error); + + const isItemNotSharedYet = + !isAdvanchedShareItem(itemToShare?.item) && !itemToShare.item.sharings?.length && !sharingMeta; + + if (!isItemNotSharedYet) { + try { + const sharingData = await shareService.getSharingType(itemId, itemType); + sharingType = sharingData.type; + isAlreadyPasswordProtected = sharingData.encryptedPassword !== null; + setSharingMeta(sharingData); + } catch (error) { + errorService.reportError(error); + } } if (sharingType === 'private') { @@ -300,11 +303,14 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { trackPublicShared(trackingPublicSharedProperties); const encryptionKey = isAdvanchedShareItem(itemToShare.item) ? itemToShare?.item?.encryptionKey : undefined; - await shareService.getPublicShareLink( + const sharingInfo = await shareService.getPublicShareLink( itemToShare?.item.uuid, itemToShare.item.isFolder ? 'folder' : 'file', encryptionKey, ); + if (sharingInfo) { + setSharingMeta(sharingInfo); + } props.onShareItem?.(); closeSelectedUserPopover(); } @@ -342,19 +348,19 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { }, [isPasswordProtected]); const onSavePublicSharePassword = useCallback( - async (passwordValue: string) => { + async (plainPassword: string) => { try { let sharingInfo = sharingMeta; - if (!sharingInfo) { + if (!sharingInfo?.encryptedCode) { const itemType = itemToShare?.item.isFolder ? 'folder' : 'file'; const itemId = itemToShare?.item.uuid ?? ''; - sharingInfo = await shareService.createPublicShareFromOwnerUser(itemId, itemType); + sharingInfo = await shareService.createPublicShareFromOwnerUser(itemId, itemType, plainPassword); setSharingMeta(sharingInfo); + } else { + await shareService.saveSharingPassword(sharingInfo.id, plainPassword, sharingInfo.encryptedCode); } - const code = shareService.decryptPublicSharingCodeWithOwner(sharingInfo.encryptedCode); - await shareService.saveSharingPassword(sharingInfo.id, code, passwordValue); setIsPasswordProtected(true); } catch (error) { errorService.castError(error); @@ -389,9 +395,10 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { await shareService.updateSharingType(itemId, itemType, sharingType); if (sharingType === 'public') { - await shareService.createPublicShareFromOwnerUser(itemId, itemType); + const shareInfo = await shareService.createPublicShareFromOwnerUser(itemId, itemType); + setSharingMeta(shareInfo); + setIsPasswordProtected(false); } - await loadShareInfo(); setAccessMode(mode); } catch (error) { errorService.reportError(error); diff --git a/src/app/share/services/share.service.ts b/src/app/share/services/share.service.ts index a93fdecaa..747814118 100644 --- a/src/app/share/services/share.service.ts +++ b/src/app/share/services/share.service.ts @@ -371,6 +371,7 @@ export function stopSharingItem(itemType: string, itemId: string): Promise export const createPublicShareFromOwnerUser = async ( uuid: string, itemType: 'folder' | 'file', + plainPassword?: string, encryptionAlgorithm?: string, ): Promise => { const user = localStorageService.getUser() as UserSettings; @@ -379,6 +380,7 @@ export const createPublicShareFromOwnerUser = async ( const encryptedMnemonic = aes.encrypt(mnemonic, code); const encryptedCode = aes.encrypt(code, mnemonic); + const encryptedPassword = plainPassword ? aes.encrypt(plainPassword, code) : null; return createPublicSharingItem({ encryptionAlgorithm: encryptionAlgorithm ?? 'inxt-v2', @@ -387,6 +389,7 @@ export const createPublicShareFromOwnerUser = async ( itemId: uuid, encryptedCode, persistPreviousSharing: true, + ...(encryptedPassword && { encryptedPassword }), }); }; @@ -400,7 +403,7 @@ export const getPublicShareLink = async ( uuid: string, itemType: 'folder' | 'file', encriptedMnemonic?: string, -): Promise => { +): Promise => { const user = localStorageService.getUser() as UserSettings; let { mnemonic } = user; const code = crypto.randomBytes(32).toString('hex'); @@ -424,6 +427,7 @@ export const getPublicShareLink = async ( if (!isCopied) throw Error('Error copying shared public link'); notificationsService.show({ text: t('shared-links.toast.copy-to-clipboard'), type: ToastType.Success }); + return publicSharingItemData; } catch (error) { notificationsService.show({ text: t('modals.shareModal.errors.copy-to-clipboard'), @@ -833,9 +837,16 @@ export function getSharingType(itemId: string, itemType: 'file' | 'folder'): Pro }); } -export function saveSharingPassword(sharingId: string, code: string, password: string): Promise { +export function saveSharingPassword( + sharingId: string, + plainPassword: string, + encryptedCode: string, +): Promise { + const code = shareService.decryptPublicSharingCodeWithOwner(encryptedCode); + const encryptedPassword = aes.encrypt(plainPassword, code); + const shareClient = SdkFactory.getNewApiInstance().createShareClient(); - return shareClient.saveSharingPassword(sharingId, code, password).catch((error) => { + return shareClient.saveSharingPassword(sharingId, encryptedPassword).catch((error) => { throw errorService.castError(error); }); } From 5c21af59bb3b73e62413b41ddd9484ecaeb9aae3 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Tue, 19 Dec 2023 01:24:09 -0400 Subject: [PATCH 06/14] feat: add public shared item info when protected --- package.json | 4 +-- src/app/share/services/share.service.ts | 9 ++++++ .../share/views/ShareView/ShareFileView.tsx | 14 +++++++-- .../share/views/ShareView/ShareFolderView.tsx | 11 +++---- .../views/ShareView/ShareItemPwdView.tsx | 30 ++++++++++++++++--- 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index b6a33438f..46af1d8e9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@iconscout/react-unicons": "^1.1.6", "@internxt/inxt-js": "=1.2.21", "@internxt/lib": "^1.2.0", - "@internxt/sdk": "1.4.59", + "@internxt/sdk": "1.4.61", "@phosphor-icons/react": "^2.0.10", "@popperjs/core": "^2.11.6", "@reduxjs/toolkit": "^1.6.0", @@ -172,4 +172,4 @@ "prettier --write" ] } -} +} \ No newline at end of file diff --git a/src/app/share/services/share.service.ts b/src/app/share/services/share.service.ts index 747814118..db0421739 100644 --- a/src/app/share/services/share.service.ts +++ b/src/app/share/services/share.service.ts @@ -19,6 +19,7 @@ import { SharedFoldersInvitationsAsInvitedUserResponse, CreateSharingPayload, SharingMeta, + PublicSharedItemInfo, } from '@internxt/sdk/dist/drive/share/types'; import { domainManager } from './DomainManager'; import _ from 'lodash'; @@ -333,6 +334,13 @@ export function getUserRoleOfSharedRolder(sharingId: string): Promise { }); } +export function getPublicSharedItemInfo(sharingId: string): Promise { + const shareClient = SdkFactory.getNewApiInstance().createShareClient(); + return shareClient.getPublicSharedItemInfo(sharingId).catch((error) => { + throw errorService.castError(error); + }); +} + export function updateUserRoleOfSharedFolder({ newRoleId, sharingId, @@ -897,6 +905,7 @@ const shareService = { removeSharingPassword, decryptPublicSharingCodeWithOwner, validateSharingInvitation, + getPublicSharedItemInfo, }; export default shareService; diff --git a/src/app/share/views/ShareView/ShareFileView.tsx b/src/app/share/views/ShareView/ShareFileView.tsx index 2ba78dd9f..5deefb21b 100644 --- a/src/app/share/views/ShareView/ShareFileView.tsx +++ b/src/app/share/views/ShareView/ShareFileView.tsx @@ -27,7 +27,7 @@ import SendBanner from './SendBanner'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import { ShareTypes } from '@internxt/sdk/dist/drive'; import errorService from 'app/core/services/error.service'; -import { SharingMeta } from '@internxt/sdk/dist/drive/share/types'; +import { PublicSharedItemInfo, SharingMeta } from '@internxt/sdk/dist/drive/share/types'; import Button from '../../../shared/components/Button/Button'; export interface ShareViewProps extends ShareViewState { @@ -61,6 +61,7 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element { const [blobProgress, setBlobProgress] = useState(TaskProgress.Min); const [isDownloading, setIsDownloading] = useState(false); const [info, setInfo] = useState>({}); + const [itemData, setItemData] = useState(); const [isLoaded, setIsLoaded] = useState(false); const [isError, setIsError] = useState(false); const [openPreview, setOpenPreview] = useState(false); @@ -133,8 +134,10 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element { name: res.item.plainName, }); }) - .catch((err) => { + .catch(async (err) => { if (err.message === 'Forbidden') { + const itemData = await shareService.getPublicSharedItemInfo(sharingId); + setItemData(itemData); setRequiresPassword(true); setIsLoaded(true); } @@ -253,7 +256,12 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element { const FileIcon = iconService.getItemIcon(false, info?.item?.type); body = requiresPassword ? ( - + ) : ( <> {/* File info */} diff --git a/src/app/share/views/ShareView/ShareFolderView.tsx b/src/app/share/views/ShareView/ShareFolderView.tsx index 30df36694..f661c6c07 100644 --- a/src/app/share/views/ShareView/ShareFolderView.tsx +++ b/src/app/share/views/ShareView/ShareFolderView.tsx @@ -17,17 +17,14 @@ import UilImport from '@iconscout/react-unicons/icons/uil-import'; import './ShareView.scss'; import { ShareTypes } from '@internxt/sdk/dist/drive'; import Spinner from '../../../shared/components/Spinner/Spinner'; -import { SharingMeta } from '@internxt/sdk/dist/drive/share/types'; +import { PublicSharedItemInfo, SharingMeta } from '@internxt/sdk/dist/drive/share/types'; import shareService from 'app/share/services/share.service'; -import { downloadSharedFolderUsingReadableStream } from 'app/drive/services/download.service/downloadFolder/downloadSharedFolderUsingReadableStream'; -import { downloadSharedFolderUsingBlobs } from 'app/drive/services/download.service/downloadFolder/downloadSharedFolderUsingBlobs'; import { loadWritableStreamPonyfill } from 'app/network/download'; import ShareItemPwdView from './ShareItemPwdView'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import errorService from 'app/core/services/error.service'; import SendBanner from './SendBanner'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; -import ReportButton from './ReportButon'; interface ShareViewProps extends ShareViewState { match: match<{ @@ -56,6 +53,7 @@ export default function ShareFolderView(props: ShareViewProps): JSX.Element { const [progress, setProgress] = useState(TaskProgress.Min); const [isDownloading, setIsDownloading] = useState(false); const [info, setInfo] = useState>({}); + const [itemData, setItemData] = useState(); const [size, setSize] = useState(null); const [isLoaded, setIsLoaded] = useState(false); const [isError, setIsError] = useState(false); @@ -121,8 +119,10 @@ export default function ShareFolderView(props: ShareViewProps): JSX.Element { .then((folderSize) => { setSize(folderSize); }) - .catch((err) => { + .catch(async (err) => { if (err.message === 'Forbidden') { + const itemData = await shareService.getPublicSharedItemInfo(sharingId); + setItemData(itemData); setRequiresPassword(true); setIsLoaded(true); } @@ -244,6 +244,7 @@ export default function ShareFolderView(props: ShareViewProps): JSX.Element { onPasswordSubmitted={loadFolderInfo} itemPassword={itemPassword} setItemPassword={setItemPassword} + itemData={itemData} /> ) : ( //WITHOUT PASSWORD diff --git a/src/app/share/views/ShareView/ShareItemPwdView.tsx b/src/app/share/views/ShareView/ShareItemPwdView.tsx index 6d5731d96..93ea33445 100644 --- a/src/app/share/views/ShareView/ShareItemPwdView.tsx +++ b/src/app/share/views/ShareView/ShareItemPwdView.tsx @@ -5,18 +5,23 @@ import { ReactComponent as LockLogo } from 'assets/icons/Lock.svg'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import errorService from 'app/core/services/error.service'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import iconService from 'app/drive/services/icon.service'; +import sizeService from 'app/drive/services/size.service'; +import transformItemService from 'app/drive/services/item-transform.service'; +import { DriveItemData } from 'app/drive/types'; export interface ShareItemPwdViewProps { onPasswordSubmitted: (password: string) => Promise; itemPassword: string; setItemPassword: (password: string) => void; + itemData?: { plainName: string; size: number; type?: string }; } const ShareItemPwdView = (props: ShareItemPwdViewProps) => { const { translate } = useTranslationContext(); const { onPasswordSubmitted, setItemPassword, itemPassword } = props; const [onPasswordError, setOnPasswordError] = useState(false); - + const Icon = props.itemData ? iconService.getItemIcon(props.itemData.type === 'folder', props.itemData.type) : null; if (!onPasswordError) { setTimeout(() => setOnPasswordError(false), 6000); } @@ -27,9 +32,9 @@ const ShareItemPwdView = (props: ShareItemPwdViewProps) => { } return ( -
+
{/*
*/} -
+

{translate('shareItemPwdView.title')}

@@ -38,8 +43,25 @@ const ShareItemPwdView = (props: ShareItemPwdViewProps) => { {translate('shareItemPwdView.putPwd1')}

+ {props.itemData && Icon !== null && ( +
+
+
+ +
+
+
+
+ {transformItemService.getItemPlainNameWithExtension(props.itemData as DriveItemData)} +
+
+ {sizeService.bytesToString(props.itemData?.size ?? 0)} +
+
+
+ )} {/*
*/} -
+

{translate('shareItemPwdView.password')}

Date: Tue, 19 Dec 2023 07:59:01 -0400 Subject: [PATCH 07/14] chore: small refactor and text change to align with ticket --- src/app/i18n/locales/cn.json | 1 - src/app/i18n/locales/de.json | 1 - src/app/i18n/locales/en.json | 3 +-- src/app/i18n/locales/es.json | 1 - src/app/i18n/locales/fr.json | 1 - src/app/i18n/locales/it.json | 3 +-- src/app/i18n/locales/ru.json | 1 - .../SharePasswordInputDialog.tsx | 6 ++++-- src/app/share/views/ShareView/ShareFileView.tsx | 13 ++++++++++--- src/app/share/views/ShareView/ShareFolderView.tsx | 12 ++++++++++-- 10 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/app/i18n/locales/cn.json b/src/app/i18n/locales/cn.json index 794c4b753..a7f1ebb83 100644 --- a/src/app/i18n/locales/cn.json +++ b/src/app/i18n/locales/cn.json @@ -761,7 +761,6 @@ "protect": "使用密码保护", "disablePasswordTitle": "禁用密码保护?", "disablePasswordBody": "禁用后,拥有链接的人将能够访问内容。", - "createPasswordTitle": "创建密码", "editPasswordTitle": "编辑密码", "buttons": { "cancel": "取消", diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 17855b653..7bf7cd42a 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -657,7 +657,6 @@ "protect": "Mit Passwort schützen", "disablePasswordTitle": "Passwortschutz deaktivieren?", "disablePasswordBody": "Wenn deaktiviert, können Personen mit dem Link auf den Inhalt zugreifen.", - "createPasswordTitle": "Passwort erstellen", "editPasswordTitle": "Passwort bearbeiten", "buttons": { "cancel": "Abbrechen", diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 7c952f780..a76310971 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -234,7 +234,7 @@ "putPwd": "Please enter the password provided", "putPwd1": "by the sender for access.", "password": "Password", - "pwdLayout": "password", + "pwdLayout": "Password", "access": "Access" }, "sideNav": { @@ -774,7 +774,6 @@ "protect": "Protect with password", "disablePasswordTitle": "Disable password protection?", "disablePasswordBody": "When disabled, people with the link will be able to access the content.", - "createPasswordTitle": "Create passsword", "editPasswordTitle": "Edit password", "buttons": { "cancel": "Cancel", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index d808b62c1..35a2659a0 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -756,7 +756,6 @@ "protect": "Proteger con contraseña", "disablePasswordTitle": "¿Desactivar la protección con contraseña?", "disablePasswordBody": "Cuando está desactivada, las personas con el enlace podrán acceder al contenido.", - "createPasswordTitle": "Crear contraseña", "editPasswordTitle": "Editar contraseña", "buttons": { "cancel": "Cancelar", diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index 7ebe9f345..ee9d16cd7 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -689,7 +689,6 @@ "protect": "Protéger par mot de passe", "disablePasswordTitle": "Désactiver la protection par mot de passe ?", "disablePasswordBody": "Lorsqu'elle est désactivée, les personnes disposant du lien pourront accéder au contenu.", - "createPasswordTitle": "Créer un mot de passe", "editPasswordTitle": "Modifier le mot de passe", "buttons": { "cancel": "Annuler", diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index eb7489874..4ec3bef04 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -202,7 +202,7 @@ "putPwd": "Per accedere, inserisci la password", "putPwd1": " fornita dal mittente.", "password": "Password", - "pwdLayout": "password", + "pwdLayout": "Password", "access": "Login" }, "sideNav": { @@ -735,7 +735,6 @@ "protect": "Proteggi con password", "disablePasswordTitle": "Disabilitare la protezione con password?", "disablePasswordBody": "Quando disabilitato, le persone con il link potranno accedere al contenuto.", - "createPasswordTitle": "Crea password", "editPasswordTitle": "Modifica password", "buttons": { "cancel": "Annulla", diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index 5fdac9ee6..c0031ec0a 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -716,7 +716,6 @@ "protect": "Защитить паролем", "disablePasswordTitle": "Отключить защиту паролем?", "disablePasswordBody": "При отключении люди с ссылкой смогут получить доступ к содержимому.", - "createPasswordTitle": "Создать пароль", "editPasswordTitle": "Изменить пароль", "buttons": { "cancel": "Отмена", diff --git a/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx b/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx index c5ece8a1f..0638b7585 100644 --- a/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx +++ b/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx @@ -12,6 +12,8 @@ type SharePasswordInputDialogProps = { isAlreadyProtected?: boolean; }; +const MAX_PASSWORD_LENGTH = 50; + export const SharePasswordInputDialog = ({ isOpen, onClose, @@ -32,12 +34,12 @@ export const SharePasswordInputDialog = ({

{!isAlreadyProtected - ? translate('modals.shareModal.protectSharingModal.createPasswordTitle') + ? translate('modals.shareModal.protectSharingModal.protect') : translate('modals.shareModal.protectSharingModal.editPasswordTitle')}

{ - if (value.length <= 50) { + if (value.length <= MAX_PASSWORD_LENGTH) { setPassword(value); } }} diff --git a/src/app/share/views/ShareView/ShareFileView.tsx b/src/app/share/views/ShareView/ShareFileView.tsx index 5deefb21b..f34e60ce3 100644 --- a/src/app/share/views/ShareView/ShareFileView.tsx +++ b/src/app/share/views/ShareView/ShareFileView.tsx @@ -136,16 +136,23 @@ export default function ShareFileView(props: ShareViewProps): JSX.Element { }) .catch(async (err) => { if (err.message === 'Forbidden') { - const itemData = await shareService.getPublicSharedItemInfo(sharingId); - setItemData(itemData); + await getSharedItemInfo(sharingId); setRequiresPassword(true); setIsLoaded(true); } - throw err; }); } + const getSharedItemInfo = async (id: string) => { + try { + const itemData = await shareService.getPublicSharedItemInfo(id); + setItemData(itemData); + } catch (error) { + errorService.reportError(error); + } + }; + function getBlob(abortController: AbortController): Promise { const fileInfo = info as unknown as ShareTypes.ShareLink; diff --git a/src/app/share/views/ShareView/ShareFolderView.tsx b/src/app/share/views/ShareView/ShareFolderView.tsx index f661c6c07..b816d9cfa 100644 --- a/src/app/share/views/ShareView/ShareFolderView.tsx +++ b/src/app/share/views/ShareView/ShareFolderView.tsx @@ -121,8 +121,7 @@ export default function ShareFolderView(props: ShareViewProps): JSX.Element { }) .catch(async (err) => { if (err.message === 'Forbidden') { - const itemData = await shareService.getPublicSharedItemInfo(sharingId); - setItemData(itemData); + await getSharedFolderInfo(sharingId); setRequiresPassword(true); setIsLoaded(true); } @@ -130,6 +129,15 @@ export default function ShareFolderView(props: ShareViewProps): JSX.Element { }); } + const getSharedFolderInfo = async (id: string) => { + try { + const itemData = await shareService.getPublicSharedItemInfo(id); + setItemData(itemData); + } catch (error) { + errorService.reportError(error); + } + }; + const loadSize = (shareId: number, folderId: number): Promise => { return getSharedFolderSize(shareId.toString(), folderId.toString()); }; From 0986865977e65847074d0896b90a8c541de5ad16 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Thu, 21 Dec 2023 16:09:56 +0100 Subject: [PATCH 08/14] Updated yarn lock --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1c958ea58..676977fce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1605,10 +1605,10 @@ resolved "https://npm.pkg.github.com/download/@internxt/prettier-config/1.0.2/5bd220b8de76734448db5475b3e0c01f9d22c19b#5bd220b8de76734448db5475b3e0c01f9d22c19b" integrity sha512-t4HiqvCbC7XgQepwWlIaFJe3iwW7HCf6xOSU9nKTV0tiGqOPz7xMtIgLEloQrDA34Cx4PkOYBXrvFPV6RxSFAA== -"@internxt/sdk@1.4.59": - version "1.4.59" - resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.4.59/879fe7c663a48083c97cc2d1b8672635b59e9cd6#879fe7c663a48083c97cc2d1b8672635b59e9cd6" - integrity sha512-pcbIIQtY1mFd+QJCIpppsKnors6aVHifJq8v2xHha7TORyH4qlNGihSfWo+wzV0JSc2qMO/ty7KLuu4ASQkTkQ== +"@internxt/sdk@1.4.61": + version "1.4.61" + resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.4.61/4d6bb4815a7e128b88b9d22aad80bdbaad4c8739#4d6bb4815a7e128b88b9d22aad80bdbaad4c8739" + integrity sha512-K4rKKowIPO1YmKoag5m0NxbvHFX0WuqVOqEiVSvDeyEyxbJpzbNnVnZvJ6bZo/zV7rWtWJJFQheE8f5Bh8dC6g== dependencies: axios "^0.24.0" query-string "^7.1.0" From af59f75ac773861a60cf4dcc0b97aeed2c9203d7 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 21 Dec 2023 12:01:52 -0400 Subject: [PATCH 09/14] fix: added password to get shared folders meta call --- src/app/share/views/ShareView/ShareFolderView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/share/views/ShareView/ShareFolderView.tsx b/src/app/share/views/ShareView/ShareFolderView.tsx index b014da5e5..be9387ce9 100644 --- a/src/app/share/views/ShareView/ShareFolderView.tsx +++ b/src/app/share/views/ShareView/ShareFolderView.tsx @@ -105,7 +105,7 @@ export default function ShareFolderView(props: ShareViewProps): JSX.Element { throw new Error(CHROME_IOS_ERROR_MESSAGE); } - return getPublicSharingMeta(sharingId, code) + return getPublicSharingMeta(sharingId, code, password) .then((res) => { setInfo({ ...res }); setIsLoaded(true); From 095534a02e7e63bfe9ab9e66aad99b5bf4d82868 Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 21 Dec 2023 21:58:47 -0400 Subject: [PATCH 10/14] fix: wrong password message now is shown correctly --- .../components/ShareDialog/ShareDialog.tsx | 2 +- .../views/ShareView/ShareItemPwdView.tsx | 50 ++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/app/drive/components/ShareDialog/ShareDialog.tsx b/src/app/drive/components/ShareDialog/ShareDialog.tsx index 6241a258a..6c985169f 100644 --- a/src/app/drive/components/ShareDialog/ShareDialog.tsx +++ b/src/app/drive/components/ShareDialog/ShareDialog.tsx @@ -158,7 +158,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { useEffect(() => { if (roles.length === 0) dispatch(sharedThunks.getSharedFolderRoles()); - if (roles.length > 0) loadShareInfo(); + if (roles.length > 0 && isOpen) loadShareInfo(); }, [roles, isOpen]); useEffect(() => { diff --git a/src/app/share/views/ShareView/ShareItemPwdView.tsx b/src/app/share/views/ShareView/ShareItemPwdView.tsx index 93ea33445..970479c1a 100644 --- a/src/app/share/views/ShareView/ShareItemPwdView.tsx +++ b/src/app/share/views/ShareView/ShareItemPwdView.tsx @@ -9,6 +9,8 @@ import iconService from 'app/drive/services/icon.service'; import sizeService from 'app/drive/services/size.service'; import transformItemService from 'app/drive/services/item-transform.service'; import { DriveItemData } from 'app/drive/types'; +import AppError from 'app/core/types'; +import Button from 'app/shared/components/Button/Button'; export interface ShareItemPwdViewProps { onPasswordSubmitted: (password: string) => Promise; @@ -21,16 +23,37 @@ const ShareItemPwdView = (props: ShareItemPwdViewProps) => { const { translate } = useTranslationContext(); const { onPasswordSubmitted, setItemPassword, itemPassword } = props; const [onPasswordError, setOnPasswordError] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const Icon = props.itemData ? iconService.getItemIcon(props.itemData.type === 'folder', props.itemData.type) : null; - if (!onPasswordError) { - setTimeout(() => setOnPasswordError(false), 6000); - } function handleChange(pwd) { const value = pwd.target.value; setItemPassword(value); } + const handlePasswordSubmit = async () => { + try { + setIsSubmitting(true); + await onPasswordSubmitted(itemPassword); + } catch (error) { + if (error instanceof AppError) { + const { statusCode } = JSON.parse(error.message); + if (statusCode === 403) { + setOnPasswordError(true); + } else { + errorService.reportError(error); + notificationsService.show({ + text: errorService.castError(error).message, + type: ToastType.Warning, + duration: 50000, + }); + } + } + } finally { + setIsSubmitting(false); + } + }; + return (
{/*
*/} @@ -75,26 +98,17 @@ const ShareItemPwdView = (props: ShareItemPwdViewProps) => {

{translate('error.wrongPassword')}

)} - + {translate('shareItemPwdView.access')} +
); From 3b0c25447bb4c4c45c587ffac88c22a12f4d6f7d Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 28 Dec 2023 10:20:28 -0400 Subject: [PATCH 11/14] fix: text --- src/app/drive/components/ShareDialog/ShareDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/drive/components/ShareDialog/ShareDialog.tsx b/src/app/drive/components/ShareDialog/ShareDialog.tsx index 6c985169f..6e00e49f9 100644 --- a/src/app/drive/components/ShareDialog/ShareDialog.tsx +++ b/src/app/drive/components/ShareDialog/ShareDialog.tsx @@ -578,7 +578,7 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => {

- {translate('modals.shareModal.protectSharingModal.buttons.changePassword')} + {translate('modals.shareModal.protectSharingModal.protect')}

Date: Wed, 3 Jan 2024 09:17:35 -0400 Subject: [PATCH 12/14] fix: add encodeURIComponent for x-shared-password --- src/app/share/views/ShareView/ShareItemPwdView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/share/views/ShareView/ShareItemPwdView.tsx b/src/app/share/views/ShareView/ShareItemPwdView.tsx index 970479c1a..91963b3f5 100644 --- a/src/app/share/views/ShareView/ShareItemPwdView.tsx +++ b/src/app/share/views/ShareView/ShareItemPwdView.tsx @@ -34,7 +34,8 @@ const ShareItemPwdView = (props: ShareItemPwdViewProps) => { const handlePasswordSubmit = async () => { try { setIsSubmitting(true); - await onPasswordSubmitted(itemPassword); + const encodedPassword = encodeURIComponent(itemPassword); + await onPasswordSubmitted(encodedPassword); } catch (error) { if (error instanceof AppError) { const { statusCode } = JSON.parse(error.message); From bdaeff8c951f8d5ee1a2e5c206e93407ec8578ff Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 4 Jan 2024 17:02:14 -0400 Subject: [PATCH 13/14] fix: add password characters validation --- src/app/core/services/validation.service.ts | 7 +++++++ .../SharePasswordInputDialog/SharePasswordInputDialog.tsx | 3 ++- src/app/share/views/ShareView/ShareItemPwdView.tsx | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/core/services/validation.service.ts b/src/app/core/services/validation.service.ts index 39aab00b7..eacab777c 100644 --- a/src/app/core/services/validation.service.ts +++ b/src/app/core/services/validation.service.ts @@ -12,9 +12,16 @@ const validateSearchText = (value: string): boolean => { return alphanumericDotsAndSpaces.test(value); }; +const validatePasswordInput = (value: string): boolean => { + const latinAlphabetAndSymbols = /^[a-zA-Z0-9 ~`!@#$%^&*()_\-+={[}\]|\\:;"'<,>.?/]*$/gm; + + return latinAlphabetAndSymbols.test(value); +}; + const validationService = { validate2FA, validateSearchText, + validatePasswordInput, }; export default validationService; diff --git a/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx b/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx index 0638b7585..8c4f5fd5a 100644 --- a/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx +++ b/src/app/share/components/SharePasswordInputDialog/SharePasswordInputDialog.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { Spinner } from '@phosphor-icons/react'; import Input from 'app/shared/components/Input'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import validationService from 'app/core/services/validation.service'; type SharePasswordInputDialogProps = { isOpen: boolean; @@ -39,7 +40,7 @@ export const SharePasswordInputDialog = ({

{ - if (value.length <= MAX_PASSWORD_LENGTH) { + if (value.length <= MAX_PASSWORD_LENGTH && validationService.validatePasswordInput(value)) { setPassword(value); } }} diff --git a/src/app/share/views/ShareView/ShareItemPwdView.tsx b/src/app/share/views/ShareView/ShareItemPwdView.tsx index 91963b3f5..0c3117c3f 100644 --- a/src/app/share/views/ShareView/ShareItemPwdView.tsx +++ b/src/app/share/views/ShareView/ShareItemPwdView.tsx @@ -11,6 +11,7 @@ import transformItemService from 'app/drive/services/item-transform.service'; import { DriveItemData } from 'app/drive/types'; import AppError from 'app/core/types'; import Button from 'app/shared/components/Button/Button'; +import validationService from 'app/core/services/validation.service'; export interface ShareItemPwdViewProps { onPasswordSubmitted: (password: string) => Promise; @@ -28,7 +29,9 @@ const ShareItemPwdView = (props: ShareItemPwdViewProps) => { function handleChange(pwd) { const value = pwd.target.value; - setItemPassword(value); + if (validationService.validatePasswordInput(value)) { + setItemPassword(value); + } } const handlePasswordSubmit = async () => { From dd2fa7140f6a2691062f295b1387beaf1c7073bd Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Fri, 5 Jan 2024 08:44:42 -0400 Subject: [PATCH 14/14] =?UTF-8?q?chore:=20add=20=C3=91=20to=20supported=20?= =?UTF-8?q?letters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/core/services/validation.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/services/validation.service.ts b/src/app/core/services/validation.service.ts index eacab777c..714febcfb 100644 --- a/src/app/core/services/validation.service.ts +++ b/src/app/core/services/validation.service.ts @@ -13,7 +13,7 @@ const validateSearchText = (value: string): boolean => { }; const validatePasswordInput = (value: string): boolean => { - const latinAlphabetAndSymbols = /^[a-zA-Z0-9 ~`!@#$%^&*()_\-+={[}\]|\\:;"'<,>.?/]*$/gm; + const latinAlphabetAndSymbols = /^[a-zA-Z0-9ñÑ ~`!@#$%^&*()_\-+={[}\]|\\:;"'<,>.?/]*$/gm; return latinAlphabetAndSymbols.test(value); };