production #1

Merged
sipo merged 45 commits from production into master 2026-01-27 18:02:44 +00:00
5 changed files with 603 additions and 578 deletions
Showing only changes of commit d64a67cf76 - Show all commits

View File

@ -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,

View File

@ -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,68 +56,64 @@ 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>
<!-- Other Browser Sessions -->
<div v-if="sessions && sessions.length > 0" class="space-y-4">
<div
v-for="(session, i) in sessions"
:key="i"
class="flex items-center gap-3 rounded-lg border p-3"
>
<div class="shrink-0">
<Monitor
v-if="session.agent.is_desktop"
class="h-8 w-8 text-muted-foreground"
/>
<Smartphone v-else class="h-8 w-8 text-muted-foreground" />
</div>
<CardContent class="space-y-6"> <div class="flex-1 min-w-0">
<p class="text-sm text-muted-foreground"> <div class="text-sm font-medium">
If necessary, you may log out of all of your other browser sessions across all of {{ session.agent.platform ? session.agent.platform : "Unknown" }} -
your devices. Some of your recent sessions are listed below; however, this list {{ session.agent.browser ? session.agent.browser : "Unknown" }}
may not be exhaustive. If you feel your account has been compromised, you should
also update your password.
</p>
<!-- Other Browser Sessions -->
<div v-if="sessions && sessions.length > 0" class="space-y-4">
<div
v-for="(session, i) in sessions"
:key="i"
class="flex items-center gap-3 rounded-lg border p-3"
>
<div class="shrink-0">
<Monitor
v-if="session.agent.is_desktop"
class="h-8 w-8 text-muted-foreground"
/>
<Smartphone v-else class="h-8 w-8 text-muted-foreground" />
</div> </div>
<div class="text-xs text-muted-foreground mt-1">
<div class="flex-1 min-w-0"> {{ session.ip_address }}
<div class="text-sm font-medium"> <span
{{ session.agent.platform ? session.agent.platform : "Unknown" }} - v-if="session.is_current_device"
{{ session.agent.browser ? session.agent.browser : "Unknown" }} class="inline-flex items-center ml-2 text-green-600 dark:text-green-400 font-semibold"
</div> >
<div class="text-xs text-muted-foreground mt-1"> Ta naprava
{{ session.ip_address }} </span>
<span <span v-else class="ml-1"> · Aktiven {{ session.last_active }} </span>
v-if="session.is_current_device"
class="inline-flex items-center ml-2 text-green-600 dark:text-green-400 font-semibold"
>
This device
</span>
<span v-else class="ml-1"> · Last active {{ session.last_active }} </span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- 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. </div>
</p>
</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,38 +127,38 @@ 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">
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Log Out Other Browser Sessions</DialogTitle> <DialogTitle>Log Out Other Browser Sessions</DialogTitle>
<DialogDescription> <DialogDescription>
Please enter your password to confirm you would like to log out of your other Please enter your password to confirm you would like to log out of your other
browser sessions across all of your devices. browser sessions across all of your devices.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div class="py-4"> <div class="py-4">
<Input <Input
ref="passwordInput" ref="passwordInput"
v-model="form.password" v-model="form.password"
type="password" type="password"
placeholder="Password" placeholder="Password"
autocomplete="current-password" autocomplete="current-password"
@keyup.enter="logoutOtherBrowserSessions" @keyup.enter="logoutOtherBrowserSessions"
/> />
<InputError :message="form.errors.password" class="mt-2" /> <InputError :message="form.errors.password" class="mt-2" />
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" @click="closeModal"> Cancel </Button> <Button variant="outline" @click="closeModal"> Cancel </Button>
<Button :disabled="form.processing" @click="logoutOtherBrowserSessions"> <Button :disabled="form.processing" @click="logoutOtherBrowserSessions">
Log Out Other Browser Sessions Log Out Other Browser Sessions
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</Card>
</template> </template>

View File

