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 {