Skip to content

Commit 021db0d

Browse files
committed
refactor: use composable
1 parent 08b9471 commit 021db0d

File tree

4 files changed

+71
-57
lines changed

4 files changed

+71
-57
lines changed

resources/js/components/TwoFactorRecoveryCodes.vue

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
11
<script setup lang="ts">
22
import { Button } from '@/components/ui/button';
33
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4+
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
45
import { Form } from '@inertiajs/vue3';
56
import { Eye, EyeOff, LockKeyhole, RefreshCw } from 'lucide-vue-next';
67
import { nextTick, onMounted, ref } from 'vue';
78
8-
const recoveryCodesList = ref<string[]>([]);
9+
const { recoveryCodesList, fetchRecoveryCodes } = useTwoFactorAuth();
910
const isRecoveryCodesVisible = ref(false);
1011
const recoveryCodeSectionRef = ref<HTMLDivElement | null>(null);
1112
12-
const fetchRecoveryCodes = async () => {
13-
try {
14-
const response = await fetch(route('two-factor.recovery-codes'), {
15-
headers: { Accept: 'application/json' },
16-
});
17-
recoveryCodesList.value = await response.json();
18-
} catch (error) {
19-
console.error('Failed to fetch recovery codes:', error);
20-
}
21-
};
22-
2313
const toggleRecoveryCodesVisibility = async () => {
2414
const isCurrentlyHidden = !isRecoveryCodesVisible.value;
2515
const hasNoCodes = !recoveryCodesList.value.length;
@@ -46,9 +36,7 @@ onMounted(async () => {
4636
<template>
4737
<Card>
4838
<CardHeader>
49-
<CardTitle class="flex gap-3">
50-
<LockKeyhole class="size-4" />2FA Recovery Codes
51-
</CardTitle>
39+
<CardTitle class="flex gap-3"> <LockKeyhole class="size-4" />2FA Recovery Codes </CardTitle>
5240
<CardDescription>
5341
Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.
5442
</CardDescription>
@@ -65,7 +53,6 @@ onMounted(async () => {
6553
:action="route('two-factor.recovery-codes')"
6654
method="post"
6755
:options="{ preserveScroll: true }"
68-
@before="recoveryCodesList = []"
6956
@success="fetchRecoveryCodes"
7057
#default="{ processing }"
7158
>
@@ -75,24 +62,19 @@ onMounted(async () => {
7562
</Button>
7663
</Form>
7764
</div>
78-
<div
79-
:class="[
80-
'relative overflow-hidden transition-all duration-300',
81-
isRecoveryCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0',
82-
]"
83-
>
65+
<div :class="['relative overflow-hidden transition-all duration-300', isRecoveryCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0']">
8466
<div class="mt-3 space-y-3">
8567
<div ref="recoveryCodeSectionRef" class="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm">
8668
<div v-if="!recoveryCodesList.length" class="space-y-2">
87-
<div v-for="n in 8" :key="n" class="h-4 bg-muted-foreground/20 rounded animate-pulse"></div>
69+
<div v-for="n in 8" :key="n" class="h-4 animate-pulse rounded bg-muted-foreground/20"></div>
8870
</div>
8971
<div v-else v-for="(code, index) in recoveryCodesList" :key="index">
9072
{{ code }}
9173
</div>
9274
</div>
9375
<p class="text-xs text-muted-foreground select-none">
94-
Each can be used once to access your account and
95-
will be removed after use. If you need more, click <span class="font-bold">Regenerate Codes</span> above.
76+
Each can be used once to access your account and will be removed after use. If you need more, click
77+
<span class="font-bold">Regenerate Codes</span> above.
9678
</p>
9779
</div>
9880
</div>

resources/js/components/TwoFactorSetupModal.vue

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
44
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
55
import { PinInput, PinInputGroup, PinInputSlot } from '@/components/ui/pin-input';
66
import { useClipboard } from '@/composables/useClipboard';
7+
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
78
import { Form } from '@inertiajs/vue3';
89
import { Check, Copy, Loader2, ScanLine } from 'lucide-vue-next';
910
import { computed, nextTick, ref, watch } from 'vue';
@@ -22,12 +23,7 @@ const props = defineProps<Props>();
2223
const emit = defineEmits<Emits>();
2324
2425
const { recentlyCopied, copyToClipboard } = useClipboard();
25-
26-
const qrCodeSvg = ref<string | null>(null);
27-
const manualSetupKey = ref<string | null>(null);
28-
const hasSetupData = computed(() => {
29-
return qrCodeSvg.value !== null && manualSetupKey.value !== null;
30-
});
26+
const { qrCodeSvg, manualSetupKey, fetchSetupData } = useTwoFactorAuth();
3127
3228
const showVerificationStep = ref(false);
3329
const code = ref<number[]>([]);
@@ -72,23 +68,6 @@ const handleModalNextStep = () => {
7268
return;
7369
};
7470
75-
const fetchSetupData = async () => {
76-
try {
77-
const [qrResponse, keyResponse] = await Promise.all([
78-
fetch(route('two-factor.qr-code'), { headers: { Accept: 'application/json' } }),
79-
fetch(route('two-factor.secret-key'), { headers: { Accept: 'application/json' } }),
80-
]);
81-
82-
const { svg } = await qrResponse.json();
83-
const { secretKey } = await keyResponse.json();
84-
85-
qrCodeSvg.value = svg;
86-
manualSetupKey.value = secretKey;
87-
} catch (error) {
88-
console.error('Failed to fetch 2FA setup data:', error);
89-
}
90-
};
91-
9271
watch(
9372
() => props.isOpen,
9473
(isOpen) => {
@@ -97,8 +76,6 @@ watch(
9776
}
9877
},
9978
);
100-
101-
defineExpose({ hasSetupData });
10279
</script>
10380

10481
<template>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ref } from 'vue';
2+
3+
async function fetchApi<T>(url: string): Promise<T> {
4+
const response = await fetch(url, {
5+
headers: { Accept: 'application/json' },
6+
});
7+
8+
if (!response.ok) {
9+
throw new Error(`Failed to fetch: ${response.status}`);
10+
}
11+
12+
return response.json();
13+
}
14+
15+
export const useTwoFactorAuth = () => {
16+
const qrCodeSvg = ref<string | null>(null);
17+
const manualSetupKey = ref<string | null>(null);
18+
const recoveryCodesList = ref<string[]>([]);
19+
20+
const fetchQrCode = async (): Promise<void> => {
21+
const { svg } = await fetchApi<{ svg: string; url: string }>(route('two-factor.qr-code'));
22+
qrCodeSvg.value = svg;
23+
};
24+
25+
const fetchSetupKey = async (): Promise<void> => {
26+
const { secretKey } = await fetchApi<{ secretKey: string }>(route('two-factor.secret-key'));
27+
manualSetupKey.value = secretKey;
28+
};
29+
30+
const fetchRecoveryCodes = async (): Promise<void> => {
31+
try {
32+
recoveryCodesList.value = await fetchApi<string[]>(route('two-factor.recovery-codes'));
33+
} catch (error) {
34+
console.error('Failed to fetch recovery codes:', error);
35+
recoveryCodesList.value = [];
36+
}
37+
};
38+
39+
const fetchSetupData = async (): Promise<void> => {
40+
try {
41+
await Promise.all([fetchQrCode(), fetchSetupKey()]);
42+
} catch (error) {
43+
console.error('Failed to fetch setup data:', error);
44+
qrCodeSvg.value = null;
45+
manualSetupKey.value = null;
46+
}
47+
};
48+
49+
return {
50+
qrCodeSvg,
51+
manualSetupKey,
52+
recoveryCodesList,
53+
fetchQrCode,
54+
fetchSetupKey,
55+
fetchSetupData,
56+
fetchRecoveryCodes,
57+
};
58+
};

resources/js/pages/settings/TwoFactor.vue

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import TwoFactorRecoveryCodes from '@/components/TwoFactorRecoveryCodes.vue';
44
import TwoFactorSetupModal from '@/components/TwoFactorSetupModal.vue';
55
import { Badge } from '@/components/ui/badge';
66
import { Button } from '@/components/ui/button';
7+
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
78
import AppLayout from '@/layouts/AppLayout.vue';
89
import SettingsLayout from '@/layouts/settings/Layout.vue';
910
import { BreadcrumbItem } from '@/types';
@@ -28,6 +29,7 @@ const breadcrumbs: BreadcrumbItem[] = [
2829
},
2930
];
3031
32+
const { qrCodeSvg, manualSetupKey } = useTwoFactorAuth();
3133
const showSetupModal = ref(false);
3234
const setupModalRef = ref<InstanceType<typeof TwoFactorSetupModal>>();
3335
</script>
@@ -47,7 +49,7 @@ const setupModalRef = ref<InstanceType<typeof TwoFactorSetupModal>>();
4749
</p>
4850

4951
<div>
50-
<Button v-if="requiresConfirmation && setupModalRef?.hasSetupData" @click="showSetupModal = true">
52+
<Button v-if="requiresConfirmation && qrCodeSvg && manualSetupKey" @click="showSetupModal = true">
5153
<ShieldCheck />Enable
5254
</Button>
5355
<Form v-else :action="route('two-factor.enable')" method="post" @success="showSetupModal = true" #default="{ processing }">
@@ -66,12 +68,7 @@ const setupModalRef = ref<InstanceType<typeof TwoFactorSetupModal>>();
6668
<TwoFactorRecoveryCodes />
6769

6870
<div class="relative inline">
69-
<Form
70-
:action="route('two-factor.disable')"
71-
:async="false"
72-
method="delete"
73-
#default="{ processing }"
74-
>
71+
<Form :action="route('two-factor.disable')" :async="false" method="delete" #default="{ processing }">
7572
<Button variant="destructive" type="submit" :disabled="processing">
7673
<ShieldBan />
7774
{{ processing ? 'Disabling...' : 'Disable 2FA' }}

0 commit comments

Comments
 (0)