@ -1,17 +1,24 @@
<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,
}); });
const page = usePage(); const page = usePage();
@ -23,284 +30,298 @@ 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, () => {
if (! twoFactorEnabled.value) { if (!twoFactorEnabled.value) {
confirmationForm.reset(); confirmationForm.reset();
confirmationForm.clearErrors(); confirmationForm.clearErrors();
} }
}); });
const enableTwoFactorAuthentication = () => { const enableTwoFactorAuthentication = () => {
enabling.value = true; enabling.value = true;
router.post(route('two-factor.enable'), {}, { router.post(
preserveScroll: true, route("two-factor.enable"),
onSuccess: () => Promise.all([ {},
showQrCode(), {
showSetupKey(), preserveScroll: true,
showRecoveryCodes(), onSuccess: () => Promise.all([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,
onSuccess: () => { onSuccess: () => {
confirming.value = false; confirming.value = false;
qrCode.value = null; qrCode.value = null;
setupKey.value = null; setupKey.value = null;
}, },
}); });
}; };
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;
confirming.value = false; confirming.value = false;
}, },
}); });
}; };
const copyToClipboard = async (text) => { 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=""
<div class="flex items-center gap-2"> padding="none"
<Shield class="h-5 w-5 text-muted-foreground" /> class="p-0! gap-0"
<CardTitle>Two Factor Authentication</CardTitle> 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> </div>
<CardDescription> </div>
Add additional security to your account using two factor authentication. </div>
</CardDescription>
</CardHeader>
<CardContent class="space-y-6"> <!-- Confirmation Code Input -->
<!-- Status Header --> <div v-if="confirming" class="space-y-2">
<div class="flex items-start gap-3"> <Label for="code">Potrdite kodo</Label>
<div class="flex-1"> <Input
<h3 v-if="twoFactorEnabled && ! confirming" class="text-lg font-semibold flex items-center gap-2"> id="code"
<CheckCircle class="h-5 w-5 text-green-600" /> v-model="confirmationForm.code"
Two factor authentication is enabled type="text"
</h3> name="code"
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-semibold flex items-center gap-2"> inputmode="numeric"
<AlertCircle class="h-5 w-5 text-amber-600" /> autofocus
Finish enabling two factor authentication autocomplete="one-time-code"
</h3> placeholder="Enter 6-digit code"
<h3 v-else class="text-lg font-semibold flex items-center gap-2"> class="max-w-xs"
<Shield class="h-5 w-5 text-muted-foreground" /> @keyup.enter="confirmTwoFactorAuthentication"
Two factor authentication is disabled />
</h3> <InputError :message="confirmationForm.errors.code" class="mt-2" />
<p class="mt-2 text-sm text-muted-foreground"> </div>
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. </div>
</p>
</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>
</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 --> <!-- Confirm -->
<div v-if="twoFactorEnabled" class="space-y-6"> <template v-else>
<div v-if="qrCode" class="space-y-4"> <ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
<div class="rounded-lg border bg-muted/50 p-4"> <Button v-if="confirming" type="button" :disabled="enabling">
<p v-if="confirming" class="text-sm font-medium mb-4"> <CheckCircle class="h-4 w-4 mr-2" />
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. Confirm
</p> </Button>
<p v-else class="text-sm text-muted-foreground mb-4"> </ConfirmsPassword>
Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application or enter the setup key.
</p>
<!-- QR Code --> <!-- Regenerate Recovery Codes -->
<div class="flex justify-center p-4 bg-white rounded-lg" v-html="qrCode" /> <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 --> <!-- Show Recovery Codes -->
<div v-if="setupKey" class="mt-4 p-3 bg-background rounded-lg border"> <ConfirmsPassword @confirmed="showRecoveryCodes">
<div class="flex items-center justify-between gap-2"> <Button
<div class="flex-1"> v-if="recoveryCodes.length === 0 && !confirming"
<Label class="text-xs text-muted-foreground">Setup Key</Label> type="button"
<p class="font-mono text-sm font-semibold mt-1" v-html="setupKey"></p> variant="outline"
</div> >
<Button <Key class="h-4 w-4 mr-2" />
type="button" Show Recovery Codes
variant="ghost" </Button>
size="sm" </ConfirmsPassword>
@click="copyToClipboard(setupKey)"
>
<Copy class="h-4 w-4" />
</Button>
</div>
</div>
</div>
<!-- Confirmation Code Input --> <!-- Cancel/Disable -->
<div v-if="confirming" class="space-y-2"> <ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<Label for="code">Confirmation Code</Label> <Button
<Input v-if="confirming"
id="code" type="button"
v-model="confirmationForm.code" variant="outline"
type="text" :disabled="disabling"
name="code" >
inputmode="numeric" Cancel
autofocus </Button>
autocomplete="one-time-code" </ConfirmsPassword>
placeholder="Enter 6-digit code"
class="max-w-xs"
@keyup.enter="confirmTwoFactorAuthentication"
/>
<InputError :message="confirmationForm.errors.code" class="mt-2" />
</div>
</div>
<!-- Recovery Codes --> <ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<div v-if="recoveryCodes.length > 0 && ! confirming" class="space-y-4"> <Button
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950"> v-if="!confirming"
<div class="flex items-start gap-2"> type="button"
<Key class="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" /> variant="destructive"
<p class="text-sm font-medium text-amber-900 dark:text-amber-100"> :disabled="disabling"
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> Disable
</div> </Button>
</div> </ConfirmsPassword>
</template>
<div class="rounded-lg border bg-muted p-4"> </div>
<div class="grid grid-cols-2 gap-2 font-mono text-sm"> </template>
<div v-for="code in recoveryCodes" :key="code" class="flex items-center justify-between p-2 bg-background rounded border"> </AppCard>
<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>
</template> </template>

View File

@ -1,101 +1,106 @@
<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();
} }
}, },
}); });
}; };
</script> </script>
<template> <template>
<Card> <AppCard
<form @submit.prevent="updatePassword"> title=""
<CardHeader> padding="none"
<div class="flex items-center gap-2"> class="p-0! gap-0"
<Lock class="h-5 w-5 text-muted-foreground" /> header-class="py-3! px-4 gap-0 text-muted-foreground"
<CardTitle>Update Password</CardTitle> body-class="p-4 border-t"
</div> >
<CardDescription> <template #header>
Ensure your account is using a long, random password to stay secure. <div class="flex items-center gap-2">
</CardDescription> <Lock size="18" />
</CardHeader> <CardTitle>Posodobi geslo</CardTitle>
</div>
<p class="text-sm">
Poskrbite, da vaš račun uporablja dolgo, naključno geslo za varnost.
</p>
</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"
v-model="form.current_password" v-model="form.current_password"
type="password" type="password"
autocomplete="current-password" autocomplete="current-password"
/> />
<InputError :message="form.errors.current_password" class="mt-2" /> <InputError :message="form.errors.current_password" class="mt-2" />
</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"
v-model="form.password" v-model="form.password"
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
/> />
<InputError :message="form.errors.password" class="mt-2" /> <InputError :message="form.errors.password" class="mt-2" />
</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"
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
/> />
<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 gap-2 text-sm text-muted-foreground"> <div class="flex items-center justify-between w-full">
<CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" /> <div class="flex items-center gap-2 text-sm text-muted-foreground">
<span v-if="form.recentlySuccessful">Saved.</span> <CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
</div> <span v-if="form.recentlySuccessful">Shranjeno.</span>
<Button type="submit" :disabled="form.processing"> </div>
Save <Button type="submit" :disabled="form.processing"> Shrani </Button>
</Button> </div>
</CardFooter> </template>
</form> </AppCard>
</Card>
</template> </template>

