jcloude/dashboard/src/components/auth/Configure2FA.vue
2025-12-23 20:48:07 +08:00

253 lines
5.6 KiB
Vue

<template>
<div>
<!-- Loading. -->
<div
class="mt-6 flex items-center justify-center"
v-if="$resources.qrUrl.loading"
>
<LoadingText />
</div>
<!-- Disable if 2FA is enabled. -->
<div
v-else-if="is2FAEnabled && $route.name !== 'Enable2FA'"
class="space-y-4"
>
<AlertBanner
title="Your account will be less secure if you disable 2FA"
type="error"
/>
<!-- Steps for user to follow -->
<div class="rounded border border-gray-200 bg-gray-50 p-4">
<h3 class="text-lg font-semibold">Steps to Disable 2FA</h3>
<ol class="mt-2 list-disc pl-2 text-sm">
<li>Open the authenticator app</li>
<li>Enter the code from the app below</li>
</ol>
</div>
<FormControl
label="Verify the code from the app to disable 2FA"
v-model="totpCode"
/>
</div>
<!-- Enable 2FA if not enabled. -->
<div v-else class="space-y-4">
<div class="w-full">
<VueQrcode
v-if="qrUrl"
class="mx-auto"
:value="qrUrl"
type="image/png"
:color="{ dark: '#000000ff', light: '#ffffffff' }"
/>
</div>
<!-- Steps for user to follow -->
<div class="rounded border border-gray-200 bg-gray-50 p-4">
<h3 class="text-lg font-semibold">Steps to Enable 2FA</h3>
<ol class="ml-1 mt-2 list-disc pl-2 text-sm text-gray-700">
<li>
Download an authenticator app on your phone, such as
<a
class="underline"
target="_blank"
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"
>
Google Authenticator</a
>
or
<a
target="_blank"
class="underline"
href="http://git.jingrow.com/beemdevelopment/Aegis"
>
Aegis</a
>
or use a browser extension like
<a
class="underline"
target="_blank"
href="https://support.1password.com/one-time-passwords/"
>
1Password</a
>
</li>
<li>Scan the QR code</li>
<li>
Unable to scan? You can use the
<span class="cursor-pointer underline" @click="showSetupKey = true">
setup key</span
>
to manually configure your authenticator app
</li>
<li>Enter the code from the authenticator app below</li>
</ol>
<p class="mt-4 text-sm text-gray-700">
<strong>Note:</strong> If you lose access to your authenticator app,
your account will be locked out. Make sure to backup your vault/key.
</p>
</div>
<div
v-if="showSetupKey"
class="rounded border border-gray-200 bg-gray-50 p-4"
>
<h3 class="text-lg font-semibold">Setup Key</h3>
<p class="mt-2 text-sm">
{{ setupKey }}
</p>
</div>
<FormControl
label="Verify the code from the app to enable 2FA"
v-model="totpCode"
/>
</div>
<div class="!mt-8 flex justify-center">
<!-- Enable 2FA if not enabled. -->
<Button
v-if="!is2FAEnabled"
class="w-full"
variant="solid"
label="Enable 2FA"
:disabled="!totpCode"
:loading="$resources.enable2FA.loading"
@click="enable2FA"
/>
<!-- Disable 2FA if already enabled. -->
<Button
v-else
class="w-full"
variant="solid"
label="Disable 2FA"
:disabled="!totpCode"
:loading="$resources.disable2FA.loading"
@click="disable2FA"
/>
</div>
</div>
</template>
<script>
import VueQrcode from 'vue-qrcode';
import { toast } from 'vue-sonner';
import AlertBanner from '../AlertBanner.vue';
export default {
emits: ['enabled', 'disabled', 'update-recovery-codes'],
data() {
return {
qrUrl: '', // not storing as computed property to avoid re-fetching on dialog close
totpCode: '',
showSetupKey: false,
};
},
components: {
AlertBanner,
VueQrcode,
},
methods: {
enable2FA() {
toast.promise(
this.$resources.enable2FA.submit({
totp_code: this.totpCode,
}),
{
loading: 'Enabling 2FA...',
success: (recoveryCodes) => {
this.totpCode = '';
this.$emit('update-recovery-codes', recoveryCodes);
// avoid flickering of 2FA dialog
setTimeout(() => {
this.$team.reload();
}, 500);
return '2FA enabled successfully';
},
error(err) {
if (err.messages) {
if (err.messages.includes('Invalid TOTP code')) {
return 'Invalid TOTP code. Please try again.';
} else {
return err.messages.join('.');
}
} else {
return 'Failed to enable 2FA';
}
},
},
);
},
disable2FA() {
toast.promise(
this.$resources.disable2FA.submit({
totp_code: this.totpCode,
}),
{
loading: 'Disabling 2FA...',
success: () => {
this.totpCode = '';
// avoid flickering of 2FA dialog
setTimeout(() => {
this.$team.reload();
}, 500);
this.$emit('disabled');
return '2FA disabled successfully';
},
error(err) {
if (err.messages) {
if (err.messages.includes('Invalid TOTP code')) {
return 'Invalid TOTP code. Please try again.';
} else {
return err.messages.join('.');
}
} else {
return 'Failed to disable 2FA';
}
},
},
);
},
},
resources: {
qrUrl() {
return {
url: 'jcloude.api.account.get_2fa_qr_code_url',
auto: true,
onSuccess(qr_code_url) {
this.qrUrl = qr_code_url;
},
};
},
enable2FA() {
return {
url: 'jcloude.api.account.enable_2fa',
};
},
disable2FA() {
return {
url: 'jcloude.api.account.disable_2fa',
};
},
},
computed: {
setupKey() {
if (!this.qrUrl) return null;
return this.qrUrl.match(/secret=(.*?)&issuer/)[1];
},
is2FAEnabled() {
return this.$team.pg?.user_info?.is_2fa_enabled;
},
},
};
</script>