mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
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:
parent
85574a85aa
commit
8efaa49930
3 changed files with 101 additions and 0 deletions
|
|
@ -3,6 +3,7 @@ import {
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Patch,
|
Patch,
|
||||||
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
|
|
@ -124,4 +125,16 @@ export class UsersController {
|
||||||
) {
|
) {
|
||||||
return this.usersService.update(id, dto);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,21 @@ export class UsersService {
|
||||||
this.logger.log(`Passwort geändert für User ${user.email}`);
|
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).
|
* Alle User auflisten (für Admin).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
|
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
|
||||||
import api from '../api/client';
|
import api from '../api/client';
|
||||||
|
import { Modal } from '../components/Modal';
|
||||||
import { UserFormModal } from './UserFormModal';
|
import { UserFormModal } from './UserFormModal';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
|
|
@ -48,6 +49,7 @@ export function AdminUsersPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
|
||||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||||
|
const [deletingUser, setDeletingUser] = useState<User | null>(null);
|
||||||
|
|
||||||
const { data, isLoading, error } = useQuery<UsersResponse>({
|
const { data, isLoading, error } = useQuery<UsersResponse>({
|
||||||
queryKey: ['admin', 'users'],
|
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 (isLoading) return <p>Laden...</p>;
|
||||||
if (error) return <p style={{ color: 'var(--color-error)' }}>Fehler beim Laden der Benutzer</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'}
|
{user.isActive ? 'Deaktivieren' : 'Aktivieren'}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -204,6 +228,55 @@ export function AdminUsersPage() {
|
||||||
user={editingUser}
|
user={editingUser}
|
||||||
onSuccess={() => setEditingUser(null)}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue