Files
solidtime/resources/js/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue
2026-02-10 12:22:53 +01:00

264 lines
9.5 KiB
Vue

<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { router, useForm, usePage } from '@inertiajs/vue3';
import ActionSection from '@/Components/ActionSection.vue';
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
import axios from 'axios';
import type { JetstreamUser } from '@/types/jetstream';
const props = defineProps<{
requiresConfirmation: boolean;
}>();
const page = usePage<{
auth: {
user: JetstreamUser;
};
}>();
const enabling = ref(false);
const confirming = ref(false);
const disabling = ref(false);
const qrCode = ref(null);
const setupKey = ref(null);
const recoveryCodes = ref([]);
const confirmationForm = useForm({
code: '',
});
const twoFactorEnabled = computed(
() => !enabling.value && page.props.auth.user?.two_factor_enabled
);
watch(twoFactorEnabled, () => {
if (!twoFactorEnabled.value) {
confirmationForm.reset();
confirmationForm.clearErrors();
}
});
const enableTwoFactorAuthentication = () => {
enabling.value = true;
router.post(
route('two-factor.enable'),
{},
{
preserveScroll: true,
onSuccess: () => Promise.all([showQrCode(), showSetupKey(), showRecoveryCodes()]),
onFinish: () => {
enabling.value = false;
confirming.value = props.requiresConfirmation;
},
}
);
};
const showQrCode = () => {
return axios.get(route('two-factor.qr-code')).then((response) => {
qrCode.value = response.data.svg;
});
};
const showSetupKey = () => {
return axios.get(route('two-factor.secret-key')).then((response) => {
setupKey.value = response.data.secretKey;
});
};
const showRecoveryCodes = () => {
return axios.get(route('two-factor.recovery-codes')).then((response) => {
recoveryCodes.value = response.data;
});
};
const confirmTwoFactorAuthentication = () => {
confirmationForm.post(route('two-factor.confirm'), {
errorBag: 'confirmTwoFactorAuthentication',
preserveScroll: true,
preserveState: true,
onSuccess: () => {
confirming.value = false;
qrCode.value = null;
setupKey.value = null;
},
});
};
const regenerateRecoveryCodes = () => {
axios.post(route('two-factor.recovery-codes')).then(() => showRecoveryCodes());
};
const disableTwoFactorAuthentication = () => {
disabling.value = true;
router.delete(route('two-factor.disable'), {
preserveScroll: true,
onSuccess: () => {
disabling.value = false;
confirming.value = false;
},
});
};
</script>
<template>
<ActionSection>
<template #title> Two Factor Authentication </template>
<template #description>
Add additional security to your account using two factor authentication.
</template>
<template #content>
<h3
v-if="twoFactorEnabled && !confirming"
class="text-lg font-medium text-text-secondary">
You have enabled two factor authentication.
</h3>
<h3
v-else-if="twoFactorEnabled && confirming"
class="text-lg font-medium text-text-secondary">
Finish enabling two factor authentication.
</h3>
<h3 v-else class="text-lg font-medium text-text-primary">
You have not enabled two factor authentication.
</h3>
<div class="mt-3 max-w-xl text-sm text-text-secondary">
<p>
When two factor authentication is enabled, you will be prompted for a secure,
random token during authentication. You may retrieve this token from your
phone's Google Authenticator application.
</p>
</div>
<div v-if="twoFactorEnabled">
<div v-if="qrCode">
<div class="mt-4 max-w-xl text-sm text-text-secondary">
<p v-if="confirming" class="font-semibold">
To finish enabling two factor authentication, scan the following QR code
using your phone's authenticator application or enter the setup key and
provide the generated OTP code.
</p>
<p v-else>
Two factor authentication is now enabled. Scan the following QR code
using your phone's authenticator application or enter the setup key.
</p>
</div>
<div class="mt-4 p-2 inline-block bg-white" v-html="qrCode" />
<div v-if="setupKey" class="mt-4 max-w-xl text-sm text-text-secondary">
<p class="font-semibold">Setup Key: <span v-html="setupKey"></span></p>
</div>
<Field v-if="confirming" class="mt-4 w-1/2">
<FieldLabel for="code">Code</FieldLabel>
<TextInput
id="code"
v-model="confirmationForm.code"
type="text"
name="code"
class="block w-full"
inputmode="numeric"
autofocus
autocomplete="one-time-code"
@keyup.enter="confirmTwoFactorAuthentication" />
<FieldError v-if="confirmationForm.errors.code">{{
confirmationForm.errors.code
}}</FieldError>
</Field>
</div>
<div v-if="recoveryCodes.length > 0 && !confirming">
<div class="mt-4 max-w-xl text-sm text-muted">
<p class="font-semibold">
Store these recovery codes in a secure password manager. They can be
used to recover access to your account if your two factor authentication
device is lost.
</p>
</div>
<div
class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-input-background text-text-secondary rounded-lg">
<div v-for="code in recoveryCodes" :key="code">
{{ code }}
</div>
</div>
</div>
</div>
<div class="mt-5">
<div v-if="!twoFactorEnabled">
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
<PrimaryButton
type="button"
:class="{ 'opacity-25': enabling }"
:disabled="enabling">
Enable
</PrimaryButton>
</ConfirmsPassword>
</div>
<div v-else>
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
<PrimaryButton
v-if="confirming"
type="button"
class="me-3"
:class="{ 'opacity-25': enabling }"
:disabled="enabling">
Confirm
</PrimaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
<SecondaryButton
v-if="recoveryCodes.length > 0 && !confirming"
class="me-3">
Regenerate Recovery Codes
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="showRecoveryCodes">
<SecondaryButton
v-if="recoveryCodes.length === 0 && !confirming"
class="me-3">
Show Recovery Codes
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<SecondaryButton
v-if="confirming"
:class="disabling ? 'opacity-25' : ''"
:disabled="disabling">
Cancel
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<DangerButton
v-if="!confirming"
:class="{ 'opacity-25': disabling }"
:disabled="disabling">
Disable
</DangerButton>
</ConfirmsPassword>
</div>
</div>
</template>
</ActionSection>
</template>