feat: add user deletion (backend endpoint + frontend with confirmation)

- Backend: DELETE /api/v1/users/:id endpoint (PLATFORM_ADMIN only)
- Frontend: "Löschen" button with confirmation modal
- Cascading deletes handle auth providers, memberships, profiles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-09 22:00:06 +01:00
parent 85574a85aa
commit 8efaa49930
3 changed files with 101 additions and 0 deletions

View file

@ -3,6 +3,7 @@ import {
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
@ -124,4 +125,16 @@ export class UsersController {
) {
return this.usersService.update(id, dto);
}
/**
* DELETE /api/v1/users/:id
* User löschen (nur PLATFORM_ADMIN).
*/
@Delete(':id')
@Roles('PLATFORM_ADMIN')
@UseGuards(RolesGuard)
@ApiOperation({ summary: 'Benutzer löschen (Admin)' })
async delete(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.delete(id);
}
}

View file

@ -239,6 +239,21 @@ export class UsersService {
this.logger.log(`Passwort geändert für User ${user.email}`);
}
/**
* User löschen (inkl. Auth-Provider, Memberships, Profil via Cascade).
*/
async delete(id: string) {
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) {
throw new NotFoundException('Benutzer nicht gefunden');
}
await this.prisma.user.delete({ where: { id } });
this.logger.log(`User gelöscht: ${user.email}`);
return { message: 'Benutzer wurde gelöscht' };
}
/**
* Alle User auflisten (für Admin).
*/

View file

@ -1,6 +1,7 @@
import { useState } from 'react';
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
import api from '../api/client';
import { Modal } from '../components/Modal';
import { UserFormModal } from './UserFormModal';
interface User {
@ -48,6 +49,7 @@ export function AdminUsersPage() {
const queryClient = useQueryClient();
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [deletingUser, setDeletingUser] = useState<User | null>(null);
const { data, isLoading, error } = useQuery<UsersResponse>({
queryKey: ['admin', 'users'],
@ -65,6 +67,14 @@ export function AdminUsersPage() {
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => api.delete(`/users/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
setDeletingUser(null);
},
});
if (isLoading) return <p>Laden...</p>;
if (error) return <p style={{ color: 'var(--color-error)' }}>Fehler beim Laden der Benutzer</p>;
@ -181,6 +191,20 @@ export function AdminUsersPage() {
>
{user.isActive ? 'Deaktivieren' : 'Aktivieren'}
</button>
<button
onClick={() => setDeletingUser(user)}
style={{
padding: '0.25rem 0.625rem',
fontSize: '0.8125rem',
background: 'transparent',
border: '1px solid #fecaca',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
color: 'var(--color-error)',
}}
>
Löschen
</button>
</div>
</td>
</tr>
@ -204,6 +228,55 @@ export function AdminUsersPage() {
user={editingUser}
onSuccess={() => setEditingUser(null)}
/>
{/* Modal: Benutzer löschen — Bestätigung */}
<Modal
isOpen={!!deletingUser}
onClose={() => setDeletingUser(null)}
title="Benutzer löschen"
maxWidth="420px"
>
<p style={{ fontSize: '0.9375rem', color: 'var(--color-text)', marginBottom: '0.5rem' }}>
Soll der Benutzer <strong>{deletingUser?.firstName} {deletingUser?.lastName}</strong> ({deletingUser?.email}) wirklich gelöscht werden?
</p>
<p style={{ fontSize: '0.8125rem', color: 'var(--color-error)', marginBottom: '1.5rem' }}>
Diese Aktion kann nicht rückgängig gemacht werden. Alle Daten des Benutzers werden unwiderruflich gelöscht.
</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
<button
onClick={() => setDeletingUser(null)}
disabled={deleteMutation.isPending}
style={{
padding: '0.5rem 1rem',
background: 'transparent',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem',
cursor: 'pointer',
color: 'var(--color-text-secondary)',
}}
>
Abbrechen
</button>
<button
onClick={() => deletingUser && deleteMutation.mutate(deletingUser.id)}
disabled={deleteMutation.isPending}
style={{
padding: '0.5rem 1rem',
background: 'var(--color-error)',
color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem',
fontWeight: 600,
cursor: deleteMutation.isPending ? 'wait' : 'pointer',
opacity: deleteMutation.isPending ? 0.7 : 1,
}}
>
{deleteMutation.isPending ? 'Löschen...' : 'Endgültig löschen'}
</button>
</div>
</Modal>
</div>
);
}