production #1
|
|
@ -23,9 +23,11 @@ class PackageController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
|
$perPage = $request->input('per_page') ?? 25;
|
||||||
|
|
||||||
$packages = Package::query()
|
$packages = Package::query()
|
||||||
->latest('id')
|
->latest('id')
|
||||||
->paginate(25);
|
->paginate($perPage);
|
||||||
|
|
||||||
return Inertia::render('Admin/Packages/Index', [
|
return Inertia::render('Admin/Packages/Index', [
|
||||||
'packages' => $packages,
|
'packages' => $packages,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from "@/Components/ui/dialog";
|
} from "@/Components/ui/dialog";
|
||||||
import InputError from "@/Components/InputError.vue";
|
import InputError from "@/Components/InputError.vue";
|
||||||
import { Monitor, Smartphone, LogOut, CheckCircle } from "lucide-vue-next";
|
import { Monitor, Smartphone, LogOut, CheckCircle } from "lucide-vue-next";
|
||||||
|
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
sessions: Array,
|
sessions: Array,
|
||||||
|
|
@ -55,25 +56,22 @@ const closeModal = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card>
|
<AppCard
|
||||||
<CardHeader>
|
title=""
|
||||||
|
padding="none"
|
||||||
|
class="p-0! gap-0"
|
||||||
|
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
||||||
|
body-class=""
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<LogOut class="h-5 w-5 text-muted-foreground" />
|
<LogOut size="18" />
|
||||||
<CardTitle>Browser Sessions</CardTitle>
|
<CardTitle>Aktivne prijave</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Manage and log out your active sessions on other browsers and devices.
|
Upravljanje in izpis aktivnih prijav no drugih brskalnikih in napravah.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</template>
|
||||||
|
|
||||||
<CardContent class="space-y-6">
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
If necessary, you may log out of all of your other browser sessions across all of
|
|
||||||
your devices. Some of your recent sessions are listed below; however, this list
|
|
||||||
may not be exhaustive. If you feel your account has been compromised, you should
|
|
||||||
also update your password.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Other Browser Sessions -->
|
<!-- Other Browser Sessions -->
|
||||||
<div v-if="sessions && sessions.length > 0" class="space-y-4">
|
<div v-if="sessions && sessions.length > 0" class="space-y-4">
|
||||||
<div
|
<div
|
||||||
|
|
@ -100,9 +98,9 @@ const closeModal = () => {
|
||||||
v-if="session.is_current_device"
|
v-if="session.is_current_device"
|
||||||
class="inline-flex items-center ml-2 text-green-600 dark:text-green-400 font-semibold"
|
class="inline-flex items-center ml-2 text-green-600 dark:text-green-400 font-semibold"
|
||||||
>
|
>
|
||||||
This device
|
Ta naprava
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="ml-1"> · Last active {{ session.last_active }} </span>
|
<span v-else class="ml-1"> · Aktiven {{ session.last_active }} </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -111,12 +109,11 @@ const closeModal = () => {
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-else class="rounded-lg border border-dashed p-8 text-center">
|
<div v-else class="rounded-lg border border-dashed p-8 text-center">
|
||||||
<Monitor class="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
<Monitor class="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">Najdena nobena odprta prijava.</p>
|
||||||
No active sessions found. This feature requires session data to be configured in your Laravel application.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<template #footer>
|
||||||
|
<div class="flex flex-row gap-1 items-center justify-end w-full">
|
||||||
<Button @click="confirmLogout">
|
<Button @click="confirmLogout">
|
||||||
<LogOut class="h-4 w-4 mr-2" />
|
<LogOut class="h-4 w-4 mr-2" />
|
||||||
Log Out Other Browser Sessions
|
Log Out Other Browser Sessions
|
||||||
|
|
@ -130,7 +127,8 @@ const closeModal = () => {
|
||||||
<span>Done.</span>
|
<span>Done.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</template>
|
||||||
|
</AppCard>
|
||||||
|
|
||||||
<!-- Log Out Other Devices Confirmation Dialog -->
|
<!-- Log Out Other Devices Confirmation Dialog -->
|
||||||
<Dialog :open="confirmingLogout" @update:open="closeModal">
|
<Dialog :open="confirmingLogout" @update:open="closeModal">
|
||||||
|
|
@ -163,5 +161,4 @@ const closeModal = () => {
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,21 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from "vue";
|
||||||
import { router, useForm, usePage } from '@inertiajs/vue3';
|
import { router, useForm, usePage } from "@inertiajs/vue3";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
|
import {
|
||||||
import { Button } from '@/Components/ui/button';
|
Card,
|
||||||
import { Input } from '@/Components/ui/input';
|
CardContent,
|
||||||
import { Label } from '@/Components/ui/label';
|
CardDescription,
|
||||||
import { Badge } from '@/Components/ui/badge';
|
CardHeader,
|
||||||
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue';
|
CardTitle,
|
||||||
import InputError from '@/Components/InputError.vue';
|
} from "@/Components/ui/card";
|
||||||
import { Shield, Key, Copy, RefreshCw, CheckCircle, AlertCircle } from 'lucide-vue-next';
|
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({
|
const props = defineProps({
|
||||||
requiresConfirmation: Boolean,
|
requiresConfirmation: Boolean,
|
||||||
|
|
@ -23,11 +30,11 @@ const setupKey = ref(null);
|
||||||
const recoveryCodes = ref([]);
|
const recoveryCodes = ref([]);
|
||||||
|
|
||||||
const confirmationForm = useForm({
|
const confirmationForm = useForm({
|
||||||
code: '',
|
code: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const twoFactorEnabled = computed(
|
const twoFactorEnabled = computed(
|
||||||
() => ! enabling.value && page.props.auth.user?.two_factor_enabled,
|
() => !enabling.value && page.props.auth.user?.two_factor_enabled
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(twoFactorEnabled, () => {
|
watch(twoFactorEnabled, () => {
|
||||||
|
|
@ -40,40 +47,40 @@ watch(twoFactorEnabled, () => {
|
||||||
const enableTwoFactorAuthentication = () => {
|
const enableTwoFactorAuthentication = () => {
|
||||||
enabling.value = true;
|
enabling.value = true;
|
||||||
|
|
||||||
router.post(route('two-factor.enable'), {}, {
|
router.post(
|
||||||
|
route("two-factor.enable"),
|
||||||
|
{},
|
||||||
|
{
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => Promise.all([
|
onSuccess: () => Promise.all([showQrCode(), showSetupKey(), showRecoveryCodes()]),
|
||||||
showQrCode(),
|
|
||||||
showSetupKey(),
|
|
||||||
showRecoveryCodes(),
|
|
||||||
]),
|
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
enabling.value = false;
|
enabling.value = false;
|
||||||
confirming.value = props.requiresConfirmation;
|
confirming.value = props.requiresConfirmation;
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showQrCode = () => {
|
const showQrCode = () => {
|
||||||
return axios.get(route('two-factor.qr-code')).then(response => {
|
return axios.get(route("two-factor.qr-code")).then((response) => {
|
||||||
qrCode.value = response.data.svg;
|
qrCode.value = response.data.svg;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const showSetupKey = () => {
|
const showSetupKey = () => {
|
||||||
return axios.get(route('two-factor.secret-key')).then(response => {
|
return axios.get(route("two-factor.secret-key")).then((response) => {
|
||||||
setupKey.value = response.data.secretKey;
|
setupKey.value = response.data.secretKey;
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const showRecoveryCodes = () => {
|
const showRecoveryCodes = () => {
|
||||||
return axios.get(route('two-factor.recovery-codes')).then(response => {
|
return axios.get(route("two-factor.recovery-codes")).then((response) => {
|
||||||
recoveryCodes.value = response.data;
|
recoveryCodes.value = response.data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmTwoFactorAuthentication = () => {
|
const confirmTwoFactorAuthentication = () => {
|
||||||
confirmationForm.post(route('two-factor.confirm'), {
|
confirmationForm.post(route("two-factor.confirm"), {
|
||||||
errorBag: "confirmTwoFactorAuthentication",
|
errorBag: "confirmTwoFactorAuthentication",
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
preserveState: true,
|
preserveState: true,
|
||||||
|
|
@ -86,15 +93,13 @@ const confirmTwoFactorAuthentication = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const regenerateRecoveryCodes = () => {
|
const regenerateRecoveryCodes = () => {
|
||||||
axios
|
axios.post(route("two-factor.recovery-codes")).then(() => showRecoveryCodes());
|
||||||
.post(route('two-factor.recovery-codes'))
|
|
||||||
.then(() => showRecoveryCodes());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableTwoFactorAuthentication = () => {
|
const disableTwoFactorAuthentication = () => {
|
||||||
disabling.value = true;
|
disabling.value = true;
|
||||||
|
|
||||||
router.delete(route('two-factor.disable'), {
|
router.delete(route("two-factor.disable"), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
disabling.value = false;
|
disabling.value = false;
|
||||||
|
|
@ -107,42 +112,50 @@ const copyToClipboard = async (text) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy:', err);
|
console.error("Failed to copy:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card>
|
<AppCard
|
||||||
<CardHeader>
|
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">
|
<div class="flex items-center gap-2">
|
||||||
<Shield class="h-5 w-5 text-muted-foreground" />
|
<Shield size="18" />
|
||||||
<CardTitle>Two Factor Authentication</CardTitle>
|
<CardTitle>Dvonivojska overitev</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add additional security to your account using two factor authentication.
|
Dodatna varnost za vaš račun z dvonivojsko overitvijo.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</template>
|
||||||
|
|
||||||
<CardContent class="space-y-6">
|
|
||||||
<!-- Status Header -->
|
<!-- Status Header -->
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 v-if="twoFactorEnabled && ! confirming" class="text-lg font-semibold flex items-center gap-2">
|
<h3
|
||||||
|
v-if="twoFactorEnabled && !confirming"
|
||||||
|
class="text-lg font-semibold flex items-center gap-2"
|
||||||
|
>
|
||||||
<CheckCircle class="h-5 w-5 text-green-600" />
|
<CheckCircle class="h-5 w-5 text-green-600" />
|
||||||
Two factor authentication is enabled
|
Dvonivojska overitev omogočena
|
||||||
</h3>
|
</h3>
|
||||||
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-semibold flex items-center gap-2">
|
<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" />
|
<AlertCircle class="h-5 w-5 text-amber-600" />
|
||||||
Finish enabling two factor authentication
|
|
||||||
|
Dokončaj namestitev dvonivojske overitve
|
||||||
</h3>
|
</h3>
|
||||||
<h3 v-else class="text-lg font-semibold flex items-center gap-2">
|
<h3 v-else class="text-lg font-semibold flex items-center gap-2">
|
||||||
<Shield class="h-5 w-5 text-muted-foreground" />
|
Dvonivojska overitev onemogočena
|
||||||
Two factor authentication is disabled
|
|
||||||
</h3>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -151,10 +164,13 @@ const copyToClipboard = async (text) => {
|
||||||
<div v-if="qrCode" class="space-y-4">
|
<div v-if="qrCode" class="space-y-4">
|
||||||
<div class="rounded-lg border bg-muted/50 p-4">
|
<div class="rounded-lg border bg-muted/50 p-4">
|
||||||
<p v-if="confirming" class="text-sm font-medium mb-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.
|
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>
|
||||||
<p v-else class="text-sm text-muted-foreground mb-4">
|
<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.
|
Dvonivojska overitev je zdaj omogočena. Skenirajte QR kodo z aplikacijo za
|
||||||
|
preverjanje pristnosti na vašem telefonu ali vnesite namestitveni ključ.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- QR Code -->
|
<!-- QR Code -->
|
||||||
|
|
@ -164,7 +180,7 @@ const copyToClipboard = async (text) => {
|
||||||
<div v-if="setupKey" class="mt-4 p-3 bg-background rounded-lg border">
|
<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 items-center justify-between gap-2">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Label class="text-xs text-muted-foreground">Setup Key</Label>
|
<Label class="text-xs text-muted-foreground">Namestitveni Ključ</Label>
|
||||||
<p class="font-mono text-sm font-semibold mt-1" v-html="setupKey"></p>
|
<p class="font-mono text-sm font-semibold mt-1" v-html="setupKey"></p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -181,7 +197,7 @@ const copyToClipboard = async (text) => {
|
||||||
|
|
||||||
<!-- Confirmation Code Input -->
|
<!-- Confirmation Code Input -->
|
||||||
<div v-if="confirming" class="space-y-2">
|
<div v-if="confirming" class="space-y-2">
|
||||||
<Label for="code">Confirmation Code</Label>
|
<Label for="code">Potrdite kodo</Label>
|
||||||
<Input
|
<Input
|
||||||
id="code"
|
id="code"
|
||||||
v-model="confirmationForm.code"
|
v-model="confirmationForm.code"
|
||||||
|
|
@ -200,18 +216,27 @@ const copyToClipboard = async (text) => {
|
||||||
|
|
||||||
<!-- Recovery Codes -->
|
<!-- Recovery Codes -->
|
||||||
<div v-if="recoveryCodes.length > 0 && !confirming" class="space-y-4">
|
<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="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">
|
<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" />
|
<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">
|
<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.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border bg-muted p-4">
|
<div class="rounded-lg border bg-muted p-4">
|
||||||
<div class="grid grid-cols-2 gap-2 font-mono text-sm">
|
<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">
|
<div
|
||||||
|
v-for="code in recoveryCodes"
|
||||||
|
:key="code"
|
||||||
|
class="flex items-center justify-between p-2 bg-background rounded border"
|
||||||
|
>
|
||||||
<span>{{ code }}</span>
|
<span>{{ code }}</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -227,9 +252,9 @@ const copyToClipboard = async (text) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<template #footer>
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-row gap-2 items-center justify-end w-full">
|
||||||
<!-- Enable -->
|
<!-- Enable -->
|
||||||
<div v-if="!twoFactorEnabled">
|
<div v-if="!twoFactorEnabled">
|
||||||
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
|
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
|
||||||
|
|
@ -243,11 +268,7 @@ const copyToClipboard = async (text) => {
|
||||||
<!-- Confirm -->
|
<!-- Confirm -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
|
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
|
||||||
<Button
|
<Button v-if="confirming" type="button" :disabled="enabling">
|
||||||
v-if="confirming"
|
|
||||||
type="button"
|
|
||||||
:disabled="enabling"
|
|
||||||
>
|
|
||||||
<CheckCircle class="h-4 w-4 mr-2" />
|
<CheckCircle class="h-4 w-4 mr-2" />
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -301,6 +322,6 @@ const copyToClipboard = async (text) => {
|
||||||
</ConfirmsPassword>
|
</ConfirmsPassword>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</template>
|
||||||
</Card>
|
</AppCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,36 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import { useForm } from '@inertiajs/vue3';
|
import { useForm } from "@inertiajs/vue3";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/Components/ui/card';
|
import { Button } from "@/Components/ui/button";
|
||||||
import { Button } from '@/Components/ui/button';
|
import { Input } from "@/Components/ui/input";
|
||||||
import { Input } from '@/Components/ui/input';
|
import { Label } from "@/Components/ui/label";
|
||||||
import { Label } from '@/Components/ui/label';
|
import InputError from "@/Components/InputError.vue";
|
||||||
import InputError from '@/Components/InputError.vue';
|
import { CheckCircle, Lock } from "lucide-vue-next";
|
||||||
import { CheckCircle, Lock } from 'lucide-vue-next';
|
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||||
|
import { CardTitle } from "@/Components/ui/card";
|
||||||
|
|
||||||
const passwordInput = ref(null);
|
const passwordInput = ref(null);
|
||||||
const currentPasswordInput = ref(null);
|
const currentPasswordInput = ref(null);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
current_password: '',
|
current_password: "",
|
||||||
password: '',
|
password: "",
|
||||||
password_confirmation: '',
|
password_confirmation: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatePassword = () => {
|
const updatePassword = () => {
|
||||||
form.put(route('user-password.update'), {
|
form.put(route("user-password.update"), {
|
||||||
errorBag: 'updatePassword',
|
errorBag: "updatePassword",
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => form.reset(),
|
onSuccess: () => form.reset(),
|
||||||
onError: () => {
|
onError: () => {
|
||||||
if (form.errors.password) {
|
if (form.errors.password) {
|
||||||
form.reset('password', 'password_confirmation');
|
form.reset("password", "password_confirmation");
|
||||||
passwordInput.value.focus();
|
passwordInput.value.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.errors.current_password) {
|
if (form.errors.current_password) {
|
||||||
form.reset('current_password');
|
form.reset("current_password");
|
||||||
currentPasswordInput.value.focus();
|
currentPasswordInput.value.focus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -38,21 +39,26 @@ const updatePassword = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card>
|
<AppCard
|
||||||
<form @submit.prevent="updatePassword">
|
title=""
|
||||||
<CardHeader>
|
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">
|
<div class="flex items-center gap-2">
|
||||||
<Lock class="h-5 w-5 text-muted-foreground" />
|
<Lock size="18" />
|
||||||
<CardTitle>Update Password</CardTitle>
|
<CardTitle>Posodobi geslo</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<p class="text-sm">
|
||||||
Ensure your account is using a long, random password to stay secure.
|
Poskrbite, da vaš račun uporablja dolgo, naključno geslo za varnost.
|
||||||
</CardDescription>
|
</p>
|
||||||
</CardHeader>
|
</template>
|
||||||
|
|
||||||
<CardContent class="space-y-6">
|
<form @submit.prevent="updatePassword" class="space-y-6">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="current_password">Current Password</Label>
|
<Label for="current_password">Trenutno geslo</Label>
|
||||||
<Input
|
<Input
|
||||||
id="current_password"
|
id="current_password"
|
||||||
ref="currentPasswordInput"
|
ref="currentPasswordInput"
|
||||||
|
|
@ -64,7 +70,7 @@ const updatePassword = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="password">New Password</Label>
|
<Label for="password">Novo geslo</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
ref="passwordInput"
|
ref="passwordInput"
|
||||||
|
|
@ -76,7 +82,7 @@ const updatePassword = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="password_confirmation">Confirm Password</Label>
|
<Label for="password_confirmation">Potrdi geslo</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password_confirmation"
|
id="password_confirmation"
|
||||||
v-model="form.password_confirmation"
|
v-model="form.password_confirmation"
|
||||||
|
|
@ -85,17 +91,16 @@ const updatePassword = () => {
|
||||||
/>
|
/>
|
||||||
<InputError :message="form.errors.password_confirmation" class="mt-2" />
|
<InputError :message="form.errors.password_confirmation" class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</form>
|
||||||
|
|
||||||
<CardFooter class="flex items-center justify-between">
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
|
<CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
|
||||||
<span v-if="form.recentlySuccessful">Saved.</span>
|
<span v-if="form.recentlySuccessful">Shranjeno.</span>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" :disabled="form.processing"> Shrani </Button>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" :disabled="form.processing">
|
</template>
|
||||||
Save
|
</AppCard>
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import { Link, router, useForm } from '@inertiajs/vue3';
|
import { Link, router, useForm } from "@inertiajs/vue3";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/Components/ui/card';
|
import { Button } from "@/Components/ui/button";
|
||||||
import { Button } from '@/Components/ui/button';
|
import { Input } from "@/Components/ui/input";
|
||||||
import { Input } from '@/Components/ui/input';
|
import { Label } from "@/Components/ui/label";
|
||||||
import { Label } from '@/Components/ui/label';
|
import { Avatar, AvatarImage, AvatarFallback } from "@/Components/ui/avatar";
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/Components/ui/avatar';
|
import InputError from "@/Components/InputError.vue";
|
||||||
import InputError from '@/Components/InputError.vue';
|
import { User, Mail, Camera, Trash2, CheckCircle, AlertCircle } from "lucide-vue-next";
|
||||||
import { User, Mail, Camera, Trash2, CheckCircle, AlertCircle } from 'lucide-vue-next';
|
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||||
|
import { CardTitle } from "@/Components/ui/card";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
user: Object,
|
user: Object,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
_method: 'PUT',
|
_method: "PUT",
|
||||||
name: props.user.name,
|
name: props.user.name,
|
||||||
email: props.user.email,
|
email: props.user.email,
|
||||||
photo: null,
|
photo: null,
|
||||||
|
|
@ -29,8 +30,8 @@ const updateProfileInformation = () => {
|
||||||
form.photo = photoInput.value.files[0];
|
form.photo = photoInput.value.files[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
form.post(route('user-profile-information.update'), {
|
form.post(route("user-profile-information.update"), {
|
||||||
errorBag: 'updateProfileInformation',
|
errorBag: "updateProfileInformation",
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => clearPhotoFileInput(),
|
onSuccess: () => clearPhotoFileInput(),
|
||||||
});
|
});
|
||||||
|
|
@ -59,7 +60,7 @@ const updatePhotoPreview = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePhoto = () => {
|
const deletePhoto = () => {
|
||||||
router.delete(route('current-user-photo.destroy'), {
|
router.delete(route("current-user-photo.destroy"), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
photoPreview.value = null;
|
photoPreview.value = null;
|
||||||
|
|
@ -76,19 +77,22 @@ const clearPhotoFileInput = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card>
|
<AppCard
|
||||||
<form @submit.prevent="updateProfileInformation">
|
title=""
|
||||||
<CardHeader>
|
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">
|
<div class="flex items-center gap-2">
|
||||||
<User class="h-5 w-5 text-muted-foreground" />
|
<User size="18" />
|
||||||
<CardTitle>Profile Information</CardTitle>
|
<CardTitle>Informacije profila</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<p class="text-sm">Posodobite informacije vašega profila in e-poštni naslov.</p>
|
||||||
Update your account's profile information and email address.
|
</template>
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent class="space-y-6">
|
<form @submit.prevent="updateProfileInformation" class="space-y-6">
|
||||||
<!-- Profile Photo -->
|
<!-- Profile Photo -->
|
||||||
<div v-if="$page.props.jetstream.managesProfilePhotos" class="space-y-4">
|
<div v-if="$page.props.jetstream.managesProfilePhotos" class="space-y-4">
|
||||||
<input
|
<input
|
||||||
|
|
@ -98,23 +102,15 @@ const clearPhotoFileInput = () => {
|
||||||
class="hidden"
|
class="hidden"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@change="updatePhotoPreview"
|
@change="updatePhotoPreview"
|
||||||
>
|
/>
|
||||||
|
|
||||||
<Label for="photo">Photo</Label>
|
<Label for="photo">Fotografija</Label>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<!-- Current/Preview Photo -->
|
<!-- Current/Preview Photo -->
|
||||||
<Avatar class="h-20 w-20">
|
<Avatar class="h-20 w-20">
|
||||||
<AvatarImage
|
<AvatarImage v-if="photoPreview" :src="photoPreview" :alt="user.name" />
|
||||||
v-if="photoPreview"
|
<AvatarImage v-else :src="user.profile_photo_url" :alt="user.name" />
|
||||||
:src="photoPreview"
|
|
||||||
:alt="user.name"
|
|
||||||
/>
|
|
||||||
<AvatarImage
|
|
||||||
v-else
|
|
||||||
:src="user.profile_photo_url"
|
|
||||||
:alt="user.name"
|
|
||||||
/>
|
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
<User class="h-8 w-8" />
|
<User class="h-8 w-8" />
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
|
|
@ -128,7 +124,7 @@ const clearPhotoFileInput = () => {
|
||||||
@click.prevent="selectNewPhoto"
|
@click.prevent="selectNewPhoto"
|
||||||
>
|
>
|
||||||
<Camera class="h-4 w-4 mr-2" />
|
<Camera class="h-4 w-4 mr-2" />
|
||||||
Select Photo
|
Izberi fotografijo
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -139,7 +135,7 @@ const clearPhotoFileInput = () => {
|
||||||
@click.prevent="deletePhoto"
|
@click.prevent="deletePhoto"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-4 w-4 mr-2" />
|
<Trash2 class="h-4 w-4 mr-2" />
|
||||||
Remove
|
Odstrani
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -149,20 +145,14 @@ const clearPhotoFileInput = () => {
|
||||||
|
|
||||||
<!-- Name -->
|
<!-- Name -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="name">Name</Label>
|
<Label for="name">Ime</Label>
|
||||||
<Input
|
<Input id="name" v-model="form.name" type="text" required autocomplete="name" />
|
||||||
id="name"
|
|
||||||
v-model="form.name"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autocomplete="name"
|
|
||||||
/>
|
|
||||||
<InputError :message="form.errors.name" class="mt-2" />
|
<InputError :message="form.errors.name" class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="email">Email</Label>
|
<Label for="email">E-pošta</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
|
|
@ -173,12 +163,17 @@ const clearPhotoFileInput = () => {
|
||||||
<InputError :message="form.errors.email" class="mt-2" />
|
<InputError :message="form.errors.email" class="mt-2" />
|
||||||
|
|
||||||
<!-- Email Verification -->
|
<!-- Email Verification -->
|
||||||
<div v-if="$page.props.jetstream.hasEmailVerification && user.email_verified_at === null" class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-950">
|
<div
|
||||||
|
v-if="
|
||||||
|
$page.props.jetstream.hasEmailVerification && user.email_verified_at === null
|
||||||
|
"
|
||||||
|
class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-950"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<AlertCircle class="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5" />
|
<AlertCircle class="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5" />
|
||||||
<div class="flex-1 text-sm">
|
<div class="flex-1 text-sm">
|
||||||
<p class="text-amber-800 dark:text-amber-200">
|
<p class="text-amber-800 dark:text-amber-200">
|
||||||
Your email address is unverified.
|
Vaš e-poštni naslov ni potrjen.
|
||||||
<Link
|
<Link
|
||||||
:href="route('verification.send')"
|
:href="route('verification.send')"
|
||||||
method="post"
|
method="post"
|
||||||
|
|
@ -186,28 +181,33 @@ const clearPhotoFileInput = () => {
|
||||||
class="underline text-amber-900 hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300 font-medium"
|
class="underline text-amber-900 hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300 font-medium"
|
||||||
@click.prevent="sendEmailVerification"
|
@click.prevent="sendEmailVerification"
|
||||||
>
|
>
|
||||||
Click here to re-send the verification email.
|
Kliknite tukaj za ponovno pošiljanje potrditvenega e-sporočila.
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<div v-show="verificationLinkSent" class="mt-2 flex items-center gap-1.5 text-green-700 dark:text-green-400">
|
<div
|
||||||
|
v-show="verificationLinkSent"
|
||||||
|
class="mt-2 flex items-center gap-1.5 text-green-700 dark:text-green-400"
|
||||||
|
>
|
||||||
<CheckCircle class="h-4 w-4" />
|
<CheckCircle class="h-4 w-4" />
|
||||||
<span>A new verification link has been sent to your email address.</span>
|
<span
|
||||||
|
>Nova povezava za potrditev je bila poslana na vaš e-poštni
|
||||||
|
naslov.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</form>
|
||||||
|
|
||||||
<CardFooter class="flex items-center justify-between">
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
|
<CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
|
||||||
<span v-if="form.recentlySuccessful">Saved.</span>
|
<span v-if="form.recentlySuccessful">Shranjeno.</span>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" :disabled="form.processing"> Shrani </Button>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" :disabled="form.processing">
|
</template>
|
||||||
Save
|
</AppCard>
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user