Visual changes to profile page
This commit is contained in:
@@ -1,17 +1,24 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { router, useForm, usePage } from '@inertiajs/vue3';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Input } from '@/Components/ui/input';
|
||||
import { Label } from '@/Components/ui/label';
|
||||
import { Badge } from '@/Components/ui/badge';
|
||||
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import { Shield, Key, Copy, RefreshCw, CheckCircle, AlertCircle } from 'lucide-vue-next';
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { router, useForm, usePage } from "@inertiajs/vue3";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import ConfirmsPassword from "@/Components/ConfirmsPassword.vue";
|
||||
import InputError from "@/Components/InputError.vue";
|
||||
import { Shield, Key, Copy, RefreshCw, CheckCircle, AlertCircle } from "lucide-vue-next";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
requiresConfirmation: Boolean,
|
||||
requiresConfirmation: Boolean,
|
||||
});
|
||||
|
||||
const page = usePage();
|
||||
@@ -23,284 +30,298 @@ const setupKey = ref(null);
|
||||
const recoveryCodes = ref([]);
|
||||
|
||||
const confirmationForm = useForm({
|
||||
code: '',
|
||||
code: "",
|
||||
});
|
||||
|
||||
const twoFactorEnabled = computed(
|
||||
() => ! enabling.value && page.props.auth.user?.two_factor_enabled,
|
||||
() => !enabling.value && page.props.auth.user?.two_factor_enabled
|
||||
);
|
||||
|
||||
watch(twoFactorEnabled, () => {
|
||||
if (! twoFactorEnabled.value) {
|
||||
confirmationForm.reset();
|
||||
confirmationForm.clearErrors();
|
||||
}
|
||||
if (!twoFactorEnabled.value) {
|
||||
confirmationForm.reset();
|
||||
confirmationForm.clearErrors();
|
||||
}
|
||||
});
|
||||
|
||||
const enableTwoFactorAuthentication = () => {
|
||||
enabling.value = true;
|
||||
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;
|
||||
},
|
||||
});
|
||||
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;
|
||||
});
|
||||
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;
|
||||
});
|
||||
}
|
||||
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;
|
||||
});
|
||||
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;
|
||||
},
|
||||
});
|
||||
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());
|
||||
axios.post(route("two-factor.recovery-codes")).then(() => showRecoveryCodes());
|
||||
};
|
||||
|
||||
const disableTwoFactorAuthentication = () => {
|
||||
disabling.value = true;
|
||||
disabling.value = true;
|
||||
|
||||
router.delete(route('two-factor.disable'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
disabling.value = false;
|
||||
confirming.value = false;
|
||||
},
|
||||
});
|
||||
router.delete(route("two-factor.disable"), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
disabling.value = false;
|
||||
confirming.value = false;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-center gap-2">
|
||||
<Shield class="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle>Two Factor Authentication</CardTitle>
|
||||
<AppCard
|
||||
title=""
|
||||
padding="none"
|
||||
class="p-0! gap-0"
|
||||
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
||||
body-class="p-4 border-t"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<Shield size="18" />
|
||||
<CardTitle>Dvonivojska overitev</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Dodatna varnost za vaš račun z dvonivojsko overitvijo.
|
||||
</CardDescription>
|
||||
</template>
|
||||
|
||||
<!-- Status Header -->
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<h3
|
||||
v-if="twoFactorEnabled && !confirming"
|
||||
class="text-lg font-semibold flex items-center gap-2"
|
||||
>
|
||||
<CheckCircle class="h-5 w-5 text-green-600" />
|
||||
Dvonivojska overitev omogočena
|
||||
</h3>
|
||||
<h3
|
||||
v-else-if="twoFactorEnabled && confirming"
|
||||
class="text-lg font-semibold flex items-center gap-2"
|
||||
>
|
||||
<AlertCircle class="h-5 w-5 text-amber-600" />
|
||||
|
||||
Dokončaj namestitev dvonivojske overitve
|
||||
</h3>
|
||||
<h3 v-else class="text-lg font-semibold flex items-center gap-2">
|
||||
Dvonivojska overitev onemogočena
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code & Setup -->
|
||||
<div v-if="twoFactorEnabled" class="space-y-6">
|
||||
<div v-if="qrCode" class="space-y-4">
|
||||
<div class="rounded-lg border bg-muted/50 p-4">
|
||||
<p v-if="confirming" class="text-sm font-medium mb-4">
|
||||
Za dokončanje omogočanja dvostopenjske overitve skenirajte naslednjo QR-kodo z
|
||||
aplikacijo za preverjanje pristnosti na vašem telefonu ali vnesite
|
||||
namestitveno kodo in vpišite ustvarjeno OTP-kodo.
|
||||
</p>
|
||||
<p v-else class="text-sm text-muted-foreground mb-4">
|
||||
Dvonivojska overitev je zdaj omogočena. Skenirajte QR kodo z aplikacijo za
|
||||
preverjanje pristnosti na vašem telefonu ali vnesite namestitveni ključ.
|
||||
</p>
|
||||
|
||||
<!-- QR Code -->
|
||||
<div class="flex justify-center p-4 bg-white rounded-lg" v-html="qrCode" />
|
||||
|
||||
<!-- Setup Key -->
|
||||
<div v-if="setupKey" class="mt-4 p-3 bg-background rounded-lg border">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex-1">
|
||||
<Label class="text-xs text-muted-foreground">Namestitveni Ključ</Label>
|
||||
<p class="font-mono text-sm font-semibold mt-1" v-html="setupKey"></p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="copyToClipboard(setupKey)"
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Add additional security to your account using two factor authentication.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent class="space-y-6">
|
||||
<!-- Status Header -->
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<h3 v-if="twoFactorEnabled && ! confirming" class="text-lg font-semibold flex items-center gap-2">
|
||||
<CheckCircle class="h-5 w-5 text-green-600" />
|
||||
Two factor authentication is enabled
|
||||
</h3>
|
||||
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-semibold flex items-center gap-2">
|
||||
<AlertCircle class="h-5 w-5 text-amber-600" />
|
||||
Finish enabling two factor authentication
|
||||
</h3>
|
||||
<h3 v-else class="text-lg font-semibold flex items-center gap-2">
|
||||
<Shield class="h-5 w-5 text-muted-foreground" />
|
||||
Two factor authentication is disabled
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
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>
|
||||
<!-- Confirmation Code Input -->
|
||||
<div v-if="confirming" class="space-y-2">
|
||||
<Label for="code">Potrdite kodo</Label>
|
||||
<Input
|
||||
id="code"
|
||||
v-model="confirmationForm.code"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
placeholder="Enter 6-digit code"
|
||||
class="max-w-xs"
|
||||
@keyup.enter="confirmTwoFactorAuthentication"
|
||||
/>
|
||||
<InputError :message="confirmationForm.errors.code" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recovery Codes -->
|
||||
<div v-if="recoveryCodes.length > 0 && !confirming" class="space-y-4">
|
||||
<div
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<Key
|
||||
class="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<p class="text-sm font-medium text-amber-900 dark:text-amber-100">
|
||||
Shranite to obnovitveno kodo v upravitelja gesel. Lahko se uporabi za obnovo
|
||||
vstopa v vaš račun, če se izgubi naprava z dvostopenjskim overjanjem.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-muted p-4">
|
||||
<div class="grid grid-cols-2 gap-2 font-mono text-sm">
|
||||
<div
|
||||
v-for="code in recoveryCodes"
|
||||
:key="code"
|
||||
class="flex items-center justify-between p-2 bg-background rounded border"
|
||||
>
|
||||
<span>{{ code }}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 w-6 p-0"
|
||||
@click="copyToClipboard(code)"
|
||||
>
|
||||
<Copy class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-row gap-2 items-center justify-end w-full">
|
||||
<!-- Enable -->
|
||||
<div v-if="!twoFactorEnabled">
|
||||
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
|
||||
<Button type="button" :disabled="enabling">
|
||||
<Shield class="h-4 w-4 mr-2" />
|
||||
Enable
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
</div>
|
||||
|
||||
<!-- QR Code & Setup -->
|
||||
<div v-if="twoFactorEnabled" class="space-y-6">
|
||||
<div v-if="qrCode" class="space-y-4">
|
||||
<div class="rounded-lg border bg-muted/50 p-4">
|
||||
<p v-if="confirming" class="text-sm font-medium mb-4">
|
||||
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 class="text-sm text-muted-foreground mb-4">
|
||||
Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application or enter the setup key.
|
||||
</p>
|
||||
<!-- Confirm -->
|
||||
<template v-else>
|
||||
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
|
||||
<Button v-if="confirming" type="button" :disabled="enabling">
|
||||
<CheckCircle class="h-4 w-4 mr-2" />
|
||||
Confirm
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<!-- QR Code -->
|
||||
<div class="flex justify-center p-4 bg-white rounded-lg" v-html="qrCode" />
|
||||
<!-- Regenerate Recovery Codes -->
|
||||
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
|
||||
<Button
|
||||
v-if="recoveryCodes.length > 0 && !confirming"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
Regenerate Recovery Codes
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<!-- Setup Key -->
|
||||
<div v-if="setupKey" class="mt-4 p-3 bg-background rounded-lg border">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex-1">
|
||||
<Label class="text-xs text-muted-foreground">Setup Key</Label>
|
||||
<p class="font-mono text-sm font-semibold mt-1" v-html="setupKey"></p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="copyToClipboard(setupKey)"
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Show Recovery Codes -->
|
||||
<ConfirmsPassword @confirmed="showRecoveryCodes">
|
||||
<Button
|
||||
v-if="recoveryCodes.length === 0 && !confirming"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Key class="h-4 w-4 mr-2" />
|
||||
Show Recovery Codes
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<!-- Confirmation Code Input -->
|
||||
<div v-if="confirming" class="space-y-2">
|
||||
<Label for="code">Confirmation Code</Label>
|
||||
<Input
|
||||
id="code"
|
||||
v-model="confirmationForm.code"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
placeholder="Enter 6-digit code"
|
||||
class="max-w-xs"
|
||||
@keyup.enter="confirmTwoFactorAuthentication"
|
||||
/>
|
||||
<InputError :message="confirmationForm.errors.code" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Cancel/Disable -->
|
||||
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<Button
|
||||
v-if="confirming"
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="disabling"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<!-- Recovery Codes -->
|
||||
<div v-if="recoveryCodes.length > 0 && ! confirming" class="space-y-4">
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950">
|
||||
<div class="flex items-start gap-2">
|
||||
<Key class="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<p class="text-sm font-medium text-amber-900 dark:text-amber-100">
|
||||
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>
|
||||
|
||||
<div class="rounded-lg border bg-muted p-4">
|
||||
<div class="grid grid-cols-2 gap-2 font-mono text-sm">
|
||||
<div v-for="code in recoveryCodes" :key="code" class="flex items-center justify-between p-2 bg-background rounded border">
|
||||
<span>{{ code }}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 w-6 p-0"
|
||||
@click="copyToClipboard(code)"
|
||||
>
|
||||
<Copy class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- Enable -->
|
||||
<div v-if="! twoFactorEnabled">
|
||||
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
|
||||
<Button type="button" :disabled="enabling">
|
||||
<Shield class="h-4 w-4 mr-2" />
|
||||
Enable
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
</div>
|
||||
|
||||
<!-- Confirm -->
|
||||
<template v-else>
|
||||
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
|
||||
<Button
|
||||
v-if="confirming"
|
||||
type="button"
|
||||
:disabled="enabling"
|
||||
>
|
||||
<CheckCircle class="h-4 w-4 mr-2" />
|
||||
Confirm
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<!-- Regenerate Recovery Codes -->
|
||||
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
|
||||
<Button
|
||||
v-if="recoveryCodes.length > 0 && ! confirming"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
Regenerate Recovery Codes
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<!-- Show Recovery Codes -->
|
||||
<ConfirmsPassword @confirmed="showRecoveryCodes">
|
||||
<Button
|
||||
v-if="recoveryCodes.length === 0 && ! confirming"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Key class="h-4 w-4 mr-2" />
|
||||
Show Recovery Codes
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<!-- Cancel/Disable -->
|
||||
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<Button
|
||||
v-if="confirming"
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="disabling"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
|
||||
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<Button
|
||||
v-if="! confirming"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
:disabled="disabling"
|
||||
>
|
||||
Disable
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
</template>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
|
||||
<Button
|
||||
v-if="!confirming"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
:disabled="disabling"
|
||||
>
|
||||
Disable
|
||||
</Button>
|
||||
</ConfirmsPassword>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</AppCard>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user