2fa: refactor into separate dialog

also rename routes to totp
This commit is contained in:
Girish Ramakrishnan
2026-02-22 10:43:15 +01:00
parent a98dbfdf4f
commit d0f0bb799e
6 changed files with 147 additions and 117 deletions
@@ -0,0 +1,127 @@
<script setup>
import { ref, useTemplateRef } from 'vue';
import { Button, ButtonGroup, Dialog, TextInput, InputGroup, FormGroup } from '@cloudron/pankow';
import { startRegistration } from '@simplewebauthn/browser';
import ProfileModel from '../models/ProfileModel.js';
const props = defineProps({
mandatory2FA: { type: Boolean, default: false },
has2FA: { type: Boolean, default: false }
});
const emit = defineEmits([ 'success' ]);
const profileModel = ProfileModel.create();
const dialog = useTemplateRef('dialog');
const setupMode = ref('');
const totpSecret = ref('');
const totpToken = ref('');
const totpQRCode = ref('');
const totpEnableError = ref('');
const passkeyRegisterError = ref('');
const passkeyRegisterBusy = ref(false);
async function onTotpEnable() {
const [error] = await profileModel.enableTotp(totpToken.value);
if (error) {
totpToken.value = '';
return totpEnableError.value = error.body ? error.body.message : 'Internal error';
}
emit('success');
dialog.value.close();
}
async function onRegisterPasskey() {
passkeyRegisterBusy.value = true;
passkeyRegisterError.value = '';
try {
const [optionsError, options] = await profileModel.getPasskeyRegistrationOptions();
if (optionsError) {
passkeyRegisterError.value = optionsError.body?.message || 'Failed to get registration options';
passkeyRegisterBusy.value = false;
return;
}
const credential = await startRegistration({ optionsJSON: options });
const [registerError] = await profileModel.registerPasskey(credential, 'Cloudron');
if (registerError) {
passkeyRegisterError.value = registerError.body?.message || 'Failed to register passkey';
passkeyRegisterBusy.value = false;
return;
}
emit('success');
dialog.value.close();
} catch (error) {
passkeyRegisterError.value = error.message || 'Passkey registration failed';
}
passkeyRegisterBusy.value = false;
}
defineExpose({
async open() {
setupMode.value = '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;
},
close() {
dialog.value.close();
}
});
</script>
<template>
<Dialog ref="dialog" :title="$t('profile.enable2FA.title')" :dismissable="!props.mandatory2FA || props.has2FA">
<div style="text-align: center;">
<p class="text-warning" v-if="props.mandatory2FA && !props.has2FA">{{ $t('profile.enable2FA.mandatorySetup') }}</p>
<ButtonGroup>
<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'" style="text-align: center; margin-top: 20px;">
<Button @click="onRegisterPasskey()" :loading="passkeyRegisterBusy" :disabled="passkeyRegisterBusy">{{ $t('profile.enable2FA.registerPasskey') }}</Button>
<div class="error-label" v-if="passkeyRegisterError">{{ passkeyRegisterError }}</div>
</div>
<!-- TOTP Setup -->
<div v-if="setupMode === 'totp'">
<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>
<div style="text-align: center;">
<img :src="totpQRCode" style="border-radius: 10px; margin-bottom: 10px"/>
<small>{{ totpSecret }}</small>
</div>
<br/>
<form @submit.prevent="onTotpEnable()">
<input type="submit" style="display: none;" :disabled="!totpToken"/>
<FormGroup style="text-align: left;">
<label for="totpTokenInput">{{ $t('profile.enable2FA.token') }}</label>
<InputGroup>
<TextInput v-model="totpToken" id="totpTokenInput" style="flex-grow: 1;"/>
<Button @click="onTotpEnable()" :disabled="!totpToken">{{ $t('profile.enable2FA.enable') }}</Button>
</InputGroup>
<div class="error-label" v-if="totpEnableError">{{ totpEnableError }}</div>
</FormGroup>
</form>
</div>
</div>
</Dialog>
</template>