Implement 2FA setup and disabling

This commit is contained in:
Johannes Zellner
2025-01-16 17:27:12 +01:00
parent 441b72158d
commit 3955fbdc64
2 changed files with 104 additions and 2 deletions

View File

@@ -2,6 +2,27 @@
<div class="content"> <div class="content">
<InputDialog ref="inputDialog" /> <InputDialog ref="inputDialog" />
<Dialog ref="twoFADialog"
:title="$t('profile.enable2FA.title')">
<div style="text-align: center; max-width: 420px">
<p v-show="mandatory2FAHelp">{{ $t('profile.enable2FA.description') }}</p>
<p v-html="$t('profile.enable2FA.authenticatorAppDescription', { googleAuthenticatorPlayStoreLink: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', googleAuthenticatorITunesLink: 'https://itunes.apple.com/us/app/google-authenticator/id388497605', freeOTPPlayStoreLink: 'https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp', freeOTPITunesLink: 'https://itunes.apple.com/us/app/freeotp-authenticator/id872559395'})"></p>
<img :src="twoFAQRCode" style="border-radius: 10px; margin-bottom: 10px"/>
<small>{{ twoFASecret }}</small>
<br/>
<br/>
<p class="has-error" v-show="twoFAEnableError">{{ twoFAEnableError }} </p>
<form @submit.prevent="onTwoFAEnable()">
<input type="submit" style="display: none;" :disabled="!twoFATotpToken"/>
<FormGroup>
<label for="totpTokenInput">{{ $t('profile.enable2FA.token') }}</label>
<TextInput v-model="twoFATotpToken" id="totpTokenInput" />
</FormGroup>
<Button @click="onTwoFAEnable()" :disabled="!twoFATotpToken">{{ $t('profile.enable2FA.enable') }}</Button>
</form>
</div>
</Dialog>
<h1>{{ $t('profile.title') }}</h1> <h1>{{ $t('profile.title') }}</h1>
<Card> <Card>
<div style="display: flex;"> <div style="display: flex;">
@@ -42,7 +63,7 @@
<td colspan="3" class="text-right"> <td colspan="3" class="text-right">
<!-- <Button tool @click="onPasswordReset()">{{ $t('profile.passwordResetAction') }}</Button> --> <!-- <Button tool @click="onPasswordReset()">{{ $t('profile.passwordResetAction') }}</Button> -->
<Button tool @click="onPasswordChange()">{{ $t('profile.changePasswordAction') }}</Button> <Button tool @click="onPasswordChange()">{{ $t('profile.changePasswordAction') }}</Button>
<Button tool v-show="!user.source && !config.external2FA" @click="on2FactorAuthConfig()">{{ $t(user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction') }}</Button> <Button tool v-show="!user.source && !config.external2FA" @click="user.twoFactorAuthenticationEnabled ? onTwoFADisable() : onOpenTwoFASetupDialog()">{{ $t(user.twoFactorAuthenticationEnabled ? 'profile.disable2FAAction' : 'profile.enable2FAAction') }}</Button>
<Button tool @click="profileModel.logout()" icon="fa fa-sign-out">{{ $t('main.logout') }}</Button> <Button tool @click="profileModel.logout()" icon="fa fa-sign-out">{{ $t('main.logout') }}</Button>
</td> </td>
</tr> </tr>
@@ -72,7 +93,7 @@ const i18n = useI18n();
const t = i18n.t; const t = i18n.t;
import { ref, onMounted, useTemplateRef } from 'vue'; 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 { TOKEN_TYPES } from '../constants.js';
import AppPasswords from './AppPasswords.vue'; import AppPasswords from './AppPasswords.vue';
import Card from './Card.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 // Init
onMounted(async () => { onMounted(async () => {
user.value = await profileModel.get(); user.value = await profileModel.get();

View File

@@ -124,6 +124,39 @@ function create(origin, accessToken) {
return null; 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];
},
}; };
} }