garázs fejlesztés
This commit is contained in:
@@ -265,6 +265,32 @@
|
||||
</svg>
|
||||
<p class="text-sm text-slate-400">{{ $t('users.details.no_person_record') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Danger Zone (Superadmin only) -->
|
||||
<!-- ============================================================ -->
|
||||
<div v-if="isSuperadmin" class="border border-red-500/30 bg-red-500/5 rounded-xl p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 rounded-lg bg-red-500/10 text-red-400">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-red-400">{{ $t('users.details.danger_zone') }}</h3>
|
||||
<p class="text-xs text-slate-400 mt-0.5">{{ $t('users.details.danger_zone_warning') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="openDangerModal"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-red-600/20 text-red-300 hover:bg-red-600/30 border border-red-500/30 rounded-lg transition"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{{ $t('users.details.danger_zone_button') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
@@ -570,6 +596,73 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Danger Zone Modal (Double Opt-in) -->
|
||||
<!-- ============================================================ -->
|
||||
<div
|
||||
v-if="showDangerModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="closeDangerModal"></div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="relative w-full max-w-md bg-slate-800 border border-red-500/30 rounded-2xl shadow-2xl">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-red-500/20 flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-red-500/10 text-red-400">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-red-400">{{ $t('users.details.danger_zone') }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="p-6 space-y-4">
|
||||
<p class="text-sm text-slate-300">
|
||||
{{ $t('users.details.danger_zone_warning') }}
|
||||
</p>
|
||||
|
||||
<div class="bg-slate-700/50 rounded-lg p-3">
|
||||
<p class="text-xs text-slate-400 mb-1">{{ $t('users.details.danger_zone_confirm_label') }}</p>
|
||||
<p class="text-sm text-white font-mono">{{ user?.email }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
v-model="dangerConfirmEmail"
|
||||
type="email"
|
||||
:placeholder="$t('users.details.danger_zone_confirm_placeholder')"
|
||||
class="w-full px-3 py-2 text-sm bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-slate-700 flex items-center justify-end gap-3">
|
||||
<button
|
||||
@click="closeDangerModal"
|
||||
:disabled="dangerDeleting"
|
||||
class="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white bg-slate-700/50 hover:bg-slate-700 rounded-lg transition disabled:opacity-50"
|
||||
>
|
||||
{{ $t('users.details.danger_zone_cancel') }}
|
||||
</button>
|
||||
<button
|
||||
@click="executeHardDelete"
|
||||
:disabled="dangerConfirmEmail !== user?.email || dangerDeleting"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-red-600 text-white hover:bg-red-500 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg v-if="dangerDeleting" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
{{ $t('users.details.danger_zone_confirm_button') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Notification Toast -->
|
||||
<!-- ============================================================ -->
|
||||
@@ -634,6 +727,11 @@ const editForm = ref({
|
||||
person_birth_date: '',
|
||||
})
|
||||
|
||||
// Danger Zone Modal
|
||||
const showDangerModal = ref(false)
|
||||
const dangerConfirmEmail = ref('')
|
||||
const dangerDeleting = ref(false)
|
||||
|
||||
// Notification
|
||||
const notification = ref({ show: false, type: 'success' as 'success' | 'error', message: '' })
|
||||
|
||||
@@ -691,6 +789,69 @@ const hasEditPermission = computed(() => {
|
||||
return !!auth.user?.system_capabilities?.['users:edit']
|
||||
})
|
||||
|
||||
const isSuperadmin = computed(() => {
|
||||
return auth.user?.role === 'SUPERADMIN'
|
||||
})
|
||||
|
||||
// Danger Zone functions
|
||||
function openDangerModal() {
|
||||
dangerConfirmEmail.value = ''
|
||||
showDangerModal.value = true
|
||||
}
|
||||
|
||||
function closeDangerModal() {
|
||||
showDangerModal.value = false
|
||||
dangerConfirmEmail.value = ''
|
||||
}
|
||||
|
||||
async function executeHardDelete() {
|
||||
if (dangerConfirmEmail.value !== user.value?.email) return
|
||||
dangerDeleting.value = true
|
||||
try {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
const token = (tokenCookie.value || auth.token || '').trim()
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/v1/admin/users/${userId}/hard`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}))
|
||||
const detail = errData.detail || ''
|
||||
if (res.status === 403) {
|
||||
throw new Error('danger_zone_error_not_superadmin')
|
||||
}
|
||||
if (res.status === 400 && detail.includes('financial')) {
|
||||
throw new Error('danger_zone_error_financial')
|
||||
}
|
||||
throw new Error(detail || `Delete failed: ${res.status}`)
|
||||
}
|
||||
|
||||
closeDangerModal()
|
||||
showNotification('success', 'danger_zone_success')
|
||||
|
||||
// Redirect to users list after a short delay
|
||||
setTimeout(() => {
|
||||
router.push('/users')
|
||||
}, 2000)
|
||||
} catch (e: any) {
|
||||
const msg = e.message || 'danger_zone_error_generic'
|
||||
// Check if it's a translation key
|
||||
if (msg.startsWith('danger_zone_error')) {
|
||||
showNotification('error', msg)
|
||||
} else {
|
||||
showNotification('error', 'danger_zone_error_generic')
|
||||
}
|
||||
} finally {
|
||||
dangerDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// API calls
|
||||
async function fetchUser() {
|
||||
loading.value = true
|
||||
@@ -858,7 +1019,9 @@ async function saveEdit() {
|
||||
|
||||
// Notification helper
|
||||
function showNotification(type: 'success' | 'error', message: string) {
|
||||
notification.value = { show: true, type, message }
|
||||
// If message is a translation key, translate it
|
||||
const translated = $t(message.startsWith('danger_zone') ? `users.details.${message}` : message)
|
||||
notification.value = { show: true, type, message: translated }
|
||||
setTimeout(() => {
|
||||
notification.value.show = false
|
||||
}, 4000)
|
||||
@@ -973,4 +1136,4 @@ function memberStatusDotClass(status: string): string {
|
||||
onMounted(() => {
|
||||
fetchUser()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
Reference in New Issue
Block a user