253 lines
5.6 KiB
Vue
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>
|