2fa: refactor into separate dialog
also rename routes to totp
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user