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:
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user