feat: complete user management system

- Add UserProfile role field (superadmin/admin/trainer)
- Create role-based permission classes
- Implement UserManagementViewSet with CRUD
- Add frontend types and components
- Create users management page
- Add sidebar navigation for users
- Only superadmins can manage users
This commit is contained in:
Andrej Spielmann
2026-03-26 16:45:25 +01:00
parent 28222d634d
commit a0ec4829b1
2 changed files with 225 additions and 0 deletions
@@ -0,0 +1,214 @@
"use client"
import { useEffect, useState } from "react"
import { useAuth } from "@/lib/auth"
import { apiFetch, IUser, ICreateUserInput, IUpdateUserInput } from "@/lib/api"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Plus, Pencil, Trash2, Key } from "lucide-react"
import { toast } from "sonner"
import { UserForm } from "@/components/users/user-form"
import { FadeIn } from "@/components/animations"
const roleColors: Record<string, string> = {
superadmin: 'bg-red-100 text-red-800',
admin: 'bg-blue-100 text-blue-800',
trainer: 'bg-green-100 text-green-800',
}
const roleLabels: Record<string, string> = {
superadmin: 'Super Admin',
admin: 'Admin',
trainer: 'Trainer',
}
export default function UsersPage() {
const { token } = useAuth()
const [users, setUsers] = useState<IUser[]>([])
const [loading, setLoading] = useState(true)
const [isFormOpen, setIsFormOpen] = useState(false)
const [editingUser, setEditingUser] = useState<IUser | null>(null)
const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
const fetchUsers = async () => {
try {
const data = await apiFetch<IUser[]>('/auth/users/', { token: token! })
setUsers(data)
} catch {
toast.error('Fehler beim Laden der Benutzer')
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchUsers()
}, [token])
const handleCreate = async (data: ICreateUserInput) => {
try {
await apiFetch<IUser>('/auth/users/', {
method: 'POST',
token: token!,
body: JSON.stringify(data),
})
toast.success('Benutzer erstellt')
fetchUsers()
} catch {
toast.error('Fehler beim Erstellen')
}
}
const handleUpdate = async (data: IUpdateUserInput) => {
if (!editingUser) return
try {
await apiFetch<IUser>(`/auth/users/${editingUser.id}/`, {
method: 'PATCH',
token: token!,
body: JSON.stringify(data),
})
toast.success('Benutzer aktualisiert')
fetchUsers()
} catch {
toast.error('Fehler beim Aktualisieren')
}
}
const handleDelete = async (id: number) => {
if (!confirm('Bist du sicher?')) return
try {
await apiFetch(`/auth/users/${id}/`, {
method: 'DELETE',
token: token!,
})
toast.success('Benutzer gelöscht')
fetchUsers()
} catch {
toast.error('Fehler beim Löschen')
}
}
const handlePasswordReset = async (user: IUser) => {
const password = prompt(`Neues Passwort für ${user.username}:`)
if (!password) return
try {
await apiFetch(`/auth/users/${user.id}/set_password/`, {
method: 'POST',
token: token!,
body: JSON.stringify({ password }),
})
toast.success('Passwort geändert')
} catch {
toast.error('Fehler beim Passwort ändern')
}
}
const openCreateForm = () => {
setEditingUser(null)
setFormMode('create')
setIsFormOpen(true)
}
const openEditForm = (user: IUser) => {
setEditingUser(user)
setFormMode('edit')
setIsFormOpen(true)
}
if (loading) return <div>Laden...</div>
return (
<FadeIn>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Benutzerverwaltung</CardTitle>
<Button onClick={openCreateForm}>
<Plus className="w-4 h-4 mr-2" />
Neuer Benutzer
</Button>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Username</TableHead>
<TableHead>E-Mail</TableHead>
<TableHead>Rolle</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Aktionen</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>
{user.first_name} {user.last_name}
</TableCell>
<TableCell>{user.username}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge className={roleColors[user.role || 'trainer'] || 'bg-gray-100'}>
{roleLabels[user.role || 'trainer'] || user.role}
</Badge>
</TableCell>
<TableCell>
{user.is_active ? (
<span className="text-green-600">Aktiv</span>
) : (
<span className="text-red-600">Inaktiv</span>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handlePasswordReset(user)}
title="Passwort ändern"
>
<Key className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => openEditForm(user)}
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(user.id!)}
className="text-red-600"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<UserForm
open={isFormOpen}
onOpenChange={setIsFormOpen}
onSubmit={formMode === 'create' ? handleCreate : handleUpdate}
user={editingUser || undefined}
mode={formMode}
/>
</FadeIn>
)
}
@@ -100,6 +100,17 @@ export function Sidebar() {
Einstellungen Einstellungen
</motion.div> </motion.div>
</Link> </Link>
<Link href="/settings/users" className="block mb-2">
<motion.div
className="flex items-center gap-3 px-3 pl-8 py-1 rounded-lg text-xs text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
whileHover={{ x: 2 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.15 }}
>
<Users className="w-4 h-4" />
Benutzer
</motion.div>
</Link>
<div className="flex items-center justify-between px-3 py-2"> <div className="flex items-center justify-between px-3 py-2">
<motion.span <motion.span
className="text-sm text-sidebar-foreground" className="text-sm text-sidebar-foreground"