From 0e7c363a041b8e5bd5e5415ab855313ebd81d0cf0ff84781c74e5fbeef19923d Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Sat, 14 Mar 2026 19:02:05 -0400 Subject: [PATCH] Fix code review follow-up issues - EnsureUserNotSuspended: bypass for impersonation stop, also check banned - FlashProps: add info and new_password keys - AccountLayout: impersonation stop link uses account domain (not admin) - withCount alias: billingInvoices as invoices_count for frontend compat - VPS Show: add secure password dialog with copy button for reset-password Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Controllers/Admin/CustomerController.php | 2 +- .../Middleware/EnsureUserNotSuspended.php | 13 +++++++- .../resources/ts/Layouts/AccountLayout.vue | 2 +- website/resources/ts/Pages/Services/Show.vue | 30 ++++++++++++++++++- website/resources/ts/types/index.ts | 2 ++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/website/app/Http/Controllers/Admin/CustomerController.php b/website/app/Http/Controllers/Admin/CustomerController.php index 17c2df9..19ebf6b 100644 --- a/website/app/Http/Controllers/Admin/CustomerController.php +++ b/website/app/Http/Controllers/Admin/CustomerController.php @@ -27,7 +27,7 @@ class CustomerController extends Controller public function index(Request $request): Response { $query = User::role('customer') - ->withCount(['services', 'billingInvoices']) + ->withCount(['services', 'billingInvoices as invoices_count']) ->with('subscriptions'); // Search by name or email diff --git a/website/app/Http/Middleware/EnsureUserNotSuspended.php b/website/app/Http/Middleware/EnsureUserNotSuspended.php index c5f16e9..86c8f94 100644 --- a/website/app/Http/Middleware/EnsureUserNotSuspended.php +++ b/website/app/Http/Middleware/EnsureUserNotSuspended.php @@ -12,10 +12,21 @@ class EnsureUserNotSuspended { public function handle(Request $request, Closure $next): Response { - if ($request->user()?->isSuspended()) { + // Allow impersonation stop even for suspended users + if ($request->is('impersonate/stop') && $request->session()->has('impersonator_id')) { + return $next($request); + } + + $user = $request->user(); + + if ($user?->isSuspended()) { abort(403, 'Your account has been suspended.'); } + if (method_exists($user, 'isBanned') && $user?->isBanned()) { + abort(403, 'Your account has been banned.'); + } + return $next($request); } } diff --git a/website/resources/ts/Layouts/AccountLayout.vue b/website/resources/ts/Layouts/AccountLayout.vue index 176ea1e..d91c9fa 100644 --- a/website/resources/ts/Layouts/AccountLayout.vue +++ b/website/resources/ts/Layouts/AccountLayout.vue @@ -130,7 +130,7 @@ const paletteItems = computed(() => { Impersonation Active import { ref, computed } from 'vue' -import { Link, useForm } from '@inertiajs/vue3' +import { Link, useForm, usePage } from '@inertiajs/vue3' import AccountLayout from '@/Layouts/AccountLayout.vue' import { resolveServiceStatusColor, @@ -18,6 +18,10 @@ defineOptions({ layout: AccountLayout }) const props = defineProps() +const page = usePage() +const newPassword = computed(() => (page.props as Record).flash as Record | undefined) +const showPasswordDialog = ref(!!newPassword.value?.new_password) + // Forms for VPS operations const powerForm = useForm({}) const rebuildDialog = ref(false) @@ -119,6 +123,7 @@ function resetPassword() { if (confirm('Are you sure you want to reset the root password? This will generate a new random password.')) { powerForm.post(`/services/${props.service.id}/vps/reset-password`, { preserveScroll: true, + onSuccess: () => { showPasswordDialog.value = true }, }) } } @@ -907,6 +912,29 @@ function submitRebuild() { + + + + + New Root Password + + + Copy this password now. It will not be shown again. + + + + + + Done + + + diff --git a/website/resources/ts/types/index.ts b/website/resources/ts/types/index.ts index 3f828a0..9ce0f84 100644 --- a/website/resources/ts/types/index.ts +++ b/website/resources/ts/types/index.ts @@ -35,6 +35,8 @@ export interface DomainProps { export interface FlashProps { success?: string error?: string + info?: string + new_password?: string } export interface SharedPageProps {