From a18583640cd9f0332dac27efbdce377016a31f6f Mon Sep 17 00:00:00 2001 From: KhashayarKhm Date: Tue, 29 Apr 2025 13:46:22 +0330 Subject: [PATCH] feat(frontend): add TOTP UI components and dependency - add OTP modal component with its css file - add Profile2FA component for 2FA section in settings page - add @scure/base package to encode OTP secrets in Base32, enabling alternative import options for authenticator apps - add new phrases to the en.json localization file --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 50 ++-- frontend/src/components/prompts/Otp.vue | 85 ++++++ frontend/src/components/prompts/Prompts.vue | 2 + .../src/components/settings/Profile2FA.vue | 246 ++++++++++++++++++ frontend/src/css/otp-modal.css | 29 +++ frontend/src/css/styles.css | 1 + frontend/src/i18n/en.json | 14 + 8 files changed, 410 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/prompts/Otp.vue create mode 100644 frontend/src/components/settings/Profile2FA.vue create mode 100644 frontend/src/css/otp-modal.css diff --git a/frontend/package.json b/frontend/package.json index 86c2f88d..15b00427 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@chenfengyuan/vue-number-input": "^2.0.1", + "@scure/base": "^1.2.4", "@vueuse/core": "^12.5.0", "@vueuse/integrations": "^12.5.0", "ace-builds": "^1.37.5", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2cf5b620..e016522b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@chenfengyuan/vue-number-input': specifier: ^2.0.1 version: 2.0.1(vue@3.5.13(typescript@5.6.3)) + '@scure/base': + specifier: ^1.2.4 + version: 1.2.4 '@vueuse/core': specifier: ^12.5.0 version: 12.5.0(typescript@5.6.3) @@ -934,22 +937,26 @@ packages: resolution: {integrity: sha512-nmG512G8QOABsserleechwHGZxzKSAlggGf9hQX0nltvSwyKNVuB/4o6iFeG2OnjXK253r8p8eSDOZf8PgFdWw==} engines: {node: '>= 16'} - '@intlify/message-compiler@11.0.0-rc.1': - resolution: {integrity: sha512-TGw2uBfuTFTegZf/BHtUQBEKxl7Q/dVGLoqRIdw8lFsp9g/53sYn5iD+0HxIzdYjbWL6BTJMXCPUHp9PxDTRPw==} - engines: {node: '>= 16'} - '@intlify/message-compiler@11.1.2': resolution: {integrity: sha512-T/xbNDzi+Yv0Qn2Dfz2CWCAJiwNgU5d95EhhAEf4YmOgjCKktpfpiUSmLcBvK1CtLpPQ85AMMQk/2NCcXnNj1g==} engines: {node: '>= 16'} - '@intlify/shared@11.0.0-rc.1': - resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==} + '@intlify/message-compiler@12.0.0-alpha.2': + resolution: {integrity: sha512-PD9C+oQbb7BF52hec0+vLnScaFkvnfX+R7zSbODYuRo/E2niAtGmHd0wPvEMsDhf9Z9b8f/qyDsVeZnD/ya9Ug==} engines: {node: '>= 16'} '@intlify/shared@11.1.2': resolution: {integrity: sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==} engines: {node: '>= 16'} + '@intlify/shared@11.1.3': + resolution: {integrity: sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==} + engines: {node: '>= 16'} + + '@intlify/shared@12.0.0-alpha.2': + resolution: {integrity: sha512-P2DULVX9nz3y8zKNqLw9Es1aAgQ1JGC+kgpx5q7yLmrnAKkPR5MybQWoEhxanefNJgUY5ehsgo+GKif59SrncA==} + engines: {node: '>= 16'} + '@intlify/unplugin-vue-i18n@6.0.3': resolution: {integrity: sha512-9ZDjBlhUHtgjRl23TVcgfJttgu8cNepwVhWvOv3mUMRDAhjW0pur1mWKEUKr1I8PNwE4Gvv2IQ1xcl4RL0nG0g==} engines: {node: '>= 18'} @@ -1140,6 +1147,9 @@ packages: cpu: [x64] os: [win32] + '@scure/base@1.2.4': + resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==} + '@tsconfig/node22@22.0.0': resolution: {integrity: sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg==} @@ -3578,8 +3588,8 @@ snapshots: '@intlify/bundle-utils@10.0.0(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))': dependencies: - '@intlify/message-compiler': 11.0.0-rc.1 - '@intlify/shared': 11.0.0-rc.1 + '@intlify/message-compiler': 12.0.0-alpha.2 + '@intlify/shared': 12.0.0-alpha.2 acorn: 8.14.0 escodegen: 2.1.0 estree-walker: 2.0.2 @@ -3595,26 +3605,28 @@ snapshots: '@intlify/message-compiler': 11.1.2 '@intlify/shared': 11.1.2 - '@intlify/message-compiler@11.0.0-rc.1': - dependencies: - '@intlify/shared': 11.0.0-rc.1 - source-map-js: 1.2.1 - '@intlify/message-compiler@11.1.2': dependencies: '@intlify/shared': 11.1.2 source-map-js: 1.2.1 - '@intlify/shared@11.0.0-rc.1': {} + '@intlify/message-compiler@12.0.0-alpha.2': + dependencies: + '@intlify/shared': 12.0.0-alpha.2 + source-map-js: 1.2.1 '@intlify/shared@11.1.2': {} + '@intlify/shared@11.1.3': {} + + '@intlify/shared@12.0.0-alpha.2': {} + '@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.19.0)(rollup@4.32.0)(typescript@5.6.3)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0) '@intlify/bundle-utils': 10.0.0(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3))) - '@intlify/shared': 11.1.2 - '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) + '@intlify/shared': 11.1.3 + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.3)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) '@rollup/pluginutils': 5.1.4(rollup@4.32.0) '@typescript-eslint/scope-manager': 8.21.0 '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.6.3) @@ -3636,11 +3648,11 @@ snapshots: - supports-color - typescript - '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': + '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.3)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': dependencies: '@babel/parser': 7.26.7 optionalDependencies: - '@intlify/shared': 11.1.2 + '@intlify/shared': 11.1.3 '@vue/compiler-dom': 3.5.13 vue: 3.5.13(typescript@5.6.3) vue-i18n: 11.1.2(vue@3.5.13(typescript@5.6.3)) @@ -3764,6 +3776,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.32.0': optional: true + '@scure/base@1.2.4': {} + '@tsconfig/node22@22.0.0': {} '@types/estree@1.0.6': {} diff --git a/frontend/src/components/prompts/Otp.vue b/frontend/src/components/prompts/Otp.vue new file mode 100644 index 00000000..769ab190 --- /dev/null +++ b/frontend/src/components/prompts/Otp.vue @@ -0,0 +1,85 @@ + + + diff --git a/frontend/src/components/prompts/Prompts.vue b/frontend/src/components/prompts/Prompts.vue index 71e4e753..273cb9ce 100644 --- a/frontend/src/components/prompts/Prompts.vue +++ b/frontend/src/components/prompts/Prompts.vue @@ -25,6 +25,7 @@ import Share from "./Share.vue"; import ShareDelete from "./ShareDelete.vue"; import Upload from "./Upload.vue"; import DiscardEditorChanges from "./DiscardEditorChanges.vue"; +import Otp from "./Otp.vue"; const layoutStore = useLayoutStore(); @@ -47,6 +48,7 @@ const components = new Map([ ["share-delete", ShareDelete], ["deleteUser", DeleteUser], ["discardEditorChanges", DiscardEditorChanges], + ["otp", Otp], ]); watch(currentPromptName, (newValue) => { diff --git a/frontend/src/components/settings/Profile2FA.vue b/frontend/src/components/settings/Profile2FA.vue new file mode 100644 index 00000000..88b87cc0 --- /dev/null +++ b/frontend/src/components/settings/Profile2FA.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/frontend/src/css/otp-modal.css b/frontend/src/css/otp-modal.css new file mode 100644 index 00000000..e5afb00b --- /dev/null +++ b/frontend/src/css/otp-modal.css @@ -0,0 +1,29 @@ +.otp-modal .card-title { + display: flex; + flex-direction: column; +} + +.otp-modal .card-title h2 { + text-align: center; + margin: 0 !important; +} + +.otp-modal .card-title p { + text-align: center; + color: var(--blue); + text-transform: lowercase; + font-weight: 500; + font-size: 0.9rem; + margin-top: 0.5rem; +} + +.otp-modal .card-content input { + font-size: 1.2em; + text-align: center; + letter-spacing: 0.5em; + transition: border 0.2s ease; +} + +.otp-modal .card-content input.empty { + letter-spacing: 0; +} diff --git a/frontend/src/css/styles.css b/frontend/src/css/styles.css index 19b94b95..2b0b3365 100644 --- a/frontend/src/css/styles.css +++ b/frontend/src/css/styles.css @@ -17,6 +17,7 @@ @import "./mobile.css"; @import "./epubReader.css"; @import "./mdPreview.css"; +@import "./otp-modal.css"; /* For testing only :focus { diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 1360bbec..4359c33b 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -1,6 +1,7 @@ { "buttons": { "cancel": "Cancel", + "check": "Check", "clear": "Clear", "close": "Close", "continue": "Continue", @@ -11,6 +12,8 @@ "create": "Create", "delete": "Delete", "download": "Download", + "enable": "Enable", + "disable": "Disable", "file": "File", "folder": "Folder", "fullScreen": "Toggle full screen", @@ -41,6 +44,7 @@ "toggleSidebar": "Toggle sidebar", "update": "Update", "upload": "Upload", + "verify": "Verify", "openFile": "Open file", "discardChanges": "Discard" }, @@ -182,6 +186,7 @@ "disableExternalLinks": "Disable external links (except documentation)", "disableUsedDiskPercentage": "Disable used disk percentage graph", "documentation": "documentation", + "otpCodeCheckPlaceholder": "Enter the otp code to check your setup key", "examples": "Examples", "executeOnShell": "Execute on shell", "executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you wish to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This applies to both user commands and event hooks.", @@ -239,6 +244,15 @@ "username": "Username", "users": "Users" }, + "otp": { + "name": "Two-Factor Authentication", + "verifyInstructions": "Enter the code from your authenticator app", + "codeInputPlaceholder": "6-digit code", + "invalidCodeType": "Verification code should be 6 english digits", + "enabledSuccessfully": "OTP enabled successfully", + "verificationFailed": "Verfication Failed", + "verificationSucceed": "Verification Succeed" + }, "sidebar": { "help": "Help", "hugoNew": "Hugo New",