View File

@ -1,23 +1,24 @@
<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,
}); });
const verificationLinkSent = ref(null); const verificationLinkSent = ref(null);
@ -25,189 +26,188 @@ const photoPreview = ref(null);
const photoInput = ref(null); const photoInput = ref(null);
const updateProfileInformation = () => { const updateProfileInformation = () => {
if (photoInput.value) { if (photoInput.value) {
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(),
}); });
}; };
const sendEmailVerification = () => { const sendEmailVerification = () => {
verificationLinkSent.value = true; verificationLinkSent.value = true;
}; };
const selectNewPhoto = () => { const selectNewPhoto = () => {
photoInput.value.click(); photoInput.value.click();
}; };
const updatePhotoPreview = () => { const updatePhotoPreview = () => {
const photo = photoInput.value.files[0]; const photo = photoInput.value.files[0];
if (! photo) return; if (!photo) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
photoPreview.value = e.target.result; photoPreview.value = e.target.result;
}; };
reader.readAsDataURL(photo); reader.readAsDataURL(photo);
}; };
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;
clearPhotoFileInput(); clearPhotoFileInput();
}, },
}); });
}; };
const clearPhotoFileInput = () => { const clearPhotoFileInput = () => {
if (photoInput.value?.value) { if (photoInput.value?.value) {
photoInput.value.value = null; photoInput.value.value = null;
} }
}; };
</script> </script>
<template> <template>
<Card> <AppCard
<form @submit.prevent="updateProfileInformation"> title=""
<CardHeader> padding="none"
<div class="flex items-center gap-2"> class="p-0! gap-0"
<User class="h-5 w-5 text-muted-foreground" /> header-class="py-3! px-4 gap-0 text-muted-foreground"
<CardTitle>Profile Information</CardTitle> body-class="p-4 border-t"
</div> >
<CardDescription> <template #header>
Update your account's profile information and email address. <div class="flex items-center gap-2">
</CardDescription> <User size="18" />
</CardHeader> <CardTitle>Informacije profila</CardTitle>
</div>
<p class="text-sm">Posodobite informacije vašega profila in e-poštni naslov.</p>
</template>
<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
id="photo" id="photo"
ref="photoInput" ref="photoInput"
type="file" type="file"
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" <AvatarFallback>
:alt="user.name" <User class="h-8 w-8" />
/> </AvatarFallback>
<AvatarImage </Avatar>
v-else
:src="user.profile_photo_url"
:alt="user.name"
/>
<AvatarFallback>
<User class="h-8 w-8" />
</AvatarFallback>
</Avatar>
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
@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
v-if="user.profile_photo_path" v-if="user.profile_photo_path"
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
@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>
<InputError :message="form.errors.photo" class="mt-2" /> <InputError :message="form.errors.photo" class="mt-2" />
</div> </div>
<!-- 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" <InputError :message="form.errors.name" class="mt-2" />
v-model="form.name" </div>
type="text"
required
autocomplete="name"
/>
<InputError :message="form.errors.name" class="mt-2" />
</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"
type="email" type="email"
required required
autocomplete="username" autocomplete="username"
/> />
<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
<div class="flex items-start gap-2"> v-if="
<AlertCircle class="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5" /> $page.props.jetstream.hasEmailVerification && user.email_verified_at === null
<div class="flex-1 text-sm"> "
<p class="text-amber-800 dark:text-amber-200"> class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-950"
Your email address is unverified. >
<Link <div class="flex items-start gap-2">
:href="route('verification.send')" <AlertCircle class="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5" />
method="post" <div class="flex-1 text-sm">
as="button" <p class="text-amber-800 dark:text-amber-200">
class="underline text-amber-900 hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300 font-medium" Vaš e-poštni naslov ni potrjen.
@click.prevent="sendEmailVerification" <Link
> :href="route('verification.send')"
Click here to re-send the verification email. method="post"
</Link> as="button"
</p> class="underline text-amber-900 hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300 font-medium"
<div v-show="verificationLinkSent" class="mt-2 flex items-center gap-1.5 text-green-700 dark:text-green-400"> @click.prevent="sendEmailVerification"
<CheckCircle class="h-4 w-4" /> >
<span>A new verification link has been sent to your email address.</span> Kliknite tukaj za ponovno pošiljanje potrditvenega e-sporočila.
</div> </Link>
</div> </p>
</div> <div
</div> v-show="verificationLinkSent"
</div> class="mt-2 flex items-center gap-1.5 text-green-700 dark:text-green-400"
</CardContent> >
<CheckCircle class="h-4 w-4" />
<span
>Nova povezava za potrditev je bila poslana na vaš e-poštni
naslov.</span
>
</div>
</div>
</div>
</div>
</div>
</form>
<CardFooter class="flex items-center justify-between"> <template #footer>
<div class="flex items-center gap-2 text-sm text-muted-foreground"> <div class="flex items-center justify-between w-full">
<CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" /> <div class="flex items-center gap-2 text-sm text-muted-foreground">
<span v-if="form.recentlySuccessful">Saved.</span> <CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
</div> <span v-if="form.recentlySuccessful">Shranjeno.</span>
<Button type="submit" :disabled="form.processing"> </div>
Save <Button type="submit" :disabled="form.processing"> Shrani </Button>
</Button> </div>
</CardFooter> </template>
</form> </AppCard>
</Card>
</template> </template>