From 4fade93b431eb16957c445b47bcbddbb68fb791a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sun, 19 Mar 2023 15:51:04 +0530 Subject: [PATCH 01/77] Fix compilation --- src/utils/strings/frenchConstants.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/strings/frenchConstants.tsx b/src/utils/strings/frenchConstants.tsx index e1f236cc7e..a073dd56f1 100644 --- a/src/utils/strings/frenchConstants.tsx +++ b/src/utils/strings/frenchConstants.tsx @@ -553,7 +553,7 @@ const frenchConstants = { LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE: 'Votre navigateur ou un complément bloque ente qui ne peut sauvegarder les données sur votre stockage local. Veuillez relancer cette page après avoir changé de mode de navigation.', RETRY: 'Réessayer', - SEND_OTT: 'Envoyer l'OTP', + SEND_OTT: "Envoyer l'OTP", EMAIl_ALREADY_OWNED: 'Cet e-mail est déjà pris', EMAIL_UDPATE_SUCCESSFUL: 'Votre e-mail a été mis à jour', UPLOAD_FAILED: 'Échec du chargement', From 6da5198c749f9d1679be6eda0589cd3187c090eb Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sun, 19 Mar 2023 15:54:23 +0530 Subject: [PATCH 02/77] Add TimerProgress componenet --- src/components/Authenicator/TimerProgress.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/components/Authenicator/TimerProgress.tsx diff --git a/src/components/Authenicator/TimerProgress.tsx b/src/components/Authenicator/TimerProgress.tsx new file mode 100644 index 0000000000..c9234f4acf --- /dev/null +++ b/src/components/Authenicator/TimerProgress.tsx @@ -0,0 +1,46 @@ +import React, { useState, useEffect } from 'react'; + +const TimerProgress = ({ period }) => { + const [progress, setProgress] = useState(0); + const [ticker, setTicker] = useState(null); + const microSecondsInPeriod = period * 1000000; + + const startTicker = () => { + const ticker = setInterval(() => { + updateTimeRemaining(); + }, 10); + setTicker(ticker); + }; + + const updateTimeRemaining = () => { + const timeRemaining = + microSecondsInPeriod - + ((new Date().getTime() * 1000) % microSecondsInPeriod); + setProgress(timeRemaining / microSecondsInPeriod); + }; + + useEffect(() => { + startTicker(); + return () => clearInterval(ticker); + }, []); + + const color = progress > 0.4 ? 'green' : 'orange'; + + return ( +
+
+
+ ); +}; + +export default TimerProgress; From 54ee00574a6c38fe1a2318f80cf69bfade6077f3 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sun, 19 Mar 2023 15:59:20 +0530 Subject: [PATCH 03/77] Add OTPDisplay component --- src/components/Authenicator/OTPDisplay.tsx | 145 +++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/components/Authenicator/OTPDisplay.tsx diff --git a/src/components/Authenicator/OTPDisplay.tsx b/src/components/Authenicator/OTPDisplay.tsx new file mode 100644 index 0000000000..b0c1faec38 --- /dev/null +++ b/src/components/Authenicator/OTPDisplay.tsx @@ -0,0 +1,145 @@ +import React, { useState, useEffect } from 'react'; +import { TOTP, HOTP } from 'otpauth'; +import TimerProgress from './TimerProgress'; + +const TOTPDisplay = ({ issuer, account, code, nextCode }) => { + return ( +
+
+

+ {issuer} +

+

+ {account} +

+

+ {code} +

+
+
+
+

+ next +

+

+ {nextCode} +

+
+
+ ); +}; + +const OTPDisplay = ({ + secret, + type, + algorithm, + timePeriod, + issuer, + account, +}) => { + const [code, setCode] = useState(''); + const [nextcode, setNextCode] = useState(''); + + const generateCodes = () => { + const currentTime = new Date().getTime(); + if (type.toLowerCase() === 'totp') { + const totp = new TOTP({ + secret, + algorithm, + period: timePeriod ?? 30, + }); + setCode(totp.generate()); + setNextCode( + totp.generate({ timestamp: currentTime + timePeriod * 1000 }) + ); + } else if (type.toLowerCase() === 'hotp') { + const hotp = new HOTP({ secret, counter: 0, algorithm }); + setCode(hotp.generate()); + setNextCode(hotp.generate({ counter: 1 })); + } + }; + + useEffect(() => { + let intervalId; + // compare case insensitive type + + if (type.toLowerCase() === 'totp') { + intervalId = setInterval(() => { + generateCodes(); + }, 1000); + } else if (type.toLowerCase() === 'hotp') { + intervalId = setInterval(() => { + generateCodes(); + }, 1000); + } + + return () => clearInterval(intervalId); + }, [secret, type, algorithm, timePeriod]); + + return ( +
+ + +
+ ); +}; + +export default OTPDisplay; From 1abfd2ab2b4570eb87beadffcbfada82ba2c9fdf Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sun, 19 Mar 2023 16:00:00 +0530 Subject: [PATCH 04/77] Add dependency on otpauth and vs-uri --- package.json | 2 ++ yarn.lock | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4154522808..0065d48c7c 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "ml-matrix": "^6.8.2", "next": "^13.1.2", "next-transpile-modules": "^10.0.0", + "otpauth": "^9.0.2", "p-queue": "^7.1.0", "photoswipe": "file:./thirdparty/photoswipe", "piexifjs": "^1.0.6", @@ -72,6 +73,7 @@ "similarity-transformation": "^0.0.1", "styled-components": "^5.3.5", "transformation-matrix": "^2.10.0", + "vscode-uri": "^3.0.7", "workbox-precaching": "^6.1.5", "workbox-recipes": "^6.1.5", "workbox-routing": "^6.1.5", diff --git a/yarn.lock b/yarn.lock index e23f97854e..35ce5a83de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3007,6 +3007,11 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +jssha@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/jssha/-/jssha-3.3.0.tgz#44b5531bcf55a12f4a388476c647a9a1cca92839" + integrity sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w== + "jsx-ast-utils@^2.4.1 || ^3.0.0": version "3.2.0" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz" @@ -3516,6 +3521,13 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +otpauth@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/otpauth/-/otpauth-9.0.2.tgz#5f369bdeb74513fb6c0f25c5ae5ac6b3780ebc89" + integrity sha512-0TzpkJYg24VvIK3/K91HKpTtMlwm73UoThhcGY8fZsXcwHDrqf008rfdOjj3NnQuyuT11+vHyyO//qRzi6OZ9A== + dependencies: + jssha "~3.3.0" + p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -4426,7 +4438,6 @@ tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" @@ -4607,6 +4618,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +vscode-uri@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" + integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== + warning@^4.0.0, warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz" From 50e375ebdf8173a9b31cb8c517c6d6b1a05ae3d0 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sun, 19 Mar 2023 16:05:10 +0530 Subject: [PATCH 05/77] Add ente Authenticator page with dummy data --- src/components/Sidebar/UtilitySection.tsx | 7 ++ src/constants/pages/index.ts | 1 + src/pages/authenticator/index.tsx | 130 ++++++++++++++++++++++ src/utils/strings/englishConstants.tsx | 1 + 4 files changed, 139 insertions(+) create mode 100644 src/pages/authenticator/index.tsx diff --git a/src/components/Sidebar/UtilitySection.tsx b/src/components/Sidebar/UtilitySection.tsx index 44f6f10318..f797b92cb1 100644 --- a/src/components/Sidebar/UtilitySection.tsx +++ b/src/components/Sidebar/UtilitySection.tsx @@ -62,6 +62,8 @@ export default function UtilitySection({ closeSidebar }) { const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE); + const redirectToAuthenticatorPage = () => router.push(PAGES.AUTHENICATOR); + const somethingWentWrong = () => setDialogMessage({ title: constants.ERROR, @@ -97,6 +99,11 @@ export default function UtilitySection({ closeSidebar }) { {constants.DEDUPLICATE_FILES} + {isInternalUser() && ( + + {constants.AUTHENTICATOR_SECTION} + + )} {constants.PREFERENCES} diff --git a/src/constants/pages/index.ts b/src/constants/pages/index.ts index e8612b3fdc..38f2b3a6a5 100644 --- a/src/constants/pages/index.ts +++ b/src/constants/pages/index.ts @@ -14,4 +14,5 @@ export enum PAGES { SHARED_ALBUMS = '/shared-albums', // ML_DEBUG = '/ml-debug', DEDUPLICATE = '/deduplicate', + AUTHENICATOR = '/authenticator', } diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx new file mode 100644 index 0000000000..40421483a1 --- /dev/null +++ b/src/pages/authenticator/index.tsx @@ -0,0 +1,130 @@ +import React, { useEffect, useState } from 'react'; +import OTPDisplay from 'components/Authenicator/OTPDisplay'; +const random = [ + { + issuer: 'Google', + account: 'example@gmail.com', + secret: '6GJ2E2RQKJ36BY6A', + type: 'TOTP', + algorithm: 'SHA1', + period: 30, + }, + { + issuer: 'Facebook', + account: 'example@gmail.com', + secret: 'RVZJ7N6KJKJGQ2VX', + type: 'TOTP', + algorithm: 'SHA256', + period: 60, + }, + { + issuer: 'Twitter', + account: 'example@gmail.com', + secret: 'ZPUE6KJ3WGZ3HPKJ', + type: 'TOTP', + algorithm: 'SHA256', + period: 60, + }, + { + issuer: 'GitHub', + account: 'example@gmail.com', + secret: 'AG6U5KJYHPRRNRZI', + type: 'TOTP', + algorithm: 'SHA1', + period: 30, + }, + { + issuer: 'Amazon', + account: 'example@gmail.com', + secret: 'Q2FR2KJVKJFFKMWZ', + type: 'TOTP', + algorithm: 'SHA256', + period: 60, + }, + { + issuer: 'LinkedIn', + account: 'example@gmail.com', + secret: 'SWRG4KJ4J3LNDW2Z', + type: 'TOTP', + algorithm: 'SHA256', + period: 60, + }, + { + issuer: 'Dropbox', + account: 'example@gmail.com', + secret: 'G5U6OKJU3JRM72ZK', + type: 'TOTP', + algorithm: 'SHA1', + period: 30, + }, +]; + +const OTPPage = () => { + const [secrets, setSecrets] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + const fetchSecrets = async () => { + try { + setSecrets(random); + } catch (error) { + console.error(error); + } + }; + fetchSecrets(); + }, []); + + const filteredSecrets = secrets.filter( + (secret) => + secret.issuer.toLowerCase().includes(searchTerm.toLowerCase()) || + secret.account.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + // center the page +
+

ente Authenticator

+ setSearchTerm(e.target.value)} + /> + +
+ {filteredSecrets.length === 0 ? ( +
+

No results found.

+ {/*

Add a new secret to get started.

+

Download ente auth mobile app to manage your secrets

*/} +
+ ) : ( + filteredSecrets.map((secret) => ( + + )) + )} +
+ ); +}; + +export default OTPPage; diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index a59c44932c..7d1c999654 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -800,6 +800,7 @@ const englishConstants = { UPLOAD_GOOGLE_TAKEOUT: 'Google takeout', CANCEL_UPLOADS: 'Cancel uploads', DEDUPLICATE_FILES: 'Deduplicate files', + AUTHENTICATOR_SECTION: 'Authenticator', NO_DUPLICATES_FOUND: "You've no duplicate files that can be cleared", CLUB_BY_CAPTURE_TIME: 'Club by capture time', FILES: 'Files', From 4faa05a465afff422ff2731ac95da7466ee7cf43 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 09:09:20 +0530 Subject: [PATCH 06/77] [ente Authenticator] Add types --- src/types/authenticator/auth_entity.ts | 23 ++++ src/types/authenticator/code.ts | 166 +++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 src/types/authenticator/auth_entity.ts create mode 100644 src/types/authenticator/code.ts diff --git a/src/types/authenticator/auth_entity.ts b/src/types/authenticator/auth_entity.ts new file mode 100644 index 0000000000..083651d310 --- /dev/null +++ b/src/types/authenticator/auth_entity.ts @@ -0,0 +1,23 @@ +export class AuthEntity { + id: string; + encryptedData: string | null; + header: string | null; + isDeleted: boolean; + createdAt: number; + updatedAt: number; + constructor( + id: string, + encryptedData: string | null, + header: string | null, + isDeleted: boolean, + createdAt: number, + updatedAt: number + ) { + this.id = id; + this.encryptedData = encryptedData; + this.header = header; + this.isDeleted = isDeleted; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } +} diff --git a/src/types/authenticator/code.ts b/src/types/authenticator/code.ts new file mode 100644 index 0000000000..8e94d7b45d --- /dev/null +++ b/src/types/authenticator/code.ts @@ -0,0 +1,166 @@ +import { URI } from 'vscode-uri'; + +type Type = 'totp' | 'TOTP' | 'hotp' | 'HOTP'; + +type AlgorithmType = + | 'sha1' + | 'SHA1' + | 'sha256' + | 'SHA256' + | 'sha512' + | 'SHA512'; + +export class Code { + static readonly defaultDigits = 6; + static readonly defaultPeriod = 30; + + // id for the corresponding auth entity + id?: String; + account: string; + issuer: string; + digits?: number; + period: number; + secret: string; + algorithm: AlgorithmType; + type: Type; + rawData?: string; + + constructor( + account: string, + issuer: string, + digits: number | undefined, + period: number, + secret: string, + algorithm: AlgorithmType, + type: Type, + rawData?: string, + id?: string + ) { + this.account = account; + this.issuer = issuer; + this.digits = digits; + this.period = period; + this.secret = secret; + this.algorithm = algorithm; + this.type = type; + this.rawData = rawData; + this.id = id; + } + + static fromRawData(id: string, rawData: string): Code { + let santizedRawData = rawData + .replace(/\+/g, '%2B') + .replace(/:/g, '%3A'); + if (santizedRawData.startsWith('"')) { + santizedRawData = santizedRawData.substring(1); + } + if (santizedRawData.endsWith('"')) { + santizedRawData = santizedRawData.substring( + 0, + santizedRawData.length - 1 + ); + } + + const uriParams = {}; + const searchParamsString = + decodeURIComponent(santizedRawData).split('?')[1]; + searchParamsString.split('&').forEach((pair) => { + const [key, value] = pair.split('='); + uriParams[key] = value; + }); + + const uri = URI.parse(santizedRawData); + let uriPath = uri.path; + if ( + uriPath.startsWith('/otpauth://') || + uriPath.startsWith('otpauth://') + ) { + uriPath = uriPath.split('otpauth://')[1]; + } + + return new Code( + Code._getAccount(uriPath), + Code._getIssuer(uriPath, uriParams), + Code._getDigits(uriParams), + Code._getPeriod(uriParams), + Code.getSanitizedSecret(uriParams), + Code._getAlgorithm(uriParams), + Code._getType(uriPath), + rawData, + id + ); + } + + private static _getAccount(uriPath: string): string { + try { + const path = decodeURIComponent(uriPath); + return path.split(':')[1]; + } catch (e) { + return ''; + } + } + + private static _getIssuer( + uriPath: string, + uriParams: { get?: any } + ): string { + try { + if (uriParams['issuer'] !== undefined) { + let issuer = uriParams['issuer']; + // This is to handle bug in the ente auth app + if (issuer.endsWith('period')) { + issuer = issuer.substring(0, issuer.length - 6); + } + return issuer; + } + const path = decodeURIComponent(uriPath); + return path.split(':')[0].substring(1); + } catch (e) { + return ''; + } + } + + private static _getDigits(uriParams): number { + try { + return parseInt(uriParams['digits'], 10); + } catch (e) { + return Code.defaultDigits; + } + } + + private static _getPeriod(uriParams): number { + try { + return parseInt(uriParams['period'], 10) || Code.defaultPeriod; + } catch (e) { + return Code.defaultPeriod; + } + } + + private static _getAlgorithm(uriParams): AlgorithmType { + try { + const algorithm = uriParams['algorithm'].toLowerCase(); + if (algorithm === 'sha256') { + return algorithm; + } else if (algorithm === 'sha512') { + return algorithm; + } + } catch (e) { + // nothing + } + return 'sha1'; + } + + private static _getType(uriPath: string): Type { + const oauthType = uriPath.split('/')[0].substring(0); + if (oauthType === 'totp') { + return 'totp'; + } else if (oauthType === 'hotp') { + return 'hotp'; + } + throw new Error(`Unsupported format with host ${oauthType}`); + } + + static getSanitizedSecret(uriParams): string { + return uriParams['secret'].replace(/ /g, '').toUpperCase(); + } +} From 1454ac6bf2f4acd06300f7bef64e71a1829a364f Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 09:26:04 +0530 Subject: [PATCH 07/77] Add authenticator service to fetch and decrypt codes --- .../authenticator/authenticatorService.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/services/authenticator/authenticatorService.ts diff --git a/src/services/authenticator/authenticatorService.ts b/src/services/authenticator/authenticatorService.ts new file mode 100644 index 0000000000..ebe80149e9 --- /dev/null +++ b/src/services/authenticator/authenticatorService.ts @@ -0,0 +1,91 @@ +import HTTPService from 'services/HTTPService'; +import { AuthEntity } from 'types/authenticator/auth_entity'; +import { Code } from 'types/authenticator/code'; +import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; +import { getEndpoint } from 'utils/common/apiUtil'; +import { getActualKey, getToken } from 'utils/common/key'; +import { logError } from 'utils/sentry'; + +const ENDPOINT = getEndpoint(); +export const getAuthCodes = async (): Promise => { + const masterKey = await getActualKey(); + try { + const authKeyData = await getAuthKey(); + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const authentitorKey = await cryptoWorker.decryptB64( + authKeyData.encryptedKey, + authKeyData.header, + masterKey + ); + // always fetch all data from server for now + const authEntity: AuthEntity[] = await getDiff(0); + const authCodes = await Promise.all( + authEntity + .filter((f) => !f.isDeleted) + .map(async (entity) => { + const decryptedCode = await cryptoWorker.decryptMetadata( + entity.encryptedData, + entity.header, + authentitorKey + ); + return Code.fromRawData(entity.id, decryptedCode); + }) + ); + // sort by issuer name which can be undefined also + authCodes.sort((a, b) => { + if (a.issuer && b.issuer) { + return a.issuer.localeCompare(b.issuer); + } + if (a.issuer) { + return -1; + } + if (b.issuer) { + return 1; + } + return 0; + }); + return authCodes; + } catch (e) { + logError(e, 'get authenticator entities failed'); + throw e; + } +}; + +export const getAuthKey = async () => { + try { + const resp = await HTTPService.get( + `${ENDPOINT}/authenticator/key`, + {}, + { + 'X-Auth-Token': getToken(), + } + ); + return resp.data; + } catch (e) { + logError(e, 'Get key failed'); + throw e; + } +}; + +// return a promise which resolves to list of AuthEnitity +export const getDiff = async ( + sinceTime: number, + limit = 2500 +): Promise => { + try { + const resp = await HTTPService.get( + `${ENDPOINT}/authenticator/entity/diff`, + { + sinceTime, + limit, + }, + { + 'X-Auth-Token': getToken(), + } + ); + return resp.data.diff; + } catch (e) { + logError(e, 'Get diff failed'); + throw e; + } +}; From 208e6c07fe80a54b61917b7ae4a609b49e2762fc Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 09:26:29 +0530 Subject: [PATCH 08/77] Refactor --- src/components/Authenicator/OTPDisplay.tsx | 50 ++++++++++++---------- src/components/Sidebar/UtilitySection.tsx | 2 +- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/Authenicator/OTPDisplay.tsx b/src/components/Authenicator/OTPDisplay.tsx index b0c1faec38..9be78988c2 100644 --- a/src/components/Authenicator/OTPDisplay.tsx +++ b/src/components/Authenicator/OTPDisplay.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { TOTP, HOTP } from 'otpauth'; +import { Code } from 'types/authenticator/code'; import TimerProgress from './TimerProgress'; const TOTPDisplay = ({ issuer, account, code, nextCode }) => { @@ -82,59 +83,64 @@ const TOTPDisplay = ({ issuer, account, code, nextCode }) => { ); }; -const OTPDisplay = ({ - secret, - type, - algorithm, - timePeriod, - issuer, - account, -}) => { +interface OTPDisplayProps { + codeInfo: Code; +} + +const OTPDisplay = (props: OTPDisplayProps) => { + const { codeInfo } = props; const [code, setCode] = useState(''); const [nextcode, setNextCode] = useState(''); const generateCodes = () => { const currentTime = new Date().getTime(); - if (type.toLowerCase() === 'totp') { + if (codeInfo.type.toLowerCase() === 'totp') { const totp = new TOTP({ - secret, - algorithm, - period: timePeriod ?? 30, + secret: codeInfo.secret, + algorithm: codeInfo.algorithm, + period: codeInfo.period ?? Code.defaultPeriod, + digits: codeInfo.digits, }); setCode(totp.generate()); setNextCode( - totp.generate({ timestamp: currentTime + timePeriod * 1000 }) + totp.generate({ + timestamp: currentTime + codeInfo.period * 1000, + }) ); - } else if (type.toLowerCase() === 'hotp') { - const hotp = new HOTP({ secret, counter: 0, algorithm }); + } else if (codeInfo.type.toLowerCase() === 'hotp') { + const hotp = new HOTP({ + secret: codeInfo.secret, + counter: 0, + algorithm: codeInfo.algorithm, + }); setCode(hotp.generate()); setNextCode(hotp.generate({ counter: 1 })); } }; useEffect(() => { + generateCodes(); let intervalId; - // compare case insensitive type - if (type.toLowerCase() === 'totp') { + if (codeInfo.type.toLowerCase() === 'totp') { intervalId = setInterval(() => { generateCodes(); }, 1000); - } else if (type.toLowerCase() === 'hotp') { + } else if (codeInfo.type.toLowerCase() === 'hotp') { intervalId = setInterval(() => { generateCodes(); }, 1000); } return () => clearInterval(intervalId); - }, [secret, type, algorithm, timePeriod]); + }, [codeInfo]); return (
- + diff --git a/src/components/Sidebar/UtilitySection.tsx b/src/components/Sidebar/UtilitySection.tsx index f797b92cb1..5d1ff91420 100644 --- a/src/components/Sidebar/UtilitySection.tsx +++ b/src/components/Sidebar/UtilitySection.tsx @@ -99,7 +99,7 @@ export default function UtilitySection({ closeSidebar }) { {constants.DEDUPLICATE_FILES} - {isInternalUser() && ( + {!isInternalUser() && ( {constants.AUTHENTICATOR_SECTION} From 5618e65fe10e9057eb7cf5d257dcb78be2d503db Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 09:31:12 +0530 Subject: [PATCH 09/77] [ente Authenticator] Remove hardcoded codes --- src/pages/authenticator/index.tsx | 93 ++++++------------------------- 1 file changed, 17 insertions(+), 76 deletions(-) diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index 40421483a1..05022424ab 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -1,83 +1,32 @@ import React, { useEffect, useState } from 'react'; import OTPDisplay from 'components/Authenicator/OTPDisplay'; -const random = [ - { - issuer: 'Google', - account: 'example@gmail.com', - secret: '6GJ2E2RQKJ36BY6A', - type: 'TOTP', - algorithm: 'SHA1', - period: 30, - }, - { - issuer: 'Facebook', - account: 'example@gmail.com', - secret: 'RVZJ7N6KJKJGQ2VX', - type: 'TOTP', - algorithm: 'SHA256', - period: 60, - }, - { - issuer: 'Twitter', - account: 'example@gmail.com', - secret: 'ZPUE6KJ3WGZ3HPKJ', - type: 'TOTP', - algorithm: 'SHA256', - period: 60, - }, - { - issuer: 'GitHub', - account: 'example@gmail.com', - secret: 'AG6U5KJYHPRRNRZI', - type: 'TOTP', - algorithm: 'SHA1', - period: 30, - }, - { - issuer: 'Amazon', - account: 'example@gmail.com', - secret: 'Q2FR2KJVKJFFKMWZ', - type: 'TOTP', - algorithm: 'SHA256', - period: 60, - }, - { - issuer: 'LinkedIn', - account: 'example@gmail.com', - secret: 'SWRG4KJ4J3LNDW2Z', - type: 'TOTP', - algorithm: 'SHA256', - period: 60, - }, - { - issuer: 'Dropbox', - account: 'example@gmail.com', - secret: 'G5U6OKJU3JRM72ZK', - type: 'TOTP', - algorithm: 'SHA1', - period: 30, - }, -]; +import { getAuthCodes } from 'services/authenticator/authenticatorService'; const OTPPage = () => { - const [secrets, setSecrets] = useState([]); + const [codes, setCodes] = useState([]); const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { - const fetchSecrets = async () => { + const fetchCodes = async () => { try { - setSecrets(random); + getAuthCodes().then((res) => { + setCodes(res); + }); } catch (error) { console.error(error); } }; - fetchSecrets(); + fetchCodes(); }, []); - const filteredSecrets = secrets.filter( + const filteredCodes = codes.filter( (secret) => - secret.issuer.toLowerCase().includes(searchTerm.toLowerCase()) || - secret.account.toLowerCase().includes(searchTerm.toLowerCase()) + (secret.issuer ?? '') + .toLowerCase() + .includes(searchTerm.toLowerCase()) || + (secret.account ?? '') + .toLowerCase() + .includes(searchTerm.toLowerCase()) ); return ( @@ -98,7 +47,7 @@ const OTPPage = () => { />
- {filteredSecrets.length === 0 ? ( + {filteredCodes.length === 0 ? (
{

Download ente auth mobile app to manage your secrets

*/}
) : ( - filteredSecrets.map((secret) => ( - + filteredCodes.map((code) => ( + )) )}
From 937d114f7c220b8a75ad3f4adc71bf3f79952b2d Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 10:23:14 +0530 Subject: [PATCH 10/77] Add hook to download ente auth app --- src/pages/authenticator/index.tsx | 37 +++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index 05022424ab..dbb6cee99f 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import OTPDisplay from 'components/Authenicator/OTPDisplay'; import { getAuthCodes } from 'services/authenticator/authenticatorService'; +import { Button } from '@mui/material'; const OTPPage = () => { const [codes, setCodes] = useState([]); @@ -29,8 +30,31 @@ const OTPPage = () => { .includes(searchTerm.toLowerCase()) ); + const DownloadApp = () => { + return ( +
+

Download our mobile app to add & manage your secrets.

+ + + +
+ ); + }; + return ( - // center the page
{ textAlign: 'center', marginTop: '32px', }}> -

No results found.

- {/*

Add a new secret to get started.

-

Download ente auth mobile app to manage your secrets

*/} + {searchTerm.length !== 0 ? ( +

No results found.

+ ) : ( +
+ )}
) : ( filteredCodes.map((code) => ( )) )} +
+ +
); }; From aaa3a273c0c5d7c1494101a886799d33e0fb5f50 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 10:26:40 +0530 Subject: [PATCH 11/77] Show authenticator section to internal users only --- src/components/Sidebar/UtilitySection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Sidebar/UtilitySection.tsx b/src/components/Sidebar/UtilitySection.tsx index 5d1ff91420..f797b92cb1 100644 --- a/src/components/Sidebar/UtilitySection.tsx +++ b/src/components/Sidebar/UtilitySection.tsx @@ -99,7 +99,7 @@ export default function UtilitySection({ closeSidebar }) { {constants.DEDUPLICATE_FILES} - {!isInternalUser() && ( + {isInternalUser() && ( {constants.AUTHENTICATOR_SECTION} From 7c8739a57b351ef12ee660768b19a5123d469bcf Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 10:43:17 +0530 Subject: [PATCH 12/77] [ente auth] redirect to root if key is missing --- src/pages/authenticator/index.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index dbb6cee99f..c5f5fab0b5 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import OTPDisplay from 'components/Authenicator/OTPDisplay'; import { getAuthCodes } from 'services/authenticator/authenticatorService'; import { Button } from '@mui/material'; +import { CustomError } from 'utils/error'; const OTPPage = () => { const [codes, setCodes] = useState([]); @@ -10,10 +11,20 @@ const OTPPage = () => { useEffect(() => { const fetchCodes = async () => { try { - getAuthCodes().then((res) => { - setCodes(res); - }); + getAuthCodes() + .then((res) => { + setCodes(res); + }) + .catch((err) => { + if (err.message === CustomError.KEY_MISSING) { + window.location.href = '/'; + return; + } + + console.error('something wrong here', err); + }); } catch (error) { + console.error('something wrong where asdas'); console.error(error); } }; From e8d8ffacc995287c6348773e831d406c8abbc4b7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 11:00:33 +0530 Subject: [PATCH 13/77] Fix routing on redirect to creds page --- src/pages/authenticator/index.tsx | 9 +++++++-- src/pages/credentials/index.tsx | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index c5f5fab0b5..967be144b8 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -3,8 +3,11 @@ import OTPDisplay from 'components/Authenicator/OTPDisplay'; import { getAuthCodes } from 'services/authenticator/authenticatorService'; import { Button } from '@mui/material'; import { CustomError } from 'utils/error'; +import { PAGES } from 'constants/pages'; +import { useRouter } from 'next/router'; const OTPPage = () => { + const router = useRouter(); const [codes, setCodes] = useState([]); const [searchTerm, setSearchTerm] = useState(''); @@ -17,10 +20,12 @@ const OTPPage = () => { }) .catch((err) => { if (err.message === CustomError.KEY_MISSING) { - window.location.href = '/'; + router.push({ + pathname: PAGES.CREDENTIALS, + query: { redirectPage: PAGES.AUTHENICATOR }, + }); return; } - console.error('something wrong here', err); }); } catch (error) { diff --git a/src/pages/credentials/index.tsx b/src/pages/credentials/index.tsx index 02c54f3894..2e3359a839 100644 --- a/src/pages/credentials/index.tsx +++ b/src/pages/credentials/index.tsx @@ -30,6 +30,8 @@ import VerifyMasterPasswordForm, { export default function Credentials() { const router = useRouter(); + const routeReidrectPage = + router.query.redirectPage?.toString() ?? PAGES.GALLERY; const [keyAttributes, setKeyAttributes] = useState(); const appContext = useContext(AppContext); const [user, setUser] = useState(); @@ -85,7 +87,7 @@ export default function Credentials() { await decryptAndStoreToken(key); const redirectURL = appContext.redirectURL; appContext.setRedirectURL(null); - router.push(redirectURL ?? PAGES.GALLERY); + router.push(redirectURL ?? routeReidrectPage ?? PAGES.GALLERY); } catch (e) { logError(e, 'useMasterPassword failed'); } From 02e2de1ef588733eb2c743339113f9418aa3b43a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 11:04:50 +0530 Subject: [PATCH 14/77] Hide search box when there's no entry --- src/pages/authenticator/index.tsx | 17 +++++++++++------ .../authenticator/authenticatorService.ts | 3 +++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index 967be144b8..f40cf5ef15 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -78,13 +78,18 @@ const OTPPage = () => { alignItems: 'center', justifyContent: 'flex-start', }}> +

ente Authenticator

- setSearchTerm(e.target.value)} - /> + {filteredCodes.length === 0 && searchTerm.length === 0 ? ( + <> + ) : ( + setSearchTerm(e.target.value)} + /> + )}
{filteredCodes.length === 0 ? ( diff --git a/src/services/authenticator/authenticatorService.ts b/src/services/authenticator/authenticatorService.ts index ebe80149e9..8027661bd2 100644 --- a/src/services/authenticator/authenticatorService.ts +++ b/src/services/authenticator/authenticatorService.ts @@ -44,6 +44,9 @@ export const getAuthCodes = async (): Promise => { } return 0; }); + // remove all entries from authCodes + authCodes.splice(0, authCodes.length); + return authCodes; } catch (e) { logError(e, 'get authenticator entities failed'); From 6d094b3bcad17487a02f03b24e38998fc7001e81 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 11:05:57 +0530 Subject: [PATCH 15/77] Undo test changes --- src/services/authenticator/authenticatorService.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/services/authenticator/authenticatorService.ts b/src/services/authenticator/authenticatorService.ts index 8027661bd2..ebe80149e9 100644 --- a/src/services/authenticator/authenticatorService.ts +++ b/src/services/authenticator/authenticatorService.ts @@ -44,9 +44,6 @@ export const getAuthCodes = async (): Promise => { } return 0; }); - // remove all entries from authCodes - authCodes.splice(0, authCodes.length); - return authCodes; } catch (e) { logError(e, 'get authenticator entities failed'); From 134cd8343bc83204c53e03e3a64c86b4487e9569 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 20 Mar 2023 11:58:39 +0530 Subject: [PATCH 16/77] Handle error in generating code --- src/components/Authenicator/OTPDisplay.tsx | 77 ++++++++++++------- .../authenticator/authenticatorService.ts | 13 +++- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/src/components/Authenicator/OTPDisplay.tsx b/src/components/Authenicator/OTPDisplay.tsx index 9be78988c2..f476dc4ffa 100644 --- a/src/components/Authenicator/OTPDisplay.tsx +++ b/src/components/Authenicator/OTPDisplay.tsx @@ -91,30 +91,35 @@ const OTPDisplay = (props: OTPDisplayProps) => { const { codeInfo } = props; const [code, setCode] = useState(''); const [nextcode, setNextCode] = useState(''); + const [codeErr, setCodeErr] = useState(''); const generateCodes = () => { - const currentTime = new Date().getTime(); - if (codeInfo.type.toLowerCase() === 'totp') { - const totp = new TOTP({ - secret: codeInfo.secret, - algorithm: codeInfo.algorithm, - period: codeInfo.period ?? Code.defaultPeriod, - digits: codeInfo.digits, - }); - setCode(totp.generate()); - setNextCode( - totp.generate({ - timestamp: currentTime + codeInfo.period * 1000, - }) - ); - } else if (codeInfo.type.toLowerCase() === 'hotp') { - const hotp = new HOTP({ - secret: codeInfo.secret, - counter: 0, - algorithm: codeInfo.algorithm, - }); - setCode(hotp.generate()); - setNextCode(hotp.generate({ counter: 1 })); + try { + const currentTime = new Date().getTime(); + if (codeInfo.type.toLowerCase() === 'totp') { + const totp = new TOTP({ + secret: codeInfo.secret, + algorithm: codeInfo.algorithm, + period: codeInfo.period ?? Code.defaultPeriod, + digits: codeInfo.digits, + }); + setCode(totp.generate()); + setNextCode( + totp.generate({ + timestamp: currentTime + codeInfo.period * 1000, + }) + ); + } else if (codeInfo.type.toLowerCase() === 'hotp') { + const hotp = new HOTP({ + secret: codeInfo.secret, + counter: 0, + algorithm: codeInfo.algorithm, + }); + setCode(hotp.generate()); + setNextCode(hotp.generate({ counter: 1 })); + } + } catch (err) { + setCodeErr(err.message); } }; @@ -138,12 +143,28 @@ const OTPDisplay = (props: OTPDisplayProps) => { return (
- + {codeErr === '' ? ( + + ) : ( +
+

{codeErr}

+

{codeInfo.rawData ?? 'no rawdata'}

+
+ )}
); }; diff --git a/src/services/authenticator/authenticatorService.ts b/src/services/authenticator/authenticatorService.ts index ebe80149e9..4f64b08117 100644 --- a/src/services/authenticator/authenticatorService.ts +++ b/src/services/authenticator/authenticatorService.ts @@ -28,8 +28,19 @@ export const getAuthCodes = async (): Promise => { entity.header, authentitorKey ); - return Code.fromRawData(entity.id, decryptedCode); + try { + return Code.fromRawData(entity.id, decryptedCode); + } catch (e) { + console.log( + 'failed to parse code', + e, + entity.id, + decryptedCode + ); + return null; + } }) + .filter((f) => f !== null || f !== undefined) ); // sort by issuer name which can be undefined also authCodes.sort((a, b) => { From 6cb022dede77c1e000565dc494b4a2c4c6089a9d Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 21 Mar 2023 14:11:37 +0530 Subject: [PATCH 17/77] Fix code generation --- src/components/Authenicator/OTPDisplay.tsx | 4 ++-- src/types/authenticator/code.ts | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/Authenicator/OTPDisplay.tsx b/src/components/Authenicator/OTPDisplay.tsx index f476dc4ffa..7a851aecac 100644 --- a/src/components/Authenicator/OTPDisplay.tsx +++ b/src/components/Authenicator/OTPDisplay.tsx @@ -99,9 +99,9 @@ const OTPDisplay = (props: OTPDisplayProps) => { if (codeInfo.type.toLowerCase() === 'totp') { const totp = new TOTP({ secret: codeInfo.secret, - algorithm: codeInfo.algorithm, + algorithm: codeInfo.algorithm ?? Code.defaultAlgo, period: codeInfo.period ?? Code.defaultPeriod, - digits: codeInfo.digits, + digits: codeInfo.digits ?? Code.defaultDigits, }); setCode(totp.generate()); setNextCode( diff --git a/src/types/authenticator/code.ts b/src/types/authenticator/code.ts index 8e94d7b45d..cc244a4c41 100644 --- a/src/types/authenticator/code.ts +++ b/src/types/authenticator/code.ts @@ -12,6 +12,7 @@ type AlgorithmType = export class Code { static readonly defaultDigits = 6; + static readonly defaultAlgo = 'sha1'; static readonly defaultPeriod = 30; // id for the corresponding auth entity @@ -70,12 +71,14 @@ export class Code { }); const uri = URI.parse(santizedRawData); - let uriPath = uri.path; + let uriPath = decodeURIComponent(uri.path); if ( uriPath.startsWith('/otpauth://') || uriPath.startsWith('otpauth://') ) { uriPath = uriPath.split('otpauth://')[1]; + } else if (uriPath.startsWith('otpauth%3A//')) { + uriPath = uriPath.split('otpauth%3A//')[1]; } return new Code( @@ -94,7 +97,11 @@ export class Code { private static _getAccount(uriPath: string): string { try { const path = decodeURIComponent(uriPath); - return path.split(':')[1]; + if (path.includes(':')) { + return path.split(':')[1]; + } else if (path.includes('/')) { + return path.split('/')[1]; + } } catch (e) { return ''; } @@ -122,7 +129,7 @@ export class Code { private static _getDigits(uriParams): number { try { - return parseInt(uriParams['digits'], 10); + return parseInt(uriParams['digits'], 10) || Code.defaultDigits; } catch (e) { return Code.defaultDigits; } From d2ebf79b47d50cbf6575a4b1cbc9316157eda717 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 21 Mar 2023 14:36:14 +0530 Subject: [PATCH 18/77] Remove logging for rawCode --- src/services/authenticator/authenticatorService.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/services/authenticator/authenticatorService.ts b/src/services/authenticator/authenticatorService.ts index 4f64b08117..cdf31f7ab4 100644 --- a/src/services/authenticator/authenticatorService.ts +++ b/src/services/authenticator/authenticatorService.ts @@ -31,12 +31,7 @@ export const getAuthCodes = async (): Promise => { try { return Code.fromRawData(entity.id, decryptedCode); } catch (e) { - console.log( - 'failed to parse code', - e, - entity.id, - decryptedCode - ); + console.log('failed to parse code', e, entity.id); return null; } }) From 0746deff74bdf27a9cb2d98fabe95d306338beb1 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 21 Mar 2023 16:23:31 +0530 Subject: [PATCH 19/77] Log codeInfo --- src/components/Authenicator/OTPDisplay.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Authenicator/OTPDisplay.tsx b/src/components/Authenicator/OTPDisplay.tsx index 7a851aecac..bcaecb760b 100644 --- a/src/components/Authenicator/OTPDisplay.tsx +++ b/src/components/Authenicator/OTPDisplay.tsx @@ -119,6 +119,7 @@ const OTPDisplay = (props: OTPDisplayProps) => { setNextCode(hotp.generate({ counter: 1 })); } } catch (err) { + console.log('codeInfo', codeInfo); setCodeErr(err.message); } }; From aaa301d09dba71aaf73ae23fcc1c3c4851b15b34 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 22 Mar 2023 12:44:45 +0530 Subject: [PATCH 20/77] [ente authenticator] Handle \r --- src/types/authenticator/code.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/authenticator/code.ts b/src/types/authenticator/code.ts index cc244a4c41..de32ad611e 100644 --- a/src/types/authenticator/code.ts +++ b/src/types/authenticator/code.ts @@ -51,7 +51,8 @@ export class Code { static fromRawData(id: string, rawData: string): Code { let santizedRawData = rawData .replace(/\+/g, '%2B') - .replace(/:/g, '%3A'); + .replace(/:/g, '%3A') + .replaceAll('\r', ''); if (santizedRawData.startsWith('"')) { santizedRawData = santizedRawData.substring(1); } From 3a3b1337b328e95c7dc8b1016b15a14beec1b770 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 22 Mar 2023 13:01:47 +0530 Subject: [PATCH 21/77] [ente auth] improve parsing of issuer from path --- src/types/authenticator/code.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/types/authenticator/code.ts b/src/types/authenticator/code.ts index de32ad611e..0e2ff8bba8 100644 --- a/src/types/authenticator/code.ts +++ b/src/types/authenticator/code.ts @@ -121,8 +121,16 @@ export class Code { } return issuer; } - const path = decodeURIComponent(uriPath); - return path.split(':')[0].substring(1); + let path = decodeURIComponent(uriPath); + if (path.startsWith('totp/') || path.startsWith('hotp/')) { + path = path.substring(5); + } + if (path.includes(':')) { + return path.split(':')[0]; + } else if (path.includes('-')) { + return path.split('-')[0]; + } + return path; } catch (e) { return ''; } From 76710698cdb6c1eb229caed04f49ebd5f18f2baa Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 22 Mar 2023 13:10:52 +0530 Subject: [PATCH 22/77] [ente auth] Improve bad code display --- src/components/Authenicator/OTPDisplay.tsx | 35 +++++++++++++--------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/components/Authenicator/OTPDisplay.tsx b/src/components/Authenicator/OTPDisplay.tsx index bcaecb760b..fd328041b0 100644 --- a/src/components/Authenicator/OTPDisplay.tsx +++ b/src/components/Authenicator/OTPDisplay.tsx @@ -83,6 +83,26 @@ const TOTPDisplay = ({ issuer, account, code, nextCode }) => { ); }; +function BadCodeInfo({ codeInfo, codeErr }) { + const [showRawData, setShowRawData] = useState(false); + + return ( +
+
{codeInfo.title}
+
{codeErr}
+
+ {showRawData ? ( +
setShowRawData(false)}> + {codeInfo.rawData ?? 'no raw data'} +
+ ) : ( +
setShowRawData(true)}>Show rawData
+ )} +
+
+ ); +} + interface OTPDisplayProps { codeInfo: Code; } @@ -119,7 +139,6 @@ const OTPDisplay = (props: OTPDisplayProps) => { setNextCode(hotp.generate({ counter: 1 })); } } catch (err) { - console.log('codeInfo', codeInfo); setCodeErr(err.message); } }; @@ -152,19 +171,7 @@ const OTPDisplay = (props: OTPDisplayProps) => { nextCode={nextcode} /> ) : ( -
-

{codeErr}

-

{codeInfo.rawData ?? 'no rawdata'}

-
+ )}
); From 6d9767db405d57f3f34871ce41a8a46e2e9aeb4d Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:14:08 +0530 Subject: [PATCH 23/77] Remove logging --- src/pages/authenticator/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index f40cf5ef15..6471d2263f 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -26,11 +26,10 @@ const OTPPage = () => { }); return; } - console.error('something wrong here', err); + // do not log errors }); } catch (error) { - console.error('something wrong where asdas'); - console.error(error); + // do not log errors } }; fetchCodes(); From 4c190a319c22bf453f31be621eaa50aec79ccd01 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:21:37 +0530 Subject: [PATCH 24/77] Remove exception from log --- src/services/authenticator/authenticatorService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/authenticator/authenticatorService.ts b/src/services/authenticator/authenticatorService.ts index cdf31f7ab4..8ef3df69b9 100644 --- a/src/services/authenticator/authenticatorService.ts +++ b/src/services/authenticator/authenticatorService.ts @@ -31,7 +31,10 @@ export const getAuthCodes = async (): Promise => { try { return Code.fromRawData(entity.id, decryptedCode); } catch (e) { - console.log('failed to parse code', e, entity.id); + logError( + Error('failed to parse code'), + 'codeId = ' + entity.id + ); return null; } }) From c6da52b1f56dd0ecf850e72b83b6325a38d1eadf Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:23:52 +0530 Subject: [PATCH 25/77] Switch to /auth instead of /authenticator --- src/components/Sidebar/UtilitySection.tsx | 2 +- src/constants/pages/index.ts | 3 ++- src/pages/authenticator/index.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Sidebar/UtilitySection.tsx b/src/components/Sidebar/UtilitySection.tsx index fe1db37023..99c199107f 100644 --- a/src/components/Sidebar/UtilitySection.tsx +++ b/src/components/Sidebar/UtilitySection.tsx @@ -63,7 +63,7 @@ export default function UtilitySection({ closeSidebar }) { const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE); - const redirectToAuthenticatorPage = () => router.push(PAGES.AUTHENICATOR); + const redirectToAuthenticatorPage = () => router.push(PAGES.AUTH); const somethingWentWrong = () => setDialogMessage({ diff --git a/src/constants/pages/index.ts b/src/constants/pages/index.ts index 38f2b3a6a5..10594aae62 100644 --- a/src/constants/pages/index.ts +++ b/src/constants/pages/index.ts @@ -14,5 +14,6 @@ export enum PAGES { SHARED_ALBUMS = '/shared-albums', // ML_DEBUG = '/ml-debug', DEDUPLICATE = '/deduplicate', - AUTHENICATOR = '/authenticator', + // AUTH page is used to show (auth)enticator codes + AUTH = '/auth', } diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index 6471d2263f..13aed7dc58 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -22,7 +22,7 @@ const OTPPage = () => { if (err.message === CustomError.KEY_MISSING) { router.push({ pathname: PAGES.CREDENTIALS, - query: { redirectPage: PAGES.AUTHENICATOR }, + query: { redirectPage: PAGES.AUTH }, }); return; } From e63260b5ce4c2388a6e025ecd50779cf4a003c98 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:25:07 +0530 Subject: [PATCH 26/77] Fix typo --- src/pages/credentials/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/credentials/index.tsx b/src/pages/credentials/index.tsx index e9422a8d8f..7c0f2c6516 100644 --- a/src/pages/credentials/index.tsx +++ b/src/pages/credentials/index.tsx @@ -31,7 +31,7 @@ import VerifyMasterPasswordForm, { export default function Credentials() { const router = useRouter(); - const routeReidrectPage = + const routerRedirectPage = router.query.redirectPage?.toString() ?? PAGES.GALLERY; const [keyAttributes, setKeyAttributes] = useState(); const appContext = useContext(AppContext); @@ -88,7 +88,7 @@ export default function Credentials() { await decryptAndStoreToken(key); const redirectURL = appContext.redirectURL; appContext.setRedirectURL(null); - router.push(redirectURL ?? routeReidrectPage ?? PAGES.GALLERY); + router.push(redirectURL ?? routerRedirectPage ?? PAGES.GALLERY); } catch (e) { logError(e, 'useMasterPassword failed'); } From dcaae1f4787fca75026a63689481662f53c25650 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:28:38 +0530 Subject: [PATCH 27/77] Refactor --- src/pages/authenticator/index.tsx | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index 13aed7dc58..567a98d6f9 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -12,24 +12,20 @@ const OTPPage = () => { const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { + // refactor this code const fetchCodes = async () => { try { - getAuthCodes() - .then((res) => { - setCodes(res); - }) - .catch((err) => { - if (err.message === CustomError.KEY_MISSING) { - router.push({ - pathname: PAGES.CREDENTIALS, - query: { redirectPage: PAGES.AUTH }, - }); - return; - } - // do not log errors + const res = await getAuthCodes(); + setCodes(res); + } catch (err) { + if (err.message === CustomError.KEY_MISSING) { + router.push({ + pathname: PAGES.CREDENTIALS, + query: { redirectPage: PAGES.AUTH }, }); - } catch (error) { - // do not log errors + } else { + // do not log errors + } } }; fetchCodes(); From ac713bf9d6f0d6d4826eff7b5e5736c180453ee6 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:30:19 +0530 Subject: [PATCH 28/77] Fix typos --- src/components/Authenicator/OTPDisplay.tsx | 4 ++-- src/services/authenticator/authenticatorService.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Authenicator/OTPDisplay.tsx b/src/components/Authenicator/OTPDisplay.tsx index fd328041b0..37415e644e 100644 --- a/src/components/Authenicator/OTPDisplay.tsx +++ b/src/components/Authenicator/OTPDisplay.tsx @@ -110,7 +110,7 @@ interface OTPDisplayProps { const OTPDisplay = (props: OTPDisplayProps) => { const { codeInfo } = props; const [code, setCode] = useState(''); - const [nextcode, setNextCode] = useState(''); + const [nextCode, setNextCode] = useState(''); const [codeErr, setCodeErr] = useState(''); const generateCodes = () => { @@ -168,7 +168,7 @@ const OTPDisplay = (props: OTPDisplayProps) => { issuer={codeInfo.issuer} account={codeInfo.account} code={code} - nextCode={nextcode} + nextCode={nextCode} /> ) : ( diff --git a/src/services/authenticator/authenticatorService.ts b/src/services/authenticator/authenticatorService.ts index 8ef3df69b9..54f0106837 100644 --- a/src/services/authenticator/authenticatorService.ts +++ b/src/services/authenticator/authenticatorService.ts @@ -12,7 +12,7 @@ export const getAuthCodes = async (): Promise => { try { const authKeyData = await getAuthKey(); const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const authentitorKey = await cryptoWorker.decryptB64( + const authenticatorKey = await cryptoWorker.decryptB64( authKeyData.encryptedKey, authKeyData.header, masterKey @@ -26,7 +26,7 @@ export const getAuthCodes = async (): Promise => { const decryptedCode = await cryptoWorker.decryptMetadata( entity.encryptedData, entity.header, - authentitorKey + authenticatorKey ); try { return Code.fromRawData(entity.id, decryptedCode); From f8667bf692b08c53b9ce5cb52a3fe9887d257e39 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:35:44 +0530 Subject: [PATCH 29/77] Simplify if --- src/components/Authenicator/OTPDisplay.tsx | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/Authenicator/OTPDisplay.tsx b/src/components/Authenicator/OTPDisplay.tsx index 37415e644e..a6cf1e263b 100644 --- a/src/components/Authenicator/OTPDisplay.tsx +++ b/src/components/Authenicator/OTPDisplay.tsx @@ -112,6 +112,7 @@ const OTPDisplay = (props: OTPDisplayProps) => { const [code, setCode] = useState(''); const [nextCode, setNextCode] = useState(''); const [codeErr, setCodeErr] = useState(''); + const generateCodeInterval = 1000; const generateCodes = () => { try { @@ -145,19 +146,18 @@ const OTPDisplay = (props: OTPDisplayProps) => { useEffect(() => { generateCodes(); - let intervalId; + const codeType = codeInfo.type; + const intervalId = + codeType.toLowerCase() === 'totp' || + codeType.toLowerCase() === 'hotp' + ? setInterval(() => { + generateCodes(); + }, generateCodeInterval) + : null; - if (codeInfo.type.toLowerCase() === 'totp') { - intervalId = setInterval(() => { - generateCodes(); - }, 1000); - } else if (codeInfo.type.toLowerCase() === 'hotp') { - intervalId = setInterval(() => { - generateCodes(); - }, 1000); - } - - return () => clearInterval(intervalId); + return () => { + if (intervalId) clearInterval(intervalId); + }; }, [codeInfo]); return ( From 2069fff499b2c17672a2d9ebfdc1ff0374195888 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:38:54 +0530 Subject: [PATCH 30/77] Convert pure data class to interface --- src/types/authenticator/auth_entity.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/types/authenticator/auth_entity.ts b/src/types/authenticator/auth_entity.ts index 083651d310..cd97f52623 100644 --- a/src/types/authenticator/auth_entity.ts +++ b/src/types/authenticator/auth_entity.ts @@ -1,23 +1,8 @@ -export class AuthEntity { +export interface AuthEntity { id: string; encryptedData: string | null; header: string | null; isDeleted: boolean; createdAt: number; updatedAt: number; - constructor( - id: string, - encryptedData: string | null, - header: string | null, - isDeleted: boolean, - createdAt: number, - updatedAt: number - ) { - this.id = id; - this.encryptedData = encryptedData; - this.header = header; - this.isDeleted = isDeleted; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } } From 5688c42d6e4dc9a4fd6ab5684fa296c09ef5a91c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 07:43:32 +0530 Subject: [PATCH 31/77] Fix string extraction --- public/locales/en/translation.json | 1 + src/components/Sidebar/UtilitySection.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 906b3c264f..879208e1c9 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -459,6 +459,7 @@ "UPLOAD_GOOGLE_TAKEOUT": "Google takeout", "CANCEL_UPLOADS": "Cancel uploads", "DEDUPLICATE_FILES": "Deduplicate files", + "AUTHENTICATOR_SECTION": "Authenticator", "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", "CLUB_BY_CAPTURE_TIME": "Club by capture time", "FILES": "Files", diff --git a/src/components/Sidebar/UtilitySection.tsx b/src/components/Sidebar/UtilitySection.tsx index 99c199107f..a07c70a118 100644 --- a/src/components/Sidebar/UtilitySection.tsx +++ b/src/components/Sidebar/UtilitySection.tsx @@ -102,7 +102,7 @@ export default function UtilitySection({ closeSidebar }) { {isInternalUser() && ( - {constants.AUTHENTICATOR_SECTION} + {t('AUTHENTICATOR_SECTION')} )} From c2451105e5bf8c3ca3da1a35f92538de6a908c19 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 08:07:19 +0530 Subject: [PATCH 32/77] Wrap decryption inside tryCatch --- src/services/authenticator/authenticatorService.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/services/authenticator/authenticatorService.ts b/src/services/authenticator/authenticatorService.ts index 54f0106837..b0ae58ef6a 100644 --- a/src/services/authenticator/authenticatorService.ts +++ b/src/services/authenticator/authenticatorService.ts @@ -23,12 +23,13 @@ export const getAuthCodes = async (): Promise => { authEntity .filter((f) => !f.isDeleted) .map(async (entity) => { - const decryptedCode = await cryptoWorker.decryptMetadata( - entity.encryptedData, - entity.header, - authenticatorKey - ); try { + const decryptedCode = + await cryptoWorker.decryptMetadata( + entity.encryptedData, + entity.header, + authenticatorKey + ); return Code.fromRawData(entity.id, decryptedCode); } catch (e) { logError( From 5e7411c405b775d9b60d27d497041a2d7df9c253 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 25 Mar 2023 08:52:01 +0530 Subject: [PATCH 33/77] Extract footer as component --- src/components/Authenicator/AuthFooder.tsx | 25 +++++++++++++++++++ src/pages/authenticator/index.tsx | 29 ++-------------------- 2 files changed, 27 insertions(+), 27 deletions(-) create mode 100644 src/components/Authenicator/AuthFooder.tsx diff --git a/src/components/Authenicator/AuthFooder.tsx b/src/components/Authenicator/AuthFooder.tsx new file mode 100644 index 0000000000..64eeb10295 --- /dev/null +++ b/src/components/Authenicator/AuthFooder.tsx @@ -0,0 +1,25 @@ +import { Button } from '@mui/material'; + +export const AuthFooter = () => { + return ( +
+

Download our mobile app to add & manage your secrets.

+ + + +
+ ); +}; diff --git a/src/pages/authenticator/index.tsx b/src/pages/authenticator/index.tsx index 567a98d6f9..b61adf3332 100644 --- a/src/pages/authenticator/index.tsx +++ b/src/pages/authenticator/index.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; import OTPDisplay from 'components/Authenicator/OTPDisplay'; import { getAuthCodes } from 'services/authenticator/authenticatorService'; -import { Button } from '@mui/material'; import { CustomError } from 'utils/error'; import { PAGES } from 'constants/pages'; import { useRouter } from 'next/router'; +import { AuthFooter } from 'components/Authenicator/AuthFooder'; const OTPPage = () => { const router = useRouter(); @@ -12,7 +12,6 @@ const OTPPage = () => { const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { - // refactor this code const fetchCodes = async () => { try { const res = await getAuthCodes(); @@ -41,30 +40,6 @@ const OTPPage = () => { .includes(searchTerm.toLowerCase()) ); - const DownloadApp = () => { - return ( -
-

Download our mobile app to add & manage your secrets.

- - - -
- ); - }; - return (
{ )) )}
- +
); From a0a15e7e144cd7bc0a674796ba84a75a4a27ad27 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 16:08:26 +0530 Subject: [PATCH 34/77] refactor logic to prevent multiple parallel --- src/components/ExportModal.tsx | 20 ++++------ src/services/exportService.ts | 73 +++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx index 0de1db7644..7290064dca 100644 --- a/src/components/ExportModal.tsx +++ b/src/components/ExportModal.tsx @@ -114,7 +114,7 @@ export default function ExportModal(props: Props) { setLastExportTime(exportRecord.lastAttemptTimestamp); await syncFileCounts(); if (exportRecord.stage === ExportStage.INPROGRESS) { - await startExport(); + startExport(); } } catch (e) { logError(e, 'error handling exportFolder change'); @@ -172,6 +172,7 @@ export default function ExportModal(props: Props) { throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); } await updateExportStage(ExportStage.INPROGRESS); + setExportProgress({ current: 0, total: 0 }); }; const postExportRun = async () => { @@ -209,17 +210,12 @@ export default function ExportModal(props: Props) { } }; - const startExport = async () => { - try { - await preExportRun(); - setExportProgress({ current: 0, total: 0 }); - await exportService.exportFiles(setExportProgress); - await postExportRun(); - } catch (e) { - if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { - logError(e, 'startExport failed'); - } - } + const startExport = () => { + void exportService.runExport( + preExportRun, + setExportProgress, + postExportRun + ); }; const stopExport = async () => { diff --git a/src/services/exportService.ts b/src/services/exportService.ts index 36f7a6ea25..5af12e4f79 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -62,7 +62,8 @@ export const ENTE_EXPORT_DIRECTORY = 'ente Photos'; class ExportService { private electronAPIs: ElectronAPIs; - private exportInProgress: Promise = null; + private exportInProgress: boolean = false; + private reRunNeeded = false; private exportRecordUpdater = new QueueProcessor(1); private stopExport: boolean = false; private allElectronAPIsExist: boolean = false; @@ -96,31 +97,13 @@ class ExportService { } } - enableContinuousExport(startExport: () => Promise) { + enableContinuousExport(startExport: () => void) { try { if (this.continuousExportEventHandler) { addLogLine('continuous export already enabled'); return; } - const reRunNeeded = { current: false }; - this.continuousExportEventHandler = async () => { - try { - addLogLine('continuous export triggered'); - if (this.exportInProgress) { - addLogLine('export in progress, scheduling re-run'); - reRunNeeded.current = true; - return; - } - await startExport(); - if (reRunNeeded.current) { - reRunNeeded.current = false; - addLogLine('re-running export'); - setTimeout(this.continuousExportEventHandler, 0); - } - } catch (e) { - logError(e, 'continuous export failed'); - } - }; + this.continuousExportEventHandler = startExport; this.continuousExportEventHandler(); eventBus.addListener( Events.LOCAL_FILES_UPDATED, @@ -171,15 +154,46 @@ class ExportService { this.stopExport = true; } - async exportFiles(updateProgress: (progress: ExportProgress) => void) { + runExport = async ( + preExport: () => Promise, + updateProgress: (progress: ExportProgress) => void, + postExport: () => Promise + ) => { try { - // eslint-disable-next-line @typescript-eslint/no-misused-promises if (this.exportInProgress) { + addLogLine('export in progress, scheduling re-run'); this.electronAPIs.sendNotification( t('EXPORT_NOTIFICATION.IN_PROGRESS') ); - return await this.exportInProgress; + this.reRunNeeded = true; + return; } + addLogLine('starting export'); + this.exportInProgress = true; + await preExport(); + await this.exportFiles(updateProgress); + await postExport(); + addLogLine('export completed'); + this.exportInProgress = false; + if (this.reRunNeeded) { + this.reRunNeeded = false; + addLogLine('re-running export'); + setTimeout( + () => this.runExport(preExport, updateProgress, postExport), + 0 + ); + } + } catch (e) { + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'runExport failed'); + } + } + }; + + private async exportFiles( + updateProgress: (progress: ExportProgress) => void + ) { + try { const exportDir = getData(LS_KEYS.EXPORT)?.folder; if (!exportDir) { // no-export folder set @@ -218,7 +232,7 @@ class ExportService { ); addLogLine( - `starting export, filesToExportCount: ${filesToExport?.length}, userPersonalFileCount: ${userPersonalFiles?.length}` + `exportFiles: filesToExportCount: ${filesToExport?.length}, userPersonalFileCount: ${userPersonalFiles?.length}` ); const collectionIDPathMap: CollectionIDPathMap = @@ -228,7 +242,7 @@ class ExportService { userCollections, exportRecord ); - this.exportInProgress = this.fileExporter( + await this.fileExporter( filesToExport, collectionIDNameMap, renamedCollections, @@ -236,13 +250,8 @@ class ExportService { updateProgress, exportDir ); - const resp = await this.exportInProgress; - return resp; } catch (e) { logError(e, 'exportFiles failed'); - return { paused: false }; - } finally { - this.exportInProgress = null; } } @@ -556,7 +565,7 @@ class ExportService { } isExportInProgress = () => { - return this.exportInProgress !== null; + return this.exportInProgress; }; exists = (path: string) => { From 1e7b34763df0ae42ca7aa5ec40d56a0288f7e47f Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 16:13:45 +0530 Subject: [PATCH 35/77] reset in progress on export fail --- src/services/exportService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/exportService.ts b/src/services/exportService.ts index 5af12e4f79..dc6a2125e8 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -184,6 +184,7 @@ class ExportService { ); } } catch (e) { + this.exportInProgress = false; if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { logError(e, 'runExport failed'); } From 7b96c58aa34b8ba26ec8dd74bdc1f380782f6af0 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 16:30:05 +0530 Subject: [PATCH 36/77] better handle error and refactor stopExport --- src/components/ExportModal.tsx | 9 ++----- src/services/exportService.ts | 46 +++++++++++++++++++++------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx index 7290064dca..18e6e7d205 100644 --- a/src/components/ExportModal.tsx +++ b/src/components/ExportModal.tsx @@ -218,13 +218,8 @@ export default function ExportModal(props: Props) { ); }; - const stopExport = async () => { - try { - exportService.stopRunningExport(); - await postExportRun(); - } catch (e) { - logError(e, 'stopExport failed'); - } + const stopExport = () => { + void exportService.stopRunningExport(postExportRun); }; return ( diff --git a/src/services/exportService.ts b/src/services/exportService.ts index dc6a2125e8..2c9b677275 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -150,8 +150,13 @@ class ExportService { } }; - stopRunningExport() { - this.stopExport = true; + async stopRunningExport(postExport: () => Promise) { + try { + this.stopExport = true; + await postExport(); + } catch (e) { + logError(e, 'stopRunningExport failed'); + } } runExport = async ( @@ -168,23 +173,30 @@ class ExportService { this.reRunNeeded = true; return; } - addLogLine('starting export'); - this.exportInProgress = true; - await preExport(); - await this.exportFiles(updateProgress); - await postExport(); - addLogLine('export completed'); - this.exportInProgress = false; - if (this.reRunNeeded) { - this.reRunNeeded = false; - addLogLine('re-running export'); - setTimeout( - () => this.runExport(preExport, updateProgress, postExport), - 0 - ); + try { + addLogLine('starting export'); + this.exportInProgress = true; + await preExport(); + await this.exportFiles(updateProgress); + addLogLine('export completed'); + } finally { + this.exportInProgress = false; + if (this.reRunNeeded) { + this.reRunNeeded = false; + addLogLine('re-running export'); + setTimeout( + () => + this.runExport( + preExport, + updateProgress, + postExport + ), + 0 + ); + } + await postExport(); } } catch (e) { - this.exportInProgress = false; if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { logError(e, 'runExport failed'); } From ccdde7a43c3fa0109e8d652df67a7a0ade48392a Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 17:23:24 +0530 Subject: [PATCH 37/77] fix page broke export UI update --- src/components/ExportModal.tsx | 33 ++++++++++---------- src/services/exportService.ts | 57 +++++++++++++++++----------------- src/types/export/index.ts | 7 +++++ 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx index 18e6e7d205..74365d4332 100644 --- a/src/components/ExportModal.tsx +++ b/src/components/ExportModal.tsx @@ -72,6 +72,12 @@ export default function ExportModal(props: Props) { setExportFolder(exportSettings?.folder); setContinuousExport(exportSettings?.continuousExport); syncFileCounts(); + exportService.setUIUpdaters({ + updateExportStage: updateExportStage, + updateExportProgress: setExportProgress, + updateFileExportStats: setFileExportStats, + updateLastExportTime: updateExportTime, + }); } catch (e) { logError(e, 'error in exportModal'); } @@ -90,7 +96,7 @@ export default function ExportModal(props: Props) { useEffect(() => { try { if (continuousExport) { - exportService.enableContinuousExport(startExport); + exportService.enableContinuousExport(); } else { exportService.disableContinuousExport(); } @@ -162,7 +168,7 @@ export default function ExportModal(props: Props) { // HELPER FUNCTIONS // ======================= - const preExportRun = async () => { + const verifyExportFolderExists = () => { const exportFolder = getData(LS_KEYS.EXPORT)?.folder; const exportFolderExists = exportService.exists(exportFolder); if (!exportFolderExists) { @@ -171,14 +177,6 @@ export default function ExportModal(props: Props) { ); throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); } - await updateExportStage(ExportStage.INPROGRESS); - setExportProgress({ current: 0, total: 0 }); - }; - - const postExportRun = async () => { - await updateExportStage(ExportStage.FINISHED); - await updateExportTime(Date.now()); - await syncFileCounts(); }; const syncFileCounts = async () => { @@ -211,15 +209,18 @@ export default function ExportModal(props: Props) { }; const startExport = () => { - void exportService.runExport( - preExportRun, - setExportProgress, - postExportRun - ); + try { + verifyExportFolderExists(); + exportService.runExport(); + } catch (e) { + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'startExport failed'); + } + } }; const stopExport = () => { - void exportService.stopRunningExport(postExportRun); + void exportService.stopRunningExport(); }; return ( diff --git a/src/services/exportService.ts b/src/services/exportService.ts index 2c9b677275..0615812b8a 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -41,14 +41,14 @@ import { Collection } from 'types/collection'; import { CollectionIDNameMap, CollectionIDPathMap, - ExportProgress, ExportRecord, ExportRecordV1, + ExportUIUpdaters, FileExportStats, } from 'types/export'; import { User } from 'types/user'; import { FILE_TYPE, TYPE_JPEG, TYPE_JPG } from 'constants/file'; -import { RecordType } from 'constants/export'; +import { ExportStage, RecordType } from 'constants/export'; import { ElectronAPIs } from 'types/electron'; import { CustomError } from 'utils/error'; import { addLogLine } from 'utils/logging'; @@ -69,12 +69,17 @@ class ExportService { private allElectronAPIsExist: boolean = false; private fileReader: FileReader = null; private continuousExportEventHandler: () => void; + private uiUpdater: ExportUIUpdaters; constructor() { this.electronAPIs = runningInBrowser() && window['ElectronAPIs']; this.allElectronAPIsExist = !!this.electronAPIs?.exists; } + async setUIUpdaters(uiUpdater: ExportUIUpdaters) { + this.uiUpdater = uiUpdater; + } + async changeExportDirectory(callback: (newExportDir: string) => void) { try { const newRootDir = await this.electronAPIs.selectRootDirectory(); @@ -97,13 +102,13 @@ class ExportService { } } - enableContinuousExport(startExport: () => void) { + enableContinuousExport() { try { if (this.continuousExportEventHandler) { addLogLine('continuous export already enabled'); return; } - this.continuousExportEventHandler = startExport; + this.continuousExportEventHandler = this.runExport; this.continuousExportEventHandler(); eventBus.addListener( Events.LOCAL_FILES_UPDATED, @@ -150,20 +155,16 @@ class ExportService { } }; - async stopRunningExport(postExport: () => Promise) { + async stopRunningExport() { try { this.stopExport = true; - await postExport(); + await this.postExport(); } catch (e) { logError(e, 'stopRunningExport failed'); } } - runExport = async ( - preExport: () => Promise, - updateProgress: (progress: ExportProgress) => void, - postExport: () => Promise - ) => { + runExport = async () => { try { if (this.exportInProgress) { addLogLine('export in progress, scheduling re-run'); @@ -176,25 +177,18 @@ class ExportService { try { addLogLine('starting export'); this.exportInProgress = true; - await preExport(); - await this.exportFiles(updateProgress); + await this.uiUpdater.updateExportStage(ExportStage.INPROGRESS); + this.uiUpdater.updateExportProgress({ current: 0, total: 0 }); + await this.exportFiles(); addLogLine('export completed'); } finally { this.exportInProgress = false; if (this.reRunNeeded) { this.reRunNeeded = false; addLogLine('re-running export'); - setTimeout( - () => - this.runExport( - preExport, - updateProgress, - postExport - ), - 0 - ); + setTimeout(this.runExport, 0); } - await postExport(); + await this.postExport(); } } catch (e) { if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { @@ -203,9 +197,7 @@ class ExportService { } }; - private async exportFiles( - updateProgress: (progress: ExportProgress) => void - ) { + private async exportFiles() { try { const exportDir = getData(LS_KEYS.EXPORT)?.folder; if (!exportDir) { @@ -260,7 +252,6 @@ class ExportService { collectionIDNameMap, renamedCollections, collectionIDPathMap, - updateProgress, exportDir ); } catch (e) { @@ -273,7 +264,6 @@ class ExportService { collectionIDNameMap: CollectionIDNameMap, renamedCollections: Collection[], collectionIDPathMap: CollectionIDPathMap, - updateProgress: (progress: ExportProgress) => void, exportDir: string ): Promise { try { @@ -319,7 +309,10 @@ class ExportService { RecordType.SUCCESS ); success++; - updateProgress({ current: success, total: files.length }); + this.uiUpdater.updateExportProgress({ + current: success, + total: files.length, + }); } catch (e) { logError(e, 'export failed for a file'); if ( @@ -346,6 +339,12 @@ class ExportService { } } + async postExport() { + await this.uiUpdater.updateExportStage(ExportStage.FINISHED); + await this.uiUpdater.updateLastExportTime(Date.now()); + this.uiUpdater.updateFileExportStats(await this.getFileExportStats()); + } + async addFileExportedRecord( folder: string, file: EnteFile, diff --git a/src/types/export/index.ts b/src/types/export/index.ts index b607e0c777..c25497d982 100644 --- a/src/types/export/index.ts +++ b/src/types/export/index.ts @@ -37,3 +37,10 @@ export interface ExportSettings { folder: string; continuousExport: boolean; } + +export interface ExportUIUpdaters { + updateExportStage: (stage: ExportStage) => Promise; + updateExportProgress: (progress: ExportProgress) => void; + updateFileExportStats: (fileExportStats: FileExportStats) => void; + updateLastExportTime: (exportTime: number) => Promise; +} From b244b60d13bce6e8b5c75be8fa915a450bd15548 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 18:17:14 +0530 Subject: [PATCH 38/77] refactor continuousExport and exportFolder logic --- src/components/ExportModal.tsx | 126 ++++++++++++++++++--------------- src/services/exportService.ts | 1 + 2 files changed, 70 insertions(+), 57 deletions(-) diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx index 74365d4332..b3ae21b6cf 100644 --- a/src/components/ExportModal.tsx +++ b/src/components/ExportModal.tsx @@ -1,7 +1,12 @@ import isElectron from 'is-electron'; import React, { useEffect, useState, useContext } from 'react'; import exportService from 'services/exportService'; -import { ExportProgress, ExportSettings, FileExportStats } from 'types/export'; +import { + ExportProgress, + ExportRecord, + ExportSettings, + FileExportStats, +} from 'types/export'; import { Box, Button, @@ -67,67 +72,37 @@ export default function ExportModal(props: Props) { if (!isElectron()) { return; } - try { - const exportSettings: ExportSettings = getData(LS_KEYS.EXPORT); - setExportFolder(exportSettings?.folder); - setContinuousExport(exportSettings?.continuousExport); - syncFileCounts(); - exportService.setUIUpdaters({ - updateExportStage: updateExportStage, - updateExportProgress: setExportProgress, - updateFileExportStats: setFileExportStats, - updateLastExportTime: updateExportTime, - }); - } catch (e) { - logError(e, 'error in exportModal'); - } - }, []); - - useEffect(() => { - if (!props.show) { - return; - } - if (exportService.isExportInProgress()) { - setExportStage(ExportStage.INPROGRESS); - } - syncFileCounts(); - }, [props.show]); - - useEffect(() => { - try { - if (continuousExport) { - exportService.enableContinuousExport(); - } else { - exportService.disableContinuousExport(); - } - } catch (e) { - logError(e, 'error handling continuousExport change'); - } - }, [continuousExport]); - - useEffect(() => { - if (!exportFolder) { - return; - } const main = async () => { try { - const exportRecord = await exportService.getExportRecord(); - if (!exportRecord) { - setExportStage(ExportStage.INIT); - return; - } - setExportStage(exportRecord.stage); - setLastExportTime(exportRecord.lastAttemptTimestamp); - await syncFileCounts(); - if (exportRecord.stage === ExportStage.INPROGRESS) { + exportService.setUIUpdaters({ + updateExportStage: updateExportStage, + updateExportProgress: setExportProgress, + updateFileExportStats: setFileExportStats, + updateLastExportTime: updateExportTime, + }); + const exportSettings: ExportSettings = getData(LS_KEYS.EXPORT); + setExportFolder(exportSettings?.folder); + setContinuousExport(exportSettings?.continuousExport); + const exportRecord = await syncExportRecord(exportFolder); + if (exportRecord?.stage === ExportStage.INPROGRESS) { startExport(); } + if (exportSettings?.continuousExport) { + exportService.enableContinuousExport(); + } } catch (e) { - logError(e, 'error handling exportFolder change'); + logError(e, 'export on mount useEffect failed'); } }; void main(); - }, [exportFolder]); + }, []); + + useEffect(() => { + if (!props.show) { + return; + } + void syncFileCounts(); + }, [props.show]); // ============= // STATE UPDATERS @@ -168,6 +143,16 @@ export default function ExportModal(props: Props) { // HELPER FUNCTIONS // ======================= + const onExportFolderChange = async (newFolder: string) => { + try { + updateExportFolder(newFolder); + syncExportRecord(newFolder); + } catch (e) { + logError(e, 'onExportChange failed'); + throw e; + } + }; + const verifyExportFolderExists = () => { const exportFolder = getData(LS_KEYS.EXPORT)?.folder; const exportFolderExists = exportService.exists(exportFolder); @@ -179,6 +164,27 @@ export default function ExportModal(props: Props) { } }; + const syncExportRecord = async ( + exportFolder: string + ): Promise => { + try { + const exportRecord = await exportService.getExportRecord( + exportFolder + ); + if (!exportRecord) { + setExportStage(ExportStage.INIT); + return null; + } + setExportStage(exportRecord.stage); + setLastExportTime(exportRecord.lastAttemptTimestamp); + void syncFileCounts(); + return exportRecord; + } catch (e) { + logError(e, 'syncExportRecord failed'); + throw e; + } + }; + const syncFileCounts = async () => { try { const fileExportStats = await exportService.getFileExportStats(); @@ -193,7 +199,7 @@ export default function ExportModal(props: Props) { // ============= const handleChangeExportDirectoryClick = () => { - void exportService.changeExportDirectory(updateExportFolder); + void exportService.changeExportDirectory(onExportFolderChange); }; const handleOpenExportDirectoryClick = () => { @@ -202,9 +208,15 @@ export default function ExportModal(props: Props) { const toggleContinuousExport = () => { try { - updateContinuousExport(!continuousExport); + const newContinuousExport = !continuousExport; + if (newContinuousExport) { + exportService.enableContinuousExport(); + } else { + exportService.disableContinuousExport(); + } + updateContinuousExport(newContinuousExport); } catch (e) { - logError(e, 'toggleContinuousExport failed'); + logError(e, 'onContinuousExportChange failed'); } }; diff --git a/src/services/exportService.ts b/src/services/exportService.ts index 0615812b8a..f89082623a 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -158,6 +158,7 @@ class ExportService { async stopRunningExport() { try { this.stopExport = true; + this.reRunNeeded = false; await this.postExport(); } catch (e) { logError(e, 'stopRunningExport failed'); From 3725f0598e1124fa80aba37c2488d5ef65ab3567 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 18:40:42 +0530 Subject: [PATCH 39/77] make label prop optional --- src/components/DropdownInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DropdownInput.tsx b/src/components/DropdownInput.tsx index cd53ae1a05..e64a679d10 100644 --- a/src/components/DropdownInput.tsx +++ b/src/components/DropdownInput.tsx @@ -16,7 +16,7 @@ export interface DropdownOption { interface Iprops { label: string; - labelProps: TypographyTypeMap['props']; + labelProps?: TypographyTypeMap['props']; options: DropdownOption[]; message?: string; selectedValue: string; From 6e0b59d4a5190ced5e666535ff18d16abd862d42 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 21:09:40 +0530 Subject: [PATCH 40/77] add messageProps prop dropdownInput --- src/components/DropdownInput.tsx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/DropdownInput.tsx b/src/components/DropdownInput.tsx index e64a679d10..a0487d4302 100644 --- a/src/components/DropdownInput.tsx +++ b/src/components/DropdownInput.tsx @@ -19,8 +19,9 @@ interface Iprops { labelProps?: TypographyTypeMap['props']; options: DropdownOption[]; message?: string; - selectedValue: string; - setSelectedValue: (selectedValue: T) => void; + messageProps?: TypographyTypeMap['props']; + selected: string; + setSelected: (selectedValue: T) => void; placeholder?: string; } @@ -29,10 +30,12 @@ export default function DropdownInput({ labelProps, options, message, - selectedValue, + selected, placeholder, - setSelectedValue, + setSelected, + messageProps, }: Iprops) { + console.log({ ...messageProps }); return ( {label} @@ -77,9 +80,9 @@ export default function DropdownInput({ options.find((o) => o.value === selected).label ); }} - value={selectedValue} + value={selected} onChange={(event: SelectChangeEvent) => { - setSelectedValue(event.target.value as T); + setSelected(event.target.value as T); }}> {options.map((option, index) => ( ({ ))} {message && ( - + {message} )} From cf9fc7f717b6637771b18e016fb00650cb4b45f0 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 21:10:19 +0530 Subject: [PATCH 41/77] added CheckboxInput component --- src/components/CheckboxInput.tsx | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/components/CheckboxInput.tsx diff --git a/src/components/CheckboxInput.tsx b/src/components/CheckboxInput.tsx new file mode 100644 index 0000000000..21f6850e78 --- /dev/null +++ b/src/components/CheckboxInput.tsx @@ -0,0 +1,43 @@ +import { + FormControlLabel, + Checkbox, + FormGroup, + Typography, + TypographyTypeMap, +} from '@mui/material'; + +interface Iprops { + disabled?: boolean; + checked: boolean; + onChange: (value: boolean) => void; + label: string; + labelProps?: TypographyTypeMap['props']; +} +export function CheckboxInput({ + disabled, + checked, + onChange, + label, + labelProps, +}: Iprops) { + return ( + + onChange(e.target.checked)} + color="accent" + /> + } + label={ + + {label} + + } + /> + + ); +} From 929995acfbb32dd4e7150d1fb0eec095a7d31982 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 21:10:38 +0530 Subject: [PATCH 42/77] Add MultilineInput component --- src/components/MultilineInput.tsx | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/components/MultilineInput.tsx diff --git a/src/components/MultilineInput.tsx b/src/components/MultilineInput.tsx new file mode 100644 index 0000000000..056af35124 --- /dev/null +++ b/src/components/MultilineInput.tsx @@ -0,0 +1,51 @@ +import { Stack, TextField, Typography, TypographyTypeMap } from '@mui/material'; + +interface Iprops { + label: string; + labelProps?: TypographyTypeMap['props']; + message?: string; + messageProps?: TypographyTypeMap['props']; + placeholder?: string; + value: string; + rowCount: number; + onChange: (value: string) => void; +} + +export default function MultilineInput({ + label, + labelProps, + message, + messageProps, + placeholder, + value, + rowCount, + onChange, +}: Iprops) { + return ( + + {label} + onChange(e.target.value)} + placeholder={placeholder} + sx={(theme) => ({ + border: '1px solid', + borderColor: theme.colors.stroke.faint, + borderRadius: '8px', + padding: '12px', + '.MuiInputBase-formControl': { + '::before, ::after': { + borderBottom: 'none !important', + }, + }, + })} + /> + + {message} + + + ); +} From f099d8355e642faab4fc0ce2f5f03aca00dc08d3 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 21:11:01 +0530 Subject: [PATCH 43/77] add delete feedback en strings --- public/locales/en/translation.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 1b3e82001e..c9a6a0fc47 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -497,5 +497,18 @@ "CONTINUOUS_EXPORT": "Sync continuously", "TOTAL_ITEMS": "Total items", "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting..." + "EXPORT_STARTING": "Export starting...", + "DELETE_ACCOUNT_REASON_LABEL": "What is the main reason you are deleting your account?", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Select a reason", + "DELETE_REASON": { + "MISSING_FEATURE": "It's missing a key feature that I need", + "BROKEN_BEHAVIOR": "The app or a certain feature does not behave as I think it should", + "FOUND_ANOTHER_SERVICE": "I found another service that I like better", + "USING_DIFFERENT_ACCOUNT": "I use a different account", + "NOT_LISTED": "My reason isn't listed" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", + "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion" } From ff128ae1e1ae49386354eb5fd4a197b757ac64e6 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 30 Mar 2023 21:11:34 +0530 Subject: [PATCH 44/77] added delete feedback form --- src/components/DeleteAccountModal.tsx | 208 ++++++++++++++++++-------- 1 file changed, 144 insertions(+), 64 deletions(-) diff --git a/src/components/DeleteAccountModal.tsx b/src/components/DeleteAccountModal.tsx index 0e462137d8..64ec6a831c 100644 --- a/src/components/DeleteAccountModal.tsx +++ b/src/components/DeleteAccountModal.tsx @@ -1,18 +1,7 @@ -import NoAccountsIcon from '@mui/icons-material/NoAccountsOutlined'; -import TickIcon from '@mui/icons-material/Done'; -import { - Dialog, - DialogContent, - Typography, - Button, - Stack, - Link, -} from '@mui/material'; +import { Button, Link, Stack } from '@mui/material'; import { AppContext } from 'pages/_app'; -import React, { useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { preloadImage, initiateEmail } from 'utils/common'; -import VerticallyCentered from './Container'; -import DialogTitleWithCloseButton from './DialogBox/TitleWithCloseButton'; import { deleteAccount, getAccountDeleteChallenge, @@ -23,12 +12,63 @@ import { logError } from 'utils/sentry'; import { decryptDeleteAccountChallenge } from 'utils/crypto'; import { Trans } from 'react-i18next'; import { t } from 'i18next'; -import { DELETE_ACCOUNT_EMAIL, FEEDBACK_EMAIL } from 'constants/urls'; +import { DELETE_ACCOUNT_EMAIL } from 'constants/urls'; +import DialogBoxV2 from './DialogBoxV2'; +import * as Yup from 'yup'; +import { Formik, FormikHelpers } from 'formik'; +import DropdownInput, { DropdownOption } from './DropdownInput'; +import MultilineInput from './MultilineInput'; +import { CheckboxInput } from './CheckboxInput'; interface Iprops { onClose: () => void; open: boolean; } + +interface FormValues { + reason: string; + feedback: string; +} + +enum DELETE_REASON { + MISSING_FEATURE = '0', + BROKEN_BEHAVIOR = '1', + FOUND_ANOTHER_SERVICE = '2', + USING_DIFFERENT_ACCOUNT = '3', + NOT_LISTED = '4', +} + +const getReasonOptions = (): DropdownOption[] => { + return [ + { + label: t('DELETE_REASON.MISSING_FEATURE'), + value: DELETE_REASON.MISSING_FEATURE, + }, + { + label: t('DELETE_REASON.BROKEN_BEHAVIOR'), + value: DELETE_REASON.BROKEN_BEHAVIOR, + }, + { + label: t('DELETE_REASON.FOUND_ANOTHER_SERVICE'), + value: DELETE_REASON.FOUND_ANOTHER_SERVICE, + }, + { + label: t('DELETE_REASON.USING_DIFFERENT_ACCOUNT'), + value: DELETE_REASON.USING_DIFFERENT_ACCOUNT, + }, + { + label: t('DELETE_REASON.NOT_LISTED'), + value: DELETE_REASON.NOT_LISTED, + }, + ]; +}; + +const REASON_WITH_REQUIRED_FEEDBACK = new Set([ + DELETE_REASON.MISSING_FEATURE, + DELETE_REASON.BROKEN_BEHAVIOR, + DELETE_REASON.NOT_LISTED, +]); + const DeleteAccountModal = ({ open, onClose }: Iprops) => { const { setDialogMessage, isMobile } = useContext(AppContext); const [authenticateUserModalView, setAuthenticateUserModalView] = @@ -38,13 +78,12 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => { const openAuthenticateUserModal = () => setAuthenticateUserModalView(true); const closeAuthenticateUserModal = () => setAuthenticateUserModalView(false); + const [acceptDataDeletion, setAcceptDataDeletion] = useState(false); useEffect(() => { preloadImage('/images/delete-account'); }, []); - const sendFeedbackMail = () => initiateEmail('feedback@ente.io'); - const somethingWentWrong = () => setDialogMessage({ title: t('ERROR'), @@ -52,8 +91,19 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => { content: t('UNKNOWN_ERROR'), }); - const initiateDelete = async () => { + const initiateDelete = async ( + { reason, feedback }: FormValues, + { setFieldError }: FormikHelpers + ) => { try { + console.log({ reason, feedback }); + if ( + REASON_WITH_REQUIRED_FEEDBACK.has(reason as DELETE_REASON) && + !feedback?.length + ) { + setFieldError('feedback', t('REQUIRED')); + return; + } const deleteChallengeResponse = await getAccountDeleteChallenge(); setDeleteAccountChallenge( deleteChallengeResponse.encryptedChallenge @@ -120,56 +170,86 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => { return ( <> - - - - {t('DELETE_ACCOUNT')} - - - - - - - - - , - }} - values={{ emailID: FEEDBACK_EMAIL }} - /> - - - - - - - - + fullScreen={isMobile} + attributes={{ + title: t('DELETE_ACCOUNT'), + secondary: { + action: onClose, + text: t('CANCEL'), + }, + }}> + + initialValues={{ + reason: '', + feedback: '', + }} + validationSchema={Yup.object().shape({ + reason: Yup.string().required(t('REQUIRED')), + })} + validateOnChange={false} + validateOnBlur={false} + onSubmit={initiateDelete}> + {({ + values, + errors, + handleChange, + handleSubmit, + }): JSX.Element => ( +
+ + + + + + + + + +
+ )} + + Date: Fri, 31 Mar 2023 11:13:31 +0530 Subject: [PATCH 45/77] fix message styling --- src/components/DropdownInput.tsx | 1 + src/components/MultilineInput.tsx | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/DropdownInput.tsx b/src/components/DropdownInput.tsx index a0487d4302..b8dc59efae 100644 --- a/src/components/DropdownInput.tsx +++ b/src/components/DropdownInput.tsx @@ -100,6 +100,7 @@ export default function DropdownInput({ {message && ( diff --git a/src/components/MultilineInput.tsx b/src/components/MultilineInput.tsx index 056af35124..5d70a78426 100644 --- a/src/components/MultilineInput.tsx +++ b/src/components/MultilineInput.tsx @@ -43,7 +43,11 @@ export default function MultilineInput({ }, })} /> - + {message}
From 14882778c070a56f5cf6b773fad95b1753cf86d5 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 12:01:00 +0530 Subject: [PATCH 46/77] changed feedback required copy --- public/locales/en/translation.json | 3 ++- src/components/DeleteAccountModal.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index c9a6a0fc47..26b545f518 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -510,5 +510,6 @@ "DELETE_ACCOUNT_FEEDBACK_LABEL": "We are sorry to see you go. Please explain why you are leaving to help us improve.", "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Yes, I want to permanently delete this account and all its data", - "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion" + "CONFIRM_DELETE_ACCOUNT": "Confirm Account Deletion", + "FEEDBACK_REQUIRED": "Kindly help us with this information" } diff --git a/src/components/DeleteAccountModal.tsx b/src/components/DeleteAccountModal.tsx index 64ec6a831c..88bf96f732 100644 --- a/src/components/DeleteAccountModal.tsx +++ b/src/components/DeleteAccountModal.tsx @@ -101,7 +101,7 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => { REASON_WITH_REQUIRED_FEEDBACK.has(reason as DELETE_REASON) && !feedback?.length ) { - setFieldError('feedback', t('REQUIRED')); + setFieldError('feedback', t('FEEDBACK_REQUIRED')); return; } const deleteChallengeResponse = await getAccountDeleteChallenge(); From c27bd720bbd2b6c35d553007795a2ea142e5f3a1 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 12:33:14 +0530 Subject: [PATCH 47/77] update authenticate modal to use dialogBoxV2 --- src/components/AuthenticateUserModal.tsx | 29 ++++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/AuthenticateUserModal.tsx b/src/components/AuthenticateUserModal.tsx index 4dcc11f040..8ec6f9b837 100644 --- a/src/components/AuthenticateUserModal.tsx +++ b/src/components/AuthenticateUserModal.tsx @@ -6,9 +6,9 @@ import { KeyAttributes, User } from 'types/user'; import VerifyMasterPasswordForm, { VerifyMasterPasswordFormProps, } from 'components/VerifyMasterPasswordForm'; -import { Dialog, Stack, Typography } from '@mui/material'; import { logError } from 'utils/sentry'; import { t } from 'i18next'; +import DialogBoxV2 from './DialogBoxV2'; interface Iprops { open: boolean; onClose: () => void; @@ -66,22 +66,21 @@ export default function AuthenticateUserModal({ }; return ( - - - - {t('PASSWORD')} - - - - + attributes={{ + title: t('PASSWORD'), + }} + PaperProps={{ sx: { padding: '8px 12px', maxWidth: '320px' } }}> + + ); } From 7c12826d9cdbb54e102d1275a26a17ec67a70cc4 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 12:33:52 +0530 Subject: [PATCH 48/77] add submitButtonProps prop to VerifyMasterPasswordForm --- src/components/VerifyMasterPasswordForm.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/VerifyMasterPasswordForm.tsx b/src/components/VerifyMasterPasswordForm.tsx index c5b80a1229..d69d57f48c 100644 --- a/src/components/VerifyMasterPasswordForm.tsx +++ b/src/components/VerifyMasterPasswordForm.tsx @@ -6,7 +6,7 @@ import SingleInputForm, { import { logError } from 'utils/sentry'; import { CustomError } from 'utils/error'; -import { Input } from '@mui/material'; +import { ButtonProps, Input } from '@mui/material'; import { KeyAttributes, User } from 'types/user'; import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; import { t } from 'i18next'; @@ -16,6 +16,7 @@ export interface VerifyMasterPasswordFormProps { keyAttributes: KeyAttributes; callback: (key: string, passphrase: string) => void; buttonText: string; + submitButtonProps?: ButtonProps; } export default function VerifyMasterPasswordForm({ @@ -23,6 +24,7 @@ export default function VerifyMasterPasswordForm({ keyAttributes, callback, buttonText, + submitButtonProps, }: VerifyMasterPasswordFormProps) { const verifyPassphrase: SingleInputFormProps['callback'] = async ( passphrase, @@ -72,6 +74,7 @@ export default function VerifyMasterPasswordForm({ callback={verifyPassphrase} placeholder={t('RETURN_PASSPHRASE_HINT')} buttonText={buttonText} + submitButtonProps={submitButtonProps} hiddenPreInput={ Date: Fri, 31 Mar 2023 12:47:00 +0530 Subject: [PATCH 49/77] remove button stack when no buttons present --- src/components/DialogBoxV2/index.tsx | 90 +++++++++++++++------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/src/components/DialogBoxV2/index.tsx b/src/components/DialogBoxV2/index.tsx index be9d76ef98..0c2fa24d13 100644 --- a/src/components/DialogBoxV2/index.tsx +++ b/src/components/DialogBoxV2/index.tsx @@ -42,13 +42,13 @@ export default function DialogBoxV2({ + }} + {...props}> {attributes.icon && ( @@ -73,53 +73,57 @@ export default function DialogBoxV2({ ))} - - {attributes.proceed && ( - - )} - {attributes.close && ( - - )} - {attributes.buttons && - attributes.buttons.map((b) => ( + {(attributes.proceed || + attributes.close || + attributes.buttons?.length) && ( + + {attributes.proceed && ( - ))} - + )} + {attributes.close && ( + + )} + {attributes.buttons && + attributes.buttons.map((b) => ( + + ))} + + )} ); From 0bfefb8919492bef56e1bb68a64979e42706709a Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 15:00:11 +0530 Subject: [PATCH 50/77] fix dialogBox close --- src/pages/_app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 22cd04ef1d..fa21eccb35 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -306,7 +306,7 @@ export default function App(props) { useEffect(() => { setDialogBoxV2View(true); - }, [dialogBoxV2View]); + }, [dialogBoxAttributeV2]); useEffect(() => { setNotificationView(true); From ae03dc22ba36b5f0c5dff61dae231e2997e2642f Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 15:00:41 +0530 Subject: [PATCH 51/77] update CONFIRM_ACCOUNT_DELETION_MESSAGE formatting --- public/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 26b545f518..93eacf6733 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -422,7 +422,7 @@ "ASK_FOR_FEEDBACK": "

We'll be sorry to see you go. Are you facing some issue?

Please write to us at {{emailID}}, maybe there is a way we can help.

", "SEND_FEEDBACK": "Yes, send feedback", "CONFIRM_ACCOUNT_DELETION_TITLE": "Are you sure you want to delete your account?", - "CONFIRM_ACCOUNT_DELETION_MESSAGE": "

Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.

", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", "AUTHENTICATE": "Authenticate", "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", From 7f6324789bf987591b397b6e6738c98ae8898225 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 15:02:51 +0530 Subject: [PATCH 52/77] added EnteButton component --- src/components/EnteButton.tsx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/components/EnteButton.tsx diff --git a/src/components/EnteButton.tsx b/src/components/EnteButton.tsx new file mode 100644 index 0000000000..f7d3246717 --- /dev/null +++ b/src/components/EnteButton.tsx @@ -0,0 +1,30 @@ +import Done from '@mui/icons-material/Done'; +import { Button, ButtonProps, CircularProgress } from '@mui/material'; +import { useEffect, useState } from 'react'; + +interface Iprops extends ButtonProps { + loading: boolean; +} + +export default function EnteButton({ children, loading, ...props }: Iprops) { + const [success, setSuccess] = useState(false); + + useEffect(() => { + if (loading === false) { + setSuccess(true); + setTimeout(() => setSuccess(false), 2000); + } + }, [loading]); + + return ( + + ); +} From ea18eef80de38caab9f4861069f40b0c5f996de2 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Fri, 31 Mar 2023 16:30:17 +0530 Subject: [PATCH 53/77] remove success logic from button and fixed loading and success button states --- src/components/EnteButton.tsx | 47 ++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/components/EnteButton.tsx b/src/components/EnteButton.tsx index f7d3246717..b684198fc6 100644 --- a/src/components/EnteButton.tsx +++ b/src/components/EnteButton.tsx @@ -1,25 +1,42 @@ import Done from '@mui/icons-material/Done'; -import { Button, ButtonProps, CircularProgress } from '@mui/material'; -import { useEffect, useState } from 'react'; +import { + Button, + ButtonProps, + CircularProgress, + PaletteColor, +} from '@mui/material'; interface Iprops extends ButtonProps { - loading: boolean; + loading?: boolean; + success?: boolean; } -export default function EnteButton({ children, loading, ...props }: Iprops) { - const [success, setSuccess] = useState(false); - - useEffect(() => { - if (loading === false) { - setSuccess(true); - setTimeout(() => setSuccess(false), 2000); - } - }, [loading]); - +export default function EnteButton({ + children, + loading, + success, + disabled, + sx, + ...props +}: Iprops) { return ( - + + )} {attributes.close && (