feat: implement user management system

- Add role field to UserProfile (superadmin/admin/trainer)
- Add role-based permission classes
- Create UserManagementViewSet with CRUD and password change
- Add API types and components for user management
- Create users management page in settings
- Only superadmins can manage users
This commit is contained in:
Andrej Spielmann
2026-03-26 16:42:08 +01:00
parent 7611533718
commit 28222d634d
19 changed files with 1960 additions and 7 deletions
@@ -0,0 +1,806 @@
# User Management Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement user management system with roles (superadmin, admin, trainer) where superadmins can create, edit, delete users and assign roles via Settings page.
**Architecture:** Extend Django User model with Role model, create UserManagementViewSet with permission-based access control, build React Settings page with user CRUD operations.
**Tech Stack:** Django + DRF (backend), Next.js + Shadcn UI (frontend), JWT Auth
---
## File Structure
| File | Purpose |
|------|---------|
| `backend/auth_app/models.py` | Add Role model |
| `backend/auth_app/serializers.py` | User management serializers |
| `backend/auth_app/views.py` | UserManagementViewSet |
| `backend/auth_app/permissions.py` | Role-based permissions |
| `backend/auth_app/urls.py` | Add user management routes |
| `frontend/src/app/(dashboard)/settings/users/page.tsx` | Users management page |
| `frontend/src/components/users/user-form.tsx` | Create/Edit user form |
| `frontend/src/components/users/user-table.tsx` | Users table with actions |
| `frontend/src/lib/api.ts` | Add user management types |
---
## Task 1: Create Role Model
**Files:**
- Modify: `backend/auth_app/models.py`
- [ ] **Step 1: Add Role model with choices**
```python
from django.db import models
from django.contrib.auth.models import User
class UserRole(models.Model):
ROLE_CHOICES = [
('superadmin', 'Super Admin'),
('admin', 'Admin'),
('trainer', 'Trainer'),
]
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='role')
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='trainer')
class Meta:
verbose_name = 'User Role'
verbose_name_plural = 'User Roles'
def __str__(self):
return f"{self.user.username} - {self.get_role_display()}"
```
- [ ] **Step 2: Create and run migration**
```bash
cd /Volumes/T3/Opencode/WrestleDesk/backend
python manage.py makemigrations auth_app
python manage.py migrate
```
- [ ] **Step 3: Commit**
```bash
git add backend/auth_app/models.py backend/auth_app/migrations/
git commit -m "feat(auth): add UserRole model with superadmin/admin/trainer roles"
```
---
## Task 2: Create Role-Based Permissions
**Files:**
- Create: `backend/auth_app/permissions.py`
- [ ] **Step 1: Create permission classes**
```python
from rest_framework import permissions
class IsSuperAdmin(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.is_authenticated and hasattr(request.user, 'role') and request.user.role.role == 'superadmin'
class IsAdminOrSuperAdmin(permissions.BasePermission):
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
if not hasattr(request.user, 'role'):
return False
return request.user.role.role in ['admin', 'superadmin']
class HasUserManagementAccess(permissions.BasePermission):
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
if not hasattr(request.user, 'role'):
return False
return request.user.role.role == 'superadmin'
```
- [ ] **Step 2: Commit**
```bash
git add backend/auth_app/permissions.py
git commit -m "feat(auth): add role-based permission classes"
```
---
## Task 3: Create User Management Serializers
**Files:**
- Modify: `backend/auth_app/serializers.py` (create if not exists)
- [ ] **Step 1: Create user management serializers**
```python
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import UserRole
class UserRoleSerializer(serializers.ModelSerializer):
class Meta:
model = UserRole
fields = ['role']
class UserListSerializer(serializers.ModelSerializer):
role = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'is_active', 'role', 'date_joined']
def get_role(self, obj):
if hasattr(obj, 'role'):
return obj.role.role
return 'trainer'
class UserCreateSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
role = serializers.ChoiceField(choices=UserRole.ROLE_CHOICES, default='trainer')
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'password', 'role']
def create(self, validated_data):
role = validated_data.pop('role', 'trainer')
user = User.objects.create_user(**validated_data)
UserRole.objects.create(user=user, role=role)
return user
class UserUpdateSerializer(serializers.ModelSerializer):
role = serializers.ChoiceField(choices=UserRole.ROLE_CHOICES, required=False)
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'is_active', 'role']
def update(self, instance, validated_data):
role = validated_data.pop('role', None)
user = super().update(instance, validated_data)
if role and hasattr(user, 'role'):
user.role.role = role
user.role.save()
return user
class PasswordChangeSerializer(serializers.Serializer):
password = serializers.CharField(write_only=True, required=True)
```
- [ ] **Step 2: Commit**
```bash
git add backend/auth_app/serializers.py
git commit -m "feat(auth): add user management serializers"
```
---
## Task 4: Create User Management ViewSet
**Files:**
- Modify: `backend/auth_app/views.py`
- [ ] **Step 1: Add UserManagementViewSet**
```python
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.contrib.auth.models import User
from .models import UserRole
from .serializers import (
UserListSerializer,
UserCreateSerializer,
UserUpdateSerializer,
PasswordChangeSerializer
)
from .permissions import IsSuperAdmin, HasUserManagementAccess
class UserManagementViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().select_related('role')
permission_classes = [HasUserManagementAccess]
def get_serializer_class(self):
if self.action == 'create':
return UserCreateSerializer
elif self.action in ['update', 'partial_update']:
return UserUpdateSerializer
return UserListSerializer
def get_queryset(self):
return User.objects.all().select_related('role').order_by('-date_joined')
@action(detail=True, methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordChangeSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.validated_data['password'])
user.save()
return Response({'status': 'password set'})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
```
- [ ] **Step 2: Commit**
```bash
git add backend/auth_app/views.py
git commit -m "feat(auth): add UserManagementViewSet with CRUD and password change"
```
---
## Task 5: Add URL Routes
**Files:**
- Modify: `backend/auth_app/urls.py` (create if not exists)
- [ ] **Step 1: Add user management routes**
```python
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UserManagementViewSet
router = DefaultRouter()
router.register(r'users', UserManagementViewSet, basename='usermanagement')
urlpatterns = [
path('', include(router.urls)),
]
```
- [ ] **Step 2: Include in main urls.py**
Modify `backend/wrestleDesk/urls.py`:
```python
path('api/v1/auth/', include('auth_app.urls')),
```
- [ ] **Step 3: Commit**
```bash
git add backend/auth_app/urls.py backend/wrestleDesk/urls.py
git commit -m "feat(auth): add user management URL routes"
```
---
## Task 6: Add User Management Types to Frontend
**Files:**
- Modify: `frontend/src/lib/api.ts`
- [ ] **Step 1: Add types**
```typescript
export interface IUserRole {
role: 'superadmin' | 'admin' | 'trainer'
}
export interface IUser {
id: number
username: string
email: string
first_name: string
last_name: string
is_active: boolean
role: string
date_joined: string
}
export interface ICreateUserInput {
username: string
email: string
first_name: string
last_name: string
password: string
role: 'superadmin' | 'admin' | 'trainer'
}
export interface IUpdateUserInput {
username?: string
email?: string
first_name?: string
last_name?: string
is_active?: boolean
role?: 'superadmin' | 'admin' | 'trainer'
}
```
- [ ] **Step 2: Commit**
```bash
git add frontend/src/lib/api.ts
git commit -m "feat(api): add user management types"
```
---
## Task 7: Create User Form Component
**Files:**
- Create: `frontend/src/components/users/user-form.tsx`
- [ ] **Step 1: Create user form component**
```tsx
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { ICreateUserInput, IUpdateUserInput, IUser } from "@/lib/api"
interface UserFormProps {
open: boolean
onOpenChange: (open: boolean) => void
onSubmit: (data: ICreateUserInput | IUpdateUserInput) => Promise<void>
user?: IUser
mode: 'create' | 'edit'
}
const roles = [
{ value: 'superadmin', label: 'Super Admin' },
{ value: 'admin', label: 'Admin' },
{ value: 'trainer', label: 'Trainer' },
]
export function UserForm({ open, onOpenChange, onSubmit, user, mode }: UserFormProps) {
const [loading, setLoading] = useState(false)
const [formData, setFormData] = useState<ICreateUserInput | IUpdateUserInput>({
username: user?.username || '',
email: user?.email || '',
first_name: user?.first_name || '',
last_name: user?.last_name || '',
password: '',
role: (user?.role as any) || 'trainer',
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
await onSubmit(formData)
onOpenChange(false)
} finally {
setLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>
{mode === 'create' ? 'Neuer Benutzer' : 'Benutzer bearbeiten'}
</DialogTitle>
<DialogDescription>
{mode === 'create'
? 'Erstelle einen neuen Benutzer mit Rolle'
: 'Bearbeite die Benutzerdaten'}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">Vorname</Label>
<Input
id="firstName"
value={formData.first_name}
onChange={(e) => setFormData({ ...formData, first_name: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Nachname</Label>
<Input
id="lastName"
value={formData.last_name}
onChange={(e) => setFormData({ ...formData, last_name: e.target.value })}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">E-Mail</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
</div>
{mode === 'create' && (
<div className="space-y-2">
<Label htmlFor="password">Passwort</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required={mode === 'create'}
/>
</div>
)}
<div className="space-y-2">
<Label htmlFor="role">Rolle</Label>
<Select
value={formData.role}
onValueChange={(value) => setFormData({ ...formData, role: value as any })}
>
<SelectTrigger>
<SelectValue placeholder="Rolle auswählen" />
</SelectTrigger>
<SelectContent>
{roles.map((role) => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex justify-end gap-2 pt-4">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Abbrechen
</Button>
<Button type="submit" disabled={loading}>
{loading ? 'Speichern...' : 'Speichern'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}
```
- [ ] **Step 2: Commit**
```bash
git add frontend/src/components/users/user-form.tsx
git commit -m "feat(users): add user form component"
```
---
## Task 8: Create Users Page
**Files:**
- Create: `frontend/src/app/(dashboard)/settings/users/page.tsx`
- [ ] **Step 1: Create users management page**
```tsx
"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] || 'bg-gray-100'}>
{roleLabels[user.role] || 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>
)
}
```
- [ ] **Step 2: Commit**
```bash
git add frontend/src/app/(dashboard)/settings/users/page.tsx
git commit -m "feat(users): add user management page"
```
---
## Task 9: Add Sidebar Navigation
**Files:**
- Modify: `frontend/src/components/layout/Sidebar.tsx`
- [ ] **Step 1: Add Users link to Settings section**
Füge unter der Settings Section hinzu:
```tsx
{
title: "Einstellungen",
icon: Settings,
href: "/settings",
subItems: [
{ title: "Benutzer", href: "/settings/users" },
],
}
```
- [ ] **Step 2: Commit**
```bash
git add frontend/src/components/layout/Sidebar.tsx
git commit -m "feat(nav): add users link to settings sidebar"
```
---
## Task 10: Final Verification
- [ ] **Step 1: Test Backend API**
```bash
curl -X GET http://localhost:8000/api/v1/auth/users/ \
-H "Authorization: Bearer <dein-token>"
```
- [ ] **Step 2: Build Frontend**
```bash
cd frontend && npm run build
```
- [ ] **Step 3: Final Commit and Push**
```bash
git push origin feature/pwa
```
---
## Summary
**Was implementiert wird:**
1. UserRole Model (superadmin, admin, trainer)
2. Permission classes für Rollen
3. UserManagementViewSet (CRUD + Passwort ändern)
4. Frontend User Form und Table
5. Settings/Users Page
6. Sidebar Navigation
**Rollen-Berechtigungen:**
- **superadmin**: Kann alle User verwalten, Rollen zuweisen
- **admin**: Kann User sehen, aber nicht verwalten (optional für später)
- **trainer**: Kein Zugriff auf Settings (nur superadmin hat Zugriff)
Nach dem Deploy: Nur User mit `role='superadmin'` können die Users-Seite sehen!