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) <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-03-14 19:02:05 -04:00
parent 9a410dc3c8
commit 0e7c363a04
5 changed files with 45 additions and 4 deletions

View File

@@ -27,7 +27,7 @@ class CustomerController extends Controller
public function index(Request $request): Response public function index(Request $request): Response
{ {
$query = User::role('customer') $query = User::role('customer')
->withCount(['services', 'billingInvoices']) ->withCount(['services', 'billingInvoices as invoices_count'])
->with('subscriptions'); ->with('subscriptions');
// Search by name or email // Search by name or email

View File

@@ -12,10 +12,21 @@ class EnsureUserNotSuspended
{ {
public function handle(Request $request, Closure $next): Response 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.'); abort(403, 'Your account has been suspended.');
} }
if (method_exists($user, 'isBanned') && $user?->isBanned()) {
abort(403, 'Your account has been banned.');
}
return $next($request); return $next($request);
} }
} }

View File

@@ -130,7 +130,7 @@ const paletteItems = computed(() => {
<span class="font-weight-bold">Impersonation Active</span> <span class="font-weight-bold">Impersonation Active</span>
</div> </div>
<Link <Link
:href="adminUrl + '/impersonate/stop'" href="/impersonate/stop"
method="post" method="post"
as="button" as="button"
class="text-decoration-none" class="text-decoration-none"

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue' 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 AccountLayout from '@/Layouts/AccountLayout.vue'
import { import {
resolveServiceStatusColor, resolveServiceStatusColor,
@@ -18,6 +18,10 @@ defineOptions({ layout: AccountLayout })
const props = defineProps<Props>() const props = defineProps<Props>()
const page = usePage()
const newPassword = computed(() => (page.props as Record<string, unknown>).flash as Record<string, string> | undefined)
const showPasswordDialog = ref(!!newPassword.value?.new_password)
// Forms for VPS operations // Forms for VPS operations
const powerForm = useForm({}) const powerForm = useForm({})
const rebuildDialog = ref(false) 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.')) { 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`, { powerForm.post(`/services/${props.service.id}/vps/reset-password`, {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { showPasswordDialog.value = true },
}) })
} }
} }
@@ -907,6 +912,29 @@ function submitRebuild() {
</VCardActions> </VCardActions>
</VCard> </VCard>
</VDialog> </VDialog>
<!-- New Password Dialog -->
<VDialog v-model="showPasswordDialog" max-width="440" persistent>
<VCard>
<VCardTitle class="text-h6 pa-4">New Root Password</VCardTitle>
<VCardText>
<VAlert type="warning" variant="tonal" class="mb-4">
Copy this password now. It will not be shown again.
</VAlert>
<VTextField
:model-value="newPassword?.new_password ?? ''"
readonly
variant="outlined"
append-inner-icon="tabler-copy"
@click:append-inner="navigator.clipboard.writeText(newPassword?.new_password ?? '')"
/>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn color="primary" @click="showPasswordDialog = false">Done</VBtn>
</VCardActions>
</VCard>
</VDialog>
</div> </div>
</template> </template>

View File

@@ -35,6 +35,8 @@ export interface DomainProps {
export interface FlashProps { export interface FlashProps {
success?: string success?: string
error?: string error?: string
info?: string
new_password?: string
} }
export interface SharedPageProps { export interface SharedPageProps {