# All files / js/components TwoFactorRecoveryCodes.vue

0% Statements 0/26
0% Branches 0/25
0% Functions 0/12
0% Lines 0/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120

     <script setup lang="ts">
    import { Button } from '@/components/ui/button';
    import {
        Card,
        CardContent,
        CardDescription,
        CardHeader,
        CardTitle,
    } from '@/components/ui/card';
    import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
    import { regenerateRecoveryCodes } from '@/routes/two-factor';
    import { Form } from '@inertiajs/vue3';
    import { Eye, EyeOff, LockKeyhole, RefreshCw } from 'lucide-vue-next';
    import { nextTick, onMounted, ref } from 'vue';

    const { recoveryCodesList, fetchRecoveryCodes } = useTwoFactorAuth();
    const isRecoveryCodesVisible = ref<boolean>(false);
    const recoveryCodeSectionRef = ref<HTMLDivElement | null>(null);

    const toggleRecoveryCodesVisibility = async () => {
        if (!isRecoveryCodesVisible.value && !recoveryCodesList.value.length) {
            await fetchRecoveryCodes();
        }

        isRecoveryCodesVisible.value = !isRecoveryCodesVisible.value;

        if (isRecoveryCodesVisible.value) {
            await nextTick();
            recoveryCodeSectionRef.value?.scrollIntoView({ behavior: 'smooth' });
        }
    };

    onMounted(async () => {
        if (!recoveryCodesList.value.length) {
            await fetchRecoveryCodes();
        }
    });
    </script>

    <template>
        <Card>
            <CardHeader>
                <CardTitle class="flex gap-3">
                    <LockKeyhole class="size-4" />2FA Recovery Codes
                </CardTitle>
                <CardDescription>
                    Recovery codes let you regain access if you lose your 2FA
                    device. Store them in a secure password manager.
                </CardDescription>
            </CardHeader>
            <CardContent>
                <div
                    class="flex flex-col gap-3 select-none sm:flex-row sm:items-center sm:justify-between"
                >
                    <Button @click="toggleRecoveryCodesVisibility" class="w-fit">
                        <component
                            :is="isRecoveryCodesVisible ? EyeOff : Eye"
                            class="size-4"
                        />
                        {{ isRecoveryCodesVisible ? 'Hide' : 'View' }} Recovery
                        Codes
                    </Button>

                    <Form
                        v-if="isRecoveryCodesVisible"
                        v-bind="regenerateRecoveryCodes.form()"
                        method="post"
                        :options="{ preserveScroll: true }"
                        @success="fetchRecoveryCodes"
                        #default="{ processing }"
                    >
                        <Button
                            variant="secondary"
                            type="submit"
                            :disabled="processing"
                        >
                            <RefreshCw /> Regenerate Codes
                        </Button>
                    </Form>
                </div>
                <div
                    :class="[
                        'relative overflow-hidden transition-all duration-300',
                        isRecoveryCodesVisible
                            ? 'h-auto opacity-100'
                            : 'h-0 opacity-0',
                    ]"
                >
                    <div class="mt-3 space-y-3">
                        <div
                            ref="recoveryCodeSectionRef"
                            class="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm"
                        >
                            <div v-if="!recoveryCodesList.length" class="space-y-2">
                                <div
                                    v-for="n in 8"
                                    :key="n"
                                    class="h-4 animate-pulse rounded bg-muted-foreground/20"
                                ></div>
                            </div>
                            <div
                                v-else
                                v-for="(code, index) in recoveryCodesList"
                                :key="index"
                            >
                                {{ code }}
                            </div>
                        </div>
                        <p class="text-xs text-muted-foreground select-none">
                            Each recovery code can be used once to access your
                            account and will be removed after use. If you need more,
                            click
                            <span class="font-bold">Regenerate Codes</span> above.
                        </p>
                    </div>
                </div>
            </CardContent>
        </Card>
    </template>