This commit introduces role-based access control for user contact information in the CMS. It updates the user list display to show email and phone details only to users with the 'vorstand' role, while masking this information for others. Additionally, it modifies the API endpoints to ensure that contact data is only returned for authorized users, improving data privacy and security.
485 lines
16 KiB
Vue
485 lines
16 KiB
Vue
<template>
|
|
<div class="min-h-full py-16 bg-gray-50">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-4xl font-display font-bold text-gray-900">
|
|
Benutzerverwaltung
|
|
</h1>
|
|
<div class="w-24 h-1 bg-primary-600 mt-4" />
|
|
</div>
|
|
<NuxtLink
|
|
to="/cms"
|
|
class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-lg transition-colors"
|
|
>
|
|
← Zurück zum CMS
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<!-- Pending Users -->
|
|
<div
|
|
v-if="pendingUsers.length > 0"
|
|
class="mb-8"
|
|
>
|
|
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
|
|
<AlertCircle
|
|
:size="24"
|
|
class="inline text-yellow-600 mr-2"
|
|
/>
|
|
Wartende Registrierungen ({{ pendingUsers.length }})
|
|
</h2>
|
|
<div class="space-y-4">
|
|
<div
|
|
v-for="user in pendingUsers"
|
|
:key="user.id"
|
|
class="bg-yellow-50 border-l-4 border-yellow-400 rounded-lg p-6 shadow"
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<h3 class="text-lg font-semibold text-gray-900">
|
|
{{ user.name }}
|
|
</h3>
|
|
<p class="text-sm text-gray-600 mt-1">
|
|
{{ user.email }}
|
|
</p>
|
|
<p
|
|
v-if="user.phone"
|
|
class="text-sm text-gray-600"
|
|
>
|
|
{{ user.phone }}
|
|
</p>
|
|
<p class="text-xs text-gray-500 mt-2">
|
|
Registriert am: {{ formatDate(user.created) }}
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-col space-y-2 ml-4">
|
|
<!-- Role Selection -->
|
|
<select
|
|
v-model="user.selectedRole"
|
|
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-600"
|
|
>
|
|
<option value="mitglied">
|
|
Mitglied
|
|
</option>
|
|
<option value="vorstand">
|
|
Vorstand
|
|
</option>
|
|
<option value="admin">
|
|
Administrator
|
|
</option>
|
|
<option value="newsletter">
|
|
Newsletter
|
|
</option>
|
|
</select>
|
|
|
|
<!-- Approve Button -->
|
|
<button
|
|
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-semibold rounded-lg transition-colors flex items-center justify-center"
|
|
@click="approveUser(user)"
|
|
>
|
|
<Check
|
|
:size="16"
|
|
class="mr-1"
|
|
/>
|
|
Freischalten
|
|
</button>
|
|
|
|
<!-- Reject Button -->
|
|
<button
|
|
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-semibold rounded-lg transition-colors flex items-center justify-center"
|
|
@click="rejectUser(user)"
|
|
>
|
|
<X
|
|
:size="16"
|
|
class="mr-1"
|
|
/>
|
|
Ablehnen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Users -->
|
|
<div>
|
|
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
|
|
Aktive Benutzer ({{ activeUsers.length }})
|
|
</h2>
|
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Name
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
E-Mail
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Telefon
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Rolle
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Letzter Login
|
|
</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Aktionen
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr
|
|
v-for="user in activeUsers"
|
|
:key="user.id"
|
|
class="hover:bg-gray-50"
|
|
>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-gray-900">
|
|
{{ user.name }}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-600">
|
|
<template v-if="canViewContactData">
|
|
{{ user.email || '-' }}
|
|
</template>
|
|
<span
|
|
v-else
|
|
class="text-gray-400"
|
|
>
|
|
Nur für Vorstand
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-600">
|
|
<template v-if="canViewContactData">
|
|
{{ user.phone || '-' }}
|
|
</template>
|
|
<span
|
|
v-else
|
|
class="text-gray-400"
|
|
>
|
|
Nur für Vorstand
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex flex-wrap gap-1">
|
|
<span
|
|
v-for="role in (user.roles || (user.role ? [user.role] : ['mitglied']))"
|
|
:key="role"
|
|
class="px-2 py-1 text-xs font-medium rounded"
|
|
:class="{
|
|
'bg-red-100 text-red-800': role === 'admin',
|
|
'bg-blue-100 text-blue-800': role === 'vorstand',
|
|
'bg-green-100 text-green-800': role === 'newsletter',
|
|
'bg-gray-100 text-gray-800': role === 'mitglied'
|
|
}"
|
|
>
|
|
{{ role === 'admin' ? 'Admin' : role === 'vorstand' ? 'Vorstand' : role === 'newsletter' ? 'Newsletter' : 'Mitglied' }}
|
|
</span>
|
|
</div>
|
|
<button
|
|
class="mt-1 text-xs text-primary-600 hover:text-primary-800"
|
|
@click="openRoleModal(user)"
|
|
>
|
|
Bearbeiten
|
|
</button>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-600">
|
|
{{ user.lastLogin ? formatDate(user.lastLogin) : 'Nie' }}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
|
|
<button
|
|
v-if="user.id !== currentUserId"
|
|
class="text-red-600 hover:text-red-800 font-medium"
|
|
@click="deactivateUser(user)"
|
|
>
|
|
Deaktivieren
|
|
</button>
|
|
<span
|
|
v-else
|
|
class="text-gray-400"
|
|
>Eigenes Konto</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success/Error Messages -->
|
|
<div
|
|
v-if="successMessage"
|
|
class="fixed bottom-20 right-4 bg-green-50 border border-green-200 rounded-lg p-4 shadow-lg"
|
|
>
|
|
<p class="text-sm text-green-800 flex items-center">
|
|
<Check
|
|
:size="18"
|
|
class="mr-2"
|
|
/>
|
|
{{ successMessage }}
|
|
</p>
|
|
</div>
|
|
<div
|
|
v-if="errorMessage"
|
|
class="fixed bottom-20 right-4 bg-red-50 border border-red-200 rounded-lg p-4 shadow-lg"
|
|
>
|
|
<p class="text-sm text-red-800 flex items-center">
|
|
<AlertCircle
|
|
:size="18"
|
|
class="mr-2"
|
|
/>
|
|
{{ errorMessage }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Role Edit Modal -->
|
|
<div
|
|
v-if="showRoleModal && editingUser"
|
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
|
@click.self="closeRoleModal"
|
|
>
|
|
<div class="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
|
|
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
|
|
Rollen bearbeiten: {{ editingUser.name }}
|
|
</h2>
|
|
|
|
<div class="space-y-3 mb-6">
|
|
<label class="flex items-center">
|
|
<input
|
|
v-model="selectedRoles"
|
|
type="checkbox"
|
|
value="mitglied"
|
|
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
|
>
|
|
<span class="ml-2 text-sm text-gray-700">Mitglied</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input
|
|
v-model="selectedRoles"
|
|
type="checkbox"
|
|
value="vorstand"
|
|
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
|
>
|
|
<span class="ml-2 text-sm text-gray-700">Vorstand</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input
|
|
v-model="selectedRoles"
|
|
type="checkbox"
|
|
value="newsletter"
|
|
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
|
>
|
|
<span class="ml-2 text-sm text-gray-700">Newsletter</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input
|
|
v-model="selectedRoles"
|
|
type="checkbox"
|
|
value="admin"
|
|
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
|
>
|
|
<span class="ml-2 text-sm text-gray-700">Administrator</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div
|
|
v-if="selectedRoles.length === 0"
|
|
class="mb-4 text-sm text-red-600"
|
|
>
|
|
Mindestens eine Rolle muss ausgewählt werden.
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3">
|
|
<button
|
|
type="button"
|
|
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
|
@click="closeRoleModal"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
:disabled="selectedRoles.length === 0"
|
|
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="saveUserRoles"
|
|
>
|
|
Speichern
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { AlertCircle, Check, X } from 'lucide-vue-next'
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
const canViewContactData = computed(() => {
|
|
// Kontaktdaten nur für Vorstand sichtbar
|
|
return authStore.hasRole('vorstand')
|
|
})
|
|
|
|
const allUsers = ref([])
|
|
const currentUserId = ref(null)
|
|
const successMessage = ref('')
|
|
const errorMessage = ref('')
|
|
const showRoleModal = ref(false)
|
|
const editingUser = ref(null)
|
|
const selectedRoles = ref([])
|
|
|
|
const pendingUsers = computed(() => {
|
|
return allUsers.value
|
|
.filter(u => u.active === false)
|
|
.map(u => ({
|
|
...u,
|
|
selectedRole: (u.roles && u.roles.length > 0) ? u.roles[0] : (u.role || 'mitglied')
|
|
}))
|
|
})
|
|
|
|
const activeUsers = computed(() => {
|
|
return allUsers.value.filter(u => u.active === true)
|
|
})
|
|
|
|
const formatDate = (dateString) => {
|
|
return new Date(dateString).toLocaleString('de-DE', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
}
|
|
|
|
const loadUsers = async () => {
|
|
try {
|
|
const response = await $fetch('/api/cms/users/list')
|
|
allUsers.value = response.users
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Benutzer:', error)
|
|
errorMessage.value = 'Fehler beim Laden der Benutzerliste'
|
|
}
|
|
}
|
|
|
|
const approveUser = async (user) => {
|
|
try {
|
|
await $fetch('/api/cms/users/approve', {
|
|
method: 'POST',
|
|
body: {
|
|
userId: user.id,
|
|
roles: [user.selectedRole || 'mitglied']
|
|
}
|
|
})
|
|
|
|
successMessage.value = `Benutzer ${user.name} wurde als ${user.selectedRole} freigeschaltet`
|
|
setTimeout(() => successMessage.value = '', 3000)
|
|
|
|
await loadUsers()
|
|
} catch (_error) {
|
|
errorMessage.value = 'Fehler beim Freischalten des Benutzers'
|
|
setTimeout(() => errorMessage.value = '', 3000)
|
|
}
|
|
}
|
|
|
|
function openRoleModal(user) {
|
|
editingUser.value = user
|
|
selectedRoles.value = user.roles || (user.role ? [user.role] : ['mitglied'])
|
|
showRoleModal.value = true
|
|
}
|
|
|
|
function closeRoleModal() {
|
|
showRoleModal.value = false
|
|
editingUser.value = null
|
|
selectedRoles.value = []
|
|
}
|
|
|
|
async function saveUserRoles() {
|
|
if (!editingUser.value || selectedRoles.value.length === 0) return
|
|
|
|
try {
|
|
await $fetch('/api/cms/users/update-role', {
|
|
method: 'POST',
|
|
body: {
|
|
userId: editingUser.value.id,
|
|
roles: selectedRoles.value
|
|
}
|
|
})
|
|
|
|
successMessage.value = `Rollen von ${editingUser.value.name} wurden aktualisiert`
|
|
setTimeout(() => successMessage.value = '', 3000)
|
|
|
|
closeRoleModal()
|
|
await loadUsers()
|
|
} catch (_error) {
|
|
errorMessage.value = 'Fehler beim Aktualisieren der Rollen'
|
|
setTimeout(() => errorMessage.value = '', 3000)
|
|
}
|
|
}
|
|
|
|
const rejectUser = async (user) => {
|
|
window.showConfirmModal('Registrierung ablehnen', `Möchten Sie die Registrierung von ${user.name} wirklich ablehnen?`, async () => {
|
|
try {
|
|
await $fetch('/api/cms/users/reject', {
|
|
method: 'POST',
|
|
body: { userId: user.id }
|
|
})
|
|
|
|
await loadUsers()
|
|
window.showSuccessModal('Erfolg', `Registrierung von ${user.name} wurde abgelehnt`)
|
|
} catch (error) {
|
|
console.error('Fehler beim Ablehnen:', error)
|
|
window.showErrorModal('Fehler', 'Fehler beim Ablehnen der Registrierung')
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
const deactivateUser = async (user) => {
|
|
window.showConfirmModal('Benutzer deaktivieren', `Möchten Sie ${user.name} wirklich deaktivieren?`, async () => {
|
|
try {
|
|
await $fetch('/api/cms/users/deactivate', {
|
|
method: 'POST',
|
|
body: { userId: user.id }
|
|
})
|
|
|
|
await loadUsers()
|
|
window.showSuccessModal('Erfolg', `Benutzer ${user.name} wurde deaktiviert`)
|
|
} catch (error) {
|
|
console.error('Fehler beim Deaktivieren:', error)
|
|
window.showErrorModal('Fehler', 'Fehler beim Deaktivieren des Benutzers')
|
|
}
|
|
})
|
|
}
|
|
|
|
onMounted(async () => {
|
|
// Get current user ID
|
|
try {
|
|
const response = await $fetch('/api/auth/status')
|
|
currentUserId.value = response.user?.id
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden des aktuellen Benutzers:', error)
|
|
}
|
|
|
|
await loadUsers()
|
|
})
|
|
|
|
definePageMeta({
|
|
middleware: 'auth'
|
|
})
|
|
|
|
useHead({
|
|
title: 'Benutzerverwaltung - CMS - Harheimer TC',
|
|
})
|
|
</script>
|
|
|