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:
@@ -0,0 +1,26 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = 3000;
|
||||
const PUBLIC_DIR = path.join(__dirname, 'public');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
let filePath = path.join(PUBLIC_DIR, req.url === '/' ? 'index.html' : req.url);
|
||||
|
||||
// Set CORS headers for PWA
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
// Set cache headers for icons and manifest
|
||||
if (req.url.includes('.png') || req.url.includes('.json') || req.url.includes('.svg')) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath);
|
||||
const contentTypes = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'text/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
};
|
||||
|
||||
const contentType = contentTypes[ext] || 'application/octet-stream';
|
||||
|
||||
fs.readFile(filePath, (err, content) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||
res.end('<h1>404 Not Found</h1>');
|
||||
} else {
|
||||
res.writeHead(500);
|
||||
res.end(`Server Error: ${err.code}`);
|
||||
}
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(content, 'utf-8');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`PWA Server running at http://localhost:${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -265,6 +265,27 @@ export interface IUser {
|
||||
last_name?: string
|
||||
club_id?: number | null
|
||||
club_name?: string | null
|
||||
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'
|
||||
}
|
||||
|
||||
export interface IAuthResponse {
|
||||
|
||||
Reference in New Issue
Block a user