diff --git a/dashboard/src/components/ProfileView.vue b/dashboard/src/components/ProfileView.vue index 9ffb245d4..f2065dbf6 100644 --- a/dashboard/src/components/ProfileView.vue +++ b/dashboard/src/components/ProfileView.vue @@ -2,6 +2,27 @@
+ +
+

{{ $t('profile.enable2FA.description') }}

+

+ + {{ twoFASecret }} +
+
+

{{ twoFAEnableError }}

+
+ + + + + + +
+
+
+

{{ $t('profile.title') }}

@@ -42,7 +63,7 @@ - + @@ -72,7 +93,7 @@ const i18n = useI18n(); const t = i18n.t; import { ref, onMounted, useTemplateRef } from 'vue'; -import { Button, Dropdown, InputDialog } from 'pankow'; +import { Button, Dropdown, Dialog, InputDialog, Spinner, TextInput } from 'pankow'; import { TOKEN_TYPES } from '../constants.js'; import AppPasswords from './AppPasswords.vue'; import Card from './Card.vue'; @@ -217,6 +238,54 @@ async function onRevokeAllWebAndCliTokens() { } +// 2fa +const mandatory2FAHelp = ref(''); +const twoFASecret = ref(''); +const twoFATotpToken = ref(''); +const twoFAQRCode = ref(''); +const twoFAEnableError = ref(''); +const twoFADialog = useTemplateRef('twoFADialog'); + +async function onOpenTwoFASetupDialog() { + const [error, result] = await profileModel.setTwoFASecret(); + if (error) return console.error(error); + + twoFAEnableError.value = ''; + twoFATotpToken.value = ''; + twoFASecret.value = result.secret; + twoFAQRCode.value = result.qrcode; + + twoFADialog.value.open(); +} + +async function onTwoFAEnable() { + const [error] = await profileModel.enableTwoFA(twoFATotpToken.value); + if (error) return twoFAEnableError.value = error.body ? error.body.message : 'Internal error'; + user.value = await profileModel.get(); + + twoFADialog.value.close(); +} + +async function onTwoFADisable() { + const password = await inputDialog.value.prompt({ + message: t('profile.disable2FA.title'), + modal: true, + placeholder: t('appstore.accountDialog.password'), + type: 'password', + confirmStyle: 'danger', + confirmLabel: t('main.dialog.yes'), + rejectLabel: t('main.dialog.no') + }); + + if (!password) return; + + const [error] = await profileModel.disableTwoFA(password); + if (error) return onTwoFADisable(); + + user.value = await profileModel.get(); +} + + // Init onMounted(async () => { user.value = await profileModel.get(); diff --git a/dashboard/src/models/ProfileModel.js b/dashboard/src/models/ProfileModel.js index 828dcf48b..d5fd75c7a 100644 --- a/dashboard/src/models/ProfileModel.js +++ b/dashboard/src/models/ProfileModel.js @@ -124,6 +124,39 @@ function create(origin, accessToken) { return null; }, + async setTwoFASecret() { + let error, result; + try { + result = await fetcher.post(`${origin}/api/v1/profile/twofactorauthentication_secret`, {}, { access_token: accessToken }); + } catch (e) { + error = e; + } + + if (error || result.status !== 200) return [error || result]; + return [null, result.body]; + }, + async enableTwoFA(totpToken) { + let error, result; + try { + result = await fetcher.post(`${origin}/api/v1/profile/twofactorauthentication_enable`, { totpToken }, { access_token: accessToken }); + } catch (e) { + error = e; + } + + if (error || result.status !== 204) return [error || result]; + return [null]; + }, + async disableTwoFA(password) { + let error, result; + try { + result = await fetcher.post(`${origin}/api/v1/profile/twofactorauthentication_disable`, { password }, { access_token: accessToken }); + } catch (e) { + error = e; + } + + if (error || result.status !== 204) return [error || result]; + return [null]; + }, }; }