130 lines
5.3 KiB
Vue
130 lines
5.3 KiB
Vue
<script setup>
|
|
|
|
import { ref, useTemplateRef } from 'vue';
|
|
import { Button, ButtonGroup, ClipboardAction, 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>
|
|
<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>
|
|
<div style="text-align: center;">
|
|
<Button @click="onRegisterPasskey()" :loading="passkeyRegisterBusy" :disabled="passkeyRegisterBusy">{{ $t('profile.enable2FA.registerPasskey') }}</Button>
|
|
<div class="error-label" v-if="passkeyRegisterError">{{ passkeyRegisterError }}</div>
|
|
</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 }} <ClipboardAction plain :value="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>
|