allow totp and passkey to co-exist

This commit is contained in:
Girish Ramakrishnan
2026-03-16 16:38:48 +05:30
parent 009d0b39f9
commit 189e3d5599
4 changed files with 45 additions and 29 deletions
+24 -13
View File
@@ -1,7 +1,7 @@
<script setup>
import { ref, useTemplateRef } from 'vue';
import { Button, ButtonGroup, ClipboardAction, Dialog, TextInput, InputGroup, FormGroup } from '@cloudron/pankow';
import { Button, ClipboardAction, Dialog, TextInput, InputGroup, FormGroup } from '@cloudron/pankow';
import { startRegistration } from '@simplewebauthn/browser';
import ProfileModel from '../models/ProfileModel.js';
@@ -64,37 +64,42 @@ async function onRegisterPasskey() {
passkeyRegisterBusy.value = false;
}
async function loadTotpSecret() {
const [error, result] = await profileModel.setTotpSecret();
if (error) return console.error(error);
totpSecret.value = result.secret;
totpQRCode.value = result.qrcode;
}
defineExpose({
async open() {
setupMode.value = 'passkey';
async open(method) {
setupMode.value = method || 'passkey';
totpEnableError.value = '';
totpToken.value = '';
passkeyRegisterError.value = '';
dialog.value.open();
const [error, result] = await profileModel.setTotpSecret();
if (error) return console.error(error);
totpSecret.value = result.secret;
totpQRCode.value = result.qrcode;
if (setupMode.value === 'totp') await loadTotpSecret();
},
close() {
dialog.value.close();
}
});
async function switchMode(mode) {
setupMode.value = mode;
if (mode === 'totp' && !totpSecret.value) await loadTotpSecret();
}
</script>
<template>
<Dialog ref="dialog" :title="$t('profile.enable2FA.title')" :dismissable="!props.mandatory2FA || props.has2FA">
<Dialog ref="dialog" :title="setupMode === 'totp' ? $t('profile.enableTotp.title') : $t('profile.enablePasskey.title')" :dismissable="!props.mandatory2FA || props.has2FA">
<div>
<p class="text-warning" v-if="props.mandatory2FA && !props.has2FA">{{ $t('profile.enable2FA.mandatorySetup') }}</p>
<ButtonGroup style="display: flex; justify-content: center;"> <Button secondary @click="setupMode = 'passkey'" :outline="setupMode !== 'passkey' || null">{{ $t('profile.enable2FA.passkeyOption') }}</Button>
<Button secondary @click="setupMode = 'totp'" :outline="setupMode !== 'totp' || null">{{ $t('profile.enable2FA.totpOption') }}</Button>
</ButtonGroup>
<!-- Passkey Setup -->
<div v-if="setupMode === 'passkey'">
<p v-html="$t('profile.enable2FA.passkeyDescription', { 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>
@@ -102,6 +107,9 @@ defineExpose({
<Button @click="onRegisterPasskey()" :loading="passkeyRegisterBusy" :disabled="passkeyRegisterBusy">{{ $t('profile.enable2FA.registerPasskey') }}</Button>
<div class="error-label" v-if="passkeyRegisterError">{{ passkeyRegisterError }}</div>
</div>
<p v-if="props.mandatory2FA && !props.has2FA" style="text-align: center; margin-top: 15px;">
<a href="#" @click.prevent="switchMode('totp')">{{ $t('profile.enable2FA.switchToTotp') }}</a>
</p>
</div>
<!-- TOTP Setup -->
@@ -123,6 +131,9 @@ defineExpose({
<div class="error-label" v-if="totpEnableError">{{ totpEnableError }}</div>
</FormGroup>
</form>
<p v-if="props.mandatory2FA && !props.has2FA" style="text-align: center; margin-top: 15px;">
<a href="#" @click.prevent="switchMode('passkey')">{{ $t('profile.enable2FA.switchToPasskey') }}</a>
</p>
</div>
</div>
</Dialog>
+21 -9
View File
@@ -110,8 +110,8 @@ async function loadPasskey() {
userPasskey.value = result;
}
async function onOpenTwoFASetupDialog() {
enableTwoFADialog.value.open();
async function onOpenTwoFASetupDialog(method) {
enableTwoFADialog.value.open(method);
}
async function onEnableTwoFASuccess() {
@@ -119,8 +119,8 @@ async function onEnableTwoFASuccess() {
await loadPasskey();
}
async function onTwoFADisable() {
disableTwoFADialog.value.open(userPasskey.value ? 'passkey' : 'totp');
async function onTwoFADisable(method) {
disableTwoFADialog.value.open(method);
}
async function onTwoFADisableSuccess() {
@@ -230,13 +230,25 @@ onMounted(async () => {
<SettingsItem v-if="!profile.source || !config.external2FA">
<FormGroup>
<label>{{ $t('profile.twoFactorAuth.title') }}</label>
<div v-if="!has2FA">{{ $t('profile.twoFactorAuth.disabled') }}</div>
<div v-else-if="profile.totpEnabled">{{ $t('profile.twoFactorAuth.totpEnabled') }} <i class="fa-solid fa-check text-success"></i></div>
<div v-else-if="userPasskey">{{ $t('profile.twoFactorAuth.passkeyEnabled') }} <i class="fa-solid fa-check text-success"></i></div>
<label>{{ $t('profile.twoFactorAuth.totpTitle') }}</label>
<div v-if="profile.totpEnabled">{{ $t('profile.twoFactorAuth.totpEnabled') }} <i class="fa-solid fa-check text-success"></i></div>
<div v-else>{{ $t('profile.twoFactorAuth.disabled') }}</div>
</FormGroup>
<div style="display: flex; align-items: center">
<Button tool plain @click="has2FA ? onTwoFADisable() : onOpenTwoFASetupDialog()">{{ $t(has2FA ? 'profile.disable2FAAction' : 'profile.enable2FAAction') }}</Button>
<Button tool plain v-if="profile.totpEnabled" @click="onTwoFADisable('totp')">{{ $t('profile.disable2FAAction') }}</Button>
<Button tool plain v-else @click="onOpenTwoFASetupDialog('totp')">{{ $t('profile.enable2FAAction') }}</Button>
</div>
</SettingsItem>
<SettingsItem v-if="!profile.source || !config.external2FA">
<FormGroup>
<label>{{ $t('profile.twoFactorAuth.passkeyTitle') }}</label>
<div v-if="userPasskey">{{ $t('profile.twoFactorAuth.passkeyEnabled') }} <i class="fa-solid fa-check text-success"></i></div>
<div v-else>{{ $t('profile.twoFactorAuth.disabled') }}</div>
</FormGroup>
<div style="display: flex; align-items: center">
<Button tool plain v-if="userPasskey" @click="onTwoFADisable('passkey')">{{ $t('profile.disable2FAAction') }}</Button>
<Button tool plain v-else @click="onOpenTwoFASetupDialog('passkey')">{{ $t('profile.enable2FAAction') }}</Button>
</div>
</SettingsItem>
</div>