feat: document user management system and RBAC implementation
- Update copilot-instructions.md with user model, API routes, and frontend patterns - Update README.md with RBAC details, user management API, and security sections - Add user management technical documentation to TECH-CHANGELOG.md - Bump version to 2025.1.0-alpha.13 with user management changelog entries
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appName": "Infoscreen-Management",
|
||||
"version": "2025.1.0-alpha.12",
|
||||
"version": "2025.1.0-alpha.13",
|
||||
"copyright": "© 2025 Third-Age-Applications",
|
||||
"supportContact": "support@third-age-applications.com",
|
||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||
@@ -26,10 +26,28 @@
|
||||
]
|
||||
},
|
||||
"buildInfo": {
|
||||
"buildDate": "2025-11-27T12:00:00Z",
|
||||
"buildDate": "2025-12-29T12:00:00Z",
|
||||
"commitId": "9f2ae8b44c3a"
|
||||
},
|
||||
"changelog": [
|
||||
{
|
||||
"version": "2025.1.0-alpha.13",
|
||||
"date": "2025-12-29",
|
||||
"changes": [
|
||||
"👥 UI: Neue 'Benutzer'-Seite mit vollständiger Benutzerverwaltung (CRUD) für Admins und Superadmins.",
|
||||
"🔐 Benutzer-Seite: Sortierbare Gitter-Tabelle mit Benutzer-ID, Benutzername und Rolle; 20 Einträge pro Seite.",
|
||||
"📊 Benutzer-Seite: Statistik-Karten zeigen Gesamtanzahl, aktive und inaktive Benutzer.",
|
||||
"➕ Benutzer-Seite: Dialog zum Erstellen neuer Benutzer (Benutzername, Passwort, Rolle, Status).",
|
||||
"✏️ Benutzer-Seite: Dialog zum Bearbeiten von Benutzer-Details mit Schutz vor Selbst-Änderungen.",
|
||||
"🔑 Benutzer-Seite: Dialog zum Zurücksetzen von Passwörtern durch Admins (ohne alte Passwort-Anfrage).",
|
||||
"❌ Benutzer-Seite: Dialog zum Löschen von Benutzern (nur für Superadmins; verhindert Selbst-Löschung).",
|
||||
"📋 Benutzer-Seite: Details-Modal zeigt Audit-Informationen (letzte Anmeldung, Passwort-Änderung, Abmeldungen).",
|
||||
"🎨 Benutzer-Seite: Rollen-Abzeichen mit Farb-Kodierung (Benutzer: grau, Editor: blau, Admin: grün, Superadmin: rot).",
|
||||
"🔒 Header-Menü: Neue 'Passwort ändern'-Option im Benutzer-Dropdown für Selbstbedienung (alle Benutzer).",
|
||||
"🔐 Passwort-Dialog: Authentifizierung mit aktuellem Passwort erforderlich (min. 6 Zeichen für neues Passwort).",
|
||||
"🎯 Rollenbasiert: Menu-Einträge werden basierend auf Benutzer-Rolle gefiltert (z.B. 'Benutzer' nur für Admin+)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2025.1.0-alpha.12",
|
||||
"date": "2025-11-27",
|
||||
|
||||
@@ -4,7 +4,8 @@ import { SidebarComponent } from '@syncfusion/ej2-react-navigations';
|
||||
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
||||
import { DropDownButtonComponent } from '@syncfusion/ej2-react-splitbuttons';
|
||||
import type { MenuEventArgs } from '@syncfusion/ej2-splitbuttons';
|
||||
import { TooltipComponent } from '@syncfusion/ej2-react-popups';
|
||||
import { TooltipComponent, DialogComponent } from '@syncfusion/ej2-react-popups';
|
||||
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
|
||||
import logo from './assets/logo.png';
|
||||
import './App.css';
|
||||
|
||||
@@ -25,16 +26,16 @@ import {
|
||||
import { ToastProvider } from './components/ToastProvider';
|
||||
|
||||
const sidebarItems = [
|
||||
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
||||
{ name: 'Termine', path: '/termine', icon: Calendar },
|
||||
{ name: 'Ressourcen', path: '/ressourcen', icon: Boxes },
|
||||
{ name: 'Raumgruppen', path: '/infoscr_groups', icon: MonitorDotIcon },
|
||||
{ name: 'Infoscreen-Clients', path: '/clients', icon: Monitor },
|
||||
{ name: 'Erweiterungsmodus', path: '/setup', icon: Wrench },
|
||||
{ name: 'Medien', path: '/medien', icon: Image },
|
||||
{ name: 'Benutzer', path: '/benutzer', icon: User },
|
||||
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings },
|
||||
{ name: 'Programminfo', path: '/programminfo', icon: Info },
|
||||
{ name: 'Dashboard', path: '/', icon: LayoutDashboard, minRole: 'user' },
|
||||
{ name: 'Termine', path: '/termine', icon: Calendar, minRole: 'user' },
|
||||
{ name: 'Ressourcen', path: '/ressourcen', icon: Boxes, minRole: 'editor' },
|
||||
{ name: 'Raumgruppen', path: '/infoscr_groups', icon: MonitorDotIcon, minRole: 'admin' },
|
||||
{ name: 'Infoscreen-Clients', path: '/clients', icon: Monitor, minRole: 'admin' },
|
||||
{ name: 'Erweiterungsmodus', path: '/setup', icon: Wrench, minRole: 'admin' },
|
||||
{ name: 'Medien', path: '/medien', icon: Image, minRole: 'editor' },
|
||||
{ name: 'Benutzer', path: '/benutzer', icon: User, minRole: 'admin' },
|
||||
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings, minRole: 'admin' },
|
||||
{ name: 'Programminfo', path: '/programminfo', icon: Info, minRole: 'user' },
|
||||
];
|
||||
|
||||
// Dummy Components (können in eigene Dateien ausgelagert werden)
|
||||
@@ -51,6 +52,8 @@ import Programminfo from './programminfo';
|
||||
import Logout from './logout';
|
||||
import Login from './login';
|
||||
import { useAuth } from './useAuth';
|
||||
import { changePassword } from './apiAuth';
|
||||
import { useToast } from './components/ToastProvider';
|
||||
|
||||
// ENV aus .env holen (Platzhalter, im echten Projekt über process.env oder API)
|
||||
// const ENV = import.meta.env.VITE_ENV || 'development';
|
||||
@@ -60,8 +63,16 @@ const Layout: React.FC = () => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
let sidebarRef: SidebarComponent | null;
|
||||
const { user } = useAuth();
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Change password dialog state
|
||||
const [showPwdDialog, setShowPwdDialog] = useState(false);
|
||||
const [pwdCurrent, setPwdCurrent] = useState('');
|
||||
const [pwdNew, setPwdNew] = useState('');
|
||||
const [pwdConfirm, setPwdConfirm] = useState('');
|
||||
const [pwdBusy, setPwdBusy] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetch('/program-info.json')
|
||||
.then(res => res.json())
|
||||
@@ -87,6 +98,33 @@ const Layout: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitPasswordChange = async () => {
|
||||
if (!pwdCurrent || !pwdNew || !pwdConfirm) {
|
||||
toast.show({ content: 'Bitte alle Felder ausfüllen', cssClass: 'e-toast-warning' });
|
||||
return;
|
||||
}
|
||||
if (pwdNew.length < 6) {
|
||||
toast.show({ content: 'Neues Passwort muss mindestens 6 Zeichen haben', cssClass: 'e-toast-warning' });
|
||||
return;
|
||||
}
|
||||
if (pwdNew !== pwdConfirm) {
|
||||
toast.show({ content: 'Passwörter stimmen nicht überein', cssClass: 'e-toast-warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
setPwdBusy(true);
|
||||
try {
|
||||
await changePassword(pwdCurrent, pwdNew);
|
||||
toast.show({ content: 'Passwort erfolgreich geändert', cssClass: 'e-toast-success' });
|
||||
setShowPwdDialog(false);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Ändern des Passworts';
|
||||
toast.show({ content: msg, cssClass: 'e-toast-danger' });
|
||||
} finally {
|
||||
setPwdBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sidebarTemplate = () => (
|
||||
<div
|
||||
className={`sidebar-theme ${isCollapsed ? 'collapsed' : 'expanded'}`}
|
||||
@@ -132,7 +170,16 @@ const Layout: React.FC = () => {
|
||||
minHeight: 0, // Wichtig für Flex-Shrinking
|
||||
}}
|
||||
>
|
||||
{sidebarItems.map(item => {
|
||||
{sidebarItems
|
||||
.filter(item => {
|
||||
// Only show items the current user is allowed to see
|
||||
if (!user) return false;
|
||||
const roleHierarchy = ['user', 'editor', 'admin', 'superadmin'];
|
||||
const userRoleIndex = roleHierarchy.indexOf(user.role);
|
||||
const itemRoleIndex = roleHierarchy.indexOf(item.minRole || 'user');
|
||||
return userRoleIndex >= itemRoleIndex;
|
||||
})
|
||||
.map(item => {
|
||||
const Icon = item.icon;
|
||||
const linkContent = (
|
||||
<Link
|
||||
@@ -305,13 +352,16 @@ const Layout: React.FC = () => {
|
||||
{user && (
|
||||
<DropDownButtonComponent
|
||||
items={[
|
||||
{ text: 'Profil', id: 'profile', iconCss: 'e-icons e-user' },
|
||||
{ text: 'Passwort ändern', id: 'change-password', iconCss: 'e-icons e-lock' },
|
||||
{ separator: true },
|
||||
{ text: 'Abmelden', id: 'logout', iconCss: 'e-icons e-logout' },
|
||||
]}
|
||||
select={(args: MenuEventArgs) => {
|
||||
if (args.item.id === 'profile') {
|
||||
navigate('/benutzer');
|
||||
if (args.item.id === 'change-password') {
|
||||
setPwdCurrent('');
|
||||
setPwdNew('');
|
||||
setPwdConfirm('');
|
||||
setShowPwdDialog(true);
|
||||
} else if (args.item.id === 'logout') {
|
||||
navigate('/logout');
|
||||
}
|
||||
@@ -339,6 +389,57 @@ const Layout: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<DialogComponent
|
||||
isModal={true}
|
||||
visible={showPwdDialog}
|
||||
width="480px"
|
||||
header="Passwort ändern"
|
||||
showCloseIcon={true}
|
||||
close={() => setShowPwdDialog(false)}
|
||||
footerTemplate={() => (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<ButtonComponent cssClass="e-flat" onClick={() => setShowPwdDialog(false)} disabled={pwdBusy}>
|
||||
Abbrechen
|
||||
</ButtonComponent>
|
||||
<ButtonComponent cssClass="e-primary" onClick={submitPasswordChange} disabled={pwdBusy}>
|
||||
{pwdBusy ? 'Speichere...' : 'Speichern'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 500 }}>Aktuelles Passwort *</label>
|
||||
<TextBoxComponent
|
||||
type="password"
|
||||
placeholder="Aktuelles Passwort"
|
||||
value={pwdCurrent}
|
||||
input={(e: any) => setPwdCurrent(e.value)}
|
||||
disabled={pwdBusy}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 500 }}>Neues Passwort *</label>
|
||||
<TextBoxComponent
|
||||
type="password"
|
||||
placeholder="Mindestens 6 Zeichen"
|
||||
value={pwdNew}
|
||||
input={(e: any) => setPwdNew(e.value)}
|
||||
disabled={pwdBusy}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 500 }}>Neues Passwort bestätigen *</label>
|
||||
<TextBoxComponent
|
||||
type="password"
|
||||
placeholder="Wiederholen"
|
||||
value={pwdConfirm}
|
||||
input={(e: any) => setPwdConfirm(e.value)}
|
||||
disabled={pwdBusy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
<main className="page-content">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
@@ -31,6 +31,26 @@ export interface AuthCheckResponse {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password for the currently authenticated user.
|
||||
*/
|
||||
export async function changePassword(currentPassword: string, newPassword: string): Promise<{ message: string }> {
|
||||
const res = await fetch('/api/auth/change-password', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to change password');
|
||||
}
|
||||
|
||||
return data as { message: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user with username and password.
|
||||
*
|
||||
|
||||
161
dashboard/src/apiUsers.ts
Normal file
161
dashboard/src/apiUsers.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* User management API client.
|
||||
*
|
||||
* Provides functions to manage users (CRUD operations).
|
||||
* Access is role-based: admin can manage user/editor/admin, superadmin can manage all.
|
||||
*/
|
||||
|
||||
export interface UserData {
|
||||
id: number;
|
||||
username: string;
|
||||
role: 'user' | 'editor' | 'admin' | 'superadmin';
|
||||
isActive: boolean;
|
||||
lastLoginAt?: string;
|
||||
lastPasswordChangeAt?: string;
|
||||
lastFailedLoginAt?: string;
|
||||
failedLoginAttempts?: number;
|
||||
lockedUntil?: string;
|
||||
deactivatedAt?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
role: 'user' | 'editor' | 'admin' | 'superadmin';
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
username?: string;
|
||||
role?: 'user' | 'editor' | 'admin' | 'superadmin';
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all users (filtered by current user's role).
|
||||
* Admin sees: user, editor, admin
|
||||
* Superadmin sees: all including superadmin
|
||||
*/
|
||||
export async function listUsers(): Promise<UserData[]> {
|
||||
const res = await fetch('/api/users', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || 'Failed to fetch users');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single user by ID.
|
||||
*/
|
||||
export async function getUser(userId: number): Promise<UserData> {
|
||||
const res = await fetch(`/api/users/${userId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || 'Failed to fetch user');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user.
|
||||
* Admin: can create user, editor, admin
|
||||
* Superadmin: can create any role including superadmin
|
||||
*/
|
||||
export async function createUser(userData: CreateUserRequest): Promise<UserData & { message: string }> {
|
||||
const res = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(userData),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to create user');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's details.
|
||||
* Restrictions:
|
||||
* - Cannot change own role
|
||||
* - Cannot change own active status
|
||||
* - Admin cannot edit superadmin users
|
||||
*/
|
||||
export async function updateUser(userId: number, userData: UpdateUserRequest): Promise<UserData & { message: string }> {
|
||||
const res = await fetch(`/api/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(userData),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to update user');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a user's password.
|
||||
* Admin: cannot reset superadmin passwords
|
||||
* Superadmin: can reset any password
|
||||
*/
|
||||
export async function resetUserPassword(userId: number, password: string): Promise<{ message: string }> {
|
||||
const res = await fetch(`/api/users/${userId}/password`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to reset password');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a user (superadmin only).
|
||||
* Cannot delete own account.
|
||||
*/
|
||||
export async function deleteUser(userId: number): Promise<{ message: string }> {
|
||||
const res = await fetch(`/api/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to delete user');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,8 +1,822 @@
|
||||
import React from 'react';
|
||||
const Benutzer: React.FC = () => (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">Benutzer</h2>
|
||||
<p>Willkommen im Infoscreen-Management Benutzer.</p>
|
||||
</div>
|
||||
);
|
||||
import { useAuth } from './useAuth';
|
||||
import {
|
||||
GridComponent,
|
||||
ColumnsDirective,
|
||||
ColumnDirective,
|
||||
Page,
|
||||
Inject,
|
||||
Toolbar,
|
||||
Edit,
|
||||
CommandColumn,
|
||||
type EditSettingsModel,
|
||||
type CommandModel,
|
||||
} from '@syncfusion/ej2-react-grids';
|
||||
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
||||
import { DialogComponent } from '@syncfusion/ej2-react-popups';
|
||||
import { ToastComponent } from '@syncfusion/ej2-react-notifications';
|
||||
import { TextBoxComponent } from '@syncfusion/ej2-react-inputs';
|
||||
import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns';
|
||||
import { CheckBoxComponent } from '@syncfusion/ej2-react-buttons';
|
||||
import {
|
||||
listUsers,
|
||||
createUser,
|
||||
updateUser,
|
||||
resetUserPassword,
|
||||
deleteUser,
|
||||
type UserData,
|
||||
} from './apiUsers';
|
||||
|
||||
const Benutzer: React.FC = () => {
|
||||
const { user: currentUser } = useAuth();
|
||||
const [users, setUsers] = React.useState<UserData[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
|
||||
// Dialog states
|
||||
const [showCreateDialog, setShowCreateDialog] = React.useState(false);
|
||||
const [showEditDialog, setShowEditDialog] = React.useState(false);
|
||||
const [showPasswordDialog, setShowPasswordDialog] = React.useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = React.useState(false);
|
||||
const [showDetailsDialog, setShowDetailsDialog] = React.useState(false);
|
||||
const [selectedUser, setSelectedUser] = React.useState<UserData | null>(null);
|
||||
|
||||
// Form states
|
||||
const [formUsername, setFormUsername] = React.useState('');
|
||||
const [formPassword, setFormPassword] = React.useState('');
|
||||
const [formRole, setFormRole] = React.useState<'user' | 'editor' | 'admin' | 'superadmin'>('user');
|
||||
const [formIsActive, setFormIsActive] = React.useState(true);
|
||||
const [formBusy, setFormBusy] = React.useState(false);
|
||||
|
||||
const toastRef = React.useRef<ToastComponent>(null);
|
||||
|
||||
const isSuperadmin = currentUser?.role === 'superadmin';
|
||||
|
||||
// Available roles based on current user's role
|
||||
const availableRoles = React.useMemo(() => {
|
||||
if (isSuperadmin) {
|
||||
return [
|
||||
{ value: 'user', text: 'Benutzer (Viewer)' },
|
||||
{ value: 'editor', text: 'Editor (Content Manager)' },
|
||||
{ value: 'admin', text: 'Administrator' },
|
||||
{ value: 'superadmin', text: 'Superadministrator' },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: 'user', text: 'Benutzer (Viewer)' },
|
||||
{ value: 'editor', text: 'Editor (Content Manager)' },
|
||||
{ value: 'admin', text: 'Administrator' },
|
||||
];
|
||||
}, [isSuperadmin]);
|
||||
|
||||
const showToast = (content: string, cssClass: string = 'e-toast-success') => {
|
||||
if (toastRef.current) {
|
||||
toastRef.current.show({
|
||||
content,
|
||||
cssClass,
|
||||
timeOut: 4000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = React.useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await listUsers();
|
||||
setUsers(data);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Fehler beim Laden der Benutzer';
|
||||
showToast(message, 'e-toast-danger');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadUsers();
|
||||
}, [loadUsers]);
|
||||
|
||||
// Create user
|
||||
const handleCreateClick = () => {
|
||||
setFormUsername('');
|
||||
setFormPassword('');
|
||||
setFormRole('user');
|
||||
setFormIsActive(true);
|
||||
setShowCreateDialog(true);
|
||||
};
|
||||
|
||||
const handleCreateSubmit = async () => {
|
||||
if (!formUsername.trim()) {
|
||||
showToast('Benutzername ist erforderlich', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
if (formUsername.trim().length < 3) {
|
||||
showToast('Benutzername muss mindestens 3 Zeichen lang sein', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
if (!formPassword) {
|
||||
showToast('Passwort ist erforderlich', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
if (formPassword.length < 6) {
|
||||
showToast('Passwort muss mindestens 6 Zeichen lang sein', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
|
||||
setFormBusy(true);
|
||||
try {
|
||||
await createUser({
|
||||
username: formUsername.trim(),
|
||||
password: formPassword,
|
||||
role: formRole,
|
||||
isActive: formIsActive,
|
||||
});
|
||||
showToast('Benutzer erfolgreich erstellt', 'e-toast-success');
|
||||
setShowCreateDialog(false);
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Fehler beim Erstellen des Benutzers';
|
||||
showToast(message, 'e-toast-danger');
|
||||
} finally {
|
||||
setFormBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Edit user
|
||||
const handleEditClick = (userData: UserData) => {
|
||||
setSelectedUser(userData);
|
||||
setFormUsername(userData.username);
|
||||
setFormRole(userData.role);
|
||||
setFormIsActive(userData.isActive);
|
||||
setShowEditDialog(true);
|
||||
};
|
||||
|
||||
const handleEditSubmit = async () => {
|
||||
if (!selectedUser) return;
|
||||
|
||||
if (!formUsername.trim()) {
|
||||
showToast('Benutzername ist erforderlich', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
if (formUsername.trim().length < 3) {
|
||||
showToast('Benutzername muss mindestens 3 Zeichen lang sein', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
|
||||
setFormBusy(true);
|
||||
try {
|
||||
await updateUser(selectedUser.id, {
|
||||
username: formUsername.trim(),
|
||||
role: formRole,
|
||||
isActive: formIsActive,
|
||||
});
|
||||
showToast('Benutzer erfolgreich aktualisiert', 'e-toast-success');
|
||||
setShowEditDialog(false);
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Benutzers';
|
||||
showToast(message, 'e-toast-danger');
|
||||
} finally {
|
||||
setFormBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Reset password
|
||||
const handlePasswordClick = (userData: UserData) => {
|
||||
if (currentUser && userData.id === currentUser.id) {
|
||||
showToast('Bitte ändern Sie Ihr eigenes Passwort über das Benutzer-Menü (oben rechts).', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
setSelectedUser(userData);
|
||||
setFormPassword('');
|
||||
setShowPasswordDialog(true);
|
||||
};
|
||||
|
||||
const handlePasswordSubmit = async () => {
|
||||
if (!selectedUser) return;
|
||||
|
||||
if (!formPassword) {
|
||||
showToast('Passwort ist erforderlich', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
if (formPassword.length < 6) {
|
||||
showToast('Passwort muss mindestens 6 Zeichen lang sein', 'e-toast-warning');
|
||||
return;
|
||||
}
|
||||
|
||||
setFormBusy(true);
|
||||
try {
|
||||
await resetUserPassword(selectedUser.id, formPassword);
|
||||
showToast('Passwort erfolgreich zurückgesetzt', 'e-toast-success');
|
||||
setShowPasswordDialog(false);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Fehler beim Zurücksetzen des Passworts';
|
||||
showToast(message, 'e-toast-danger');
|
||||
} finally {
|
||||
setFormBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete user
|
||||
const handleDeleteClick = (userData: UserData) => {
|
||||
setSelectedUser(userData);
|
||||
setShowDeleteDialog(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!selectedUser) return;
|
||||
|
||||
setFormBusy(true);
|
||||
try {
|
||||
await deleteUser(selectedUser.id);
|
||||
showToast('Benutzer erfolgreich gelöscht', 'e-toast-success');
|
||||
setShowDeleteDialog(false);
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Fehler beim Löschen des Benutzers';
|
||||
showToast(message, 'e-toast-danger');
|
||||
} finally {
|
||||
setFormBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// View details
|
||||
const handleDetailsClick = (userData: UserData) => {
|
||||
setSelectedUser(userData);
|
||||
setShowDetailsDialog(true);
|
||||
};
|
||||
|
||||
// Format date-time
|
||||
const getRoleBadge = (role: string) => {
|
||||
const roleMap: Record<string, { text: string; color: string }> = {
|
||||
user: { text: 'Benutzer', color: '#6c757d' },
|
||||
editor: { text: 'Editor', color: '#0d6efd' },
|
||||
admin: { text: 'Admin', color: '#198754' },
|
||||
superadmin: { text: 'Superadmin', color: '#dc3545' },
|
||||
};
|
||||
const info = roleMap[role] || { text: role, color: '#6c757d' };
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: info.color,
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
{info.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Status badge
|
||||
const getStatusBadge = (isActive: boolean) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: isActive ? '#28a745' : '#dc3545',
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
{isActive ? 'Aktiv' : 'Inaktiv'}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Grid commands - no longer needed with custom template
|
||||
// const commands: CommandModel[] = [...]
|
||||
|
||||
// Command click handler removed - using custom button template instead
|
||||
|
||||
// Format dates
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<div style={{ textAlign: 'center', padding: 40 }}>Lade Benutzer...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<ToastComponent ref={toastRef} position={{ X: 'Right', Y: 'Top' }} />
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: 24, fontWeight: 600 }}>Benutzerverwaltung</h2>
|
||||
<p style={{ margin: '8px 0 0 0', color: '#6c757d' }}>
|
||||
Verwalten Sie Benutzer und deren Rollen
|
||||
</p>
|
||||
</div>
|
||||
<ButtonComponent
|
||||
cssClass="e-success"
|
||||
iconCss="e-icons e-plus"
|
||||
onClick={handleCreateClick}
|
||||
>
|
||||
Neuer Benutzer
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div style={{ marginBottom: 24, display: 'flex', gap: 16 }}>
|
||||
<div className="e-card" style={{ flex: 1, padding: 16 }}>
|
||||
<div style={{ fontSize: 14, color: '#6c757d', marginBottom: 4 }}>Gesamt</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 600 }}>{users.length}</div>
|
||||
</div>
|
||||
<div className="e-card" style={{ flex: 1, padding: 16 }}>
|
||||
<div style={{ fontSize: 14, color: '#6c757d', marginBottom: 4 }}>Aktiv</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 600, color: '#28a745' }}>
|
||||
{users.filter(u => u.isActive).length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="e-card" style={{ flex: 1, padding: 16 }}>
|
||||
<div style={{ fontSize: 14, color: '#6c757d', marginBottom: 4 }}>Inaktiv</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 600, color: '#dc3545' }}>
|
||||
{users.filter(u => !u.isActive).length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Users Grid */}
|
||||
<GridComponent
|
||||
dataSource={users}
|
||||
allowPaging={true}
|
||||
allowSorting={true}
|
||||
pageSettings={{ pageSize: 20, pageSizes: [10, 20, 50, 100] }}
|
||||
height="600"
|
||||
>
|
||||
<ColumnsDirective>
|
||||
<ColumnDirective field="id" headerText="ID" width="80" textAlign="Center" allowSorting={true} />
|
||||
<ColumnDirective
|
||||
field="username"
|
||||
headerText="Benutzername"
|
||||
width="200"
|
||||
allowSorting={true}
|
||||
/>
|
||||
<ColumnDirective
|
||||
field="role"
|
||||
headerText="Rolle"
|
||||
width="150"
|
||||
allowSorting={true}
|
||||
template={(props: UserData) => getRoleBadge(props.role)}
|
||||
/>
|
||||
<ColumnDirective
|
||||
field="isActive"
|
||||
headerText="Status"
|
||||
width="120"
|
||||
template={(props: UserData) => getStatusBadge(props.isActive)}
|
||||
/>
|
||||
<ColumnDirective
|
||||
field="createdAt"
|
||||
headerText="Erstellt"
|
||||
width="180"
|
||||
template={(props: UserData) => formatDate(props.createdAt)}
|
||||
/>
|
||||
<ColumnDirective
|
||||
headerText="Aktionen"
|
||||
width="280"
|
||||
template={(props: UserData) => (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<ButtonComponent
|
||||
cssClass="e-flat"
|
||||
onClick={() => handleDetailsClick(props)}
|
||||
>
|
||||
Details
|
||||
</ButtonComponent>
|
||||
<ButtonComponent
|
||||
cssClass="e-flat e-primary"
|
||||
onClick={() => handleEditClick(props)}
|
||||
>
|
||||
Bearbeiten
|
||||
</ButtonComponent>
|
||||
<ButtonComponent
|
||||
cssClass="e-flat e-info"
|
||||
onClick={() => handlePasswordClick(props)}
|
||||
>
|
||||
Passwort
|
||||
</ButtonComponent>
|
||||
{isSuperadmin && currentUser?.id !== props.id && (
|
||||
<ButtonComponent
|
||||
cssClass="e-flat e-danger"
|
||||
onClick={() => handleDeleteClick(props)}
|
||||
>
|
||||
Löschen
|
||||
</ButtonComponent>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</ColumnsDirective>
|
||||
<Inject services={[Page, Toolbar, Edit, CommandColumn]} />
|
||||
</GridComponent>
|
||||
|
||||
{/* Create User Dialog */}
|
||||
<DialogComponent
|
||||
isModal={true}
|
||||
visible={showCreateDialog}
|
||||
width="500px"
|
||||
header="Neuer Benutzer"
|
||||
showCloseIcon={true}
|
||||
close={() => setShowCreateDialog(false)}
|
||||
footerTemplate={() => (
|
||||
<div>
|
||||
<ButtonComponent
|
||||
cssClass="e-flat"
|
||||
onClick={() => setShowCreateDialog(false)}
|
||||
disabled={formBusy}
|
||||
>
|
||||
Abbrechen
|
||||
</ButtonComponent>
|
||||
<ButtonComponent
|
||||
cssClass="e-primary"
|
||||
onClick={handleCreateSubmit}
|
||||
disabled={formBusy}
|
||||
>
|
||||
{formBusy ? 'Erstelle...' : 'Erstellen'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div style={{ padding: 16 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||
Benutzername *
|
||||
</label>
|
||||
<TextBoxComponent
|
||||
placeholder="Benutzername eingeben"
|
||||
value={formUsername}
|
||||
input={(e: any) => setFormUsername(e.value)}
|
||||
disabled={formBusy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||
Passwort *
|
||||
</label>
|
||||
<TextBoxComponent
|
||||
type="password"
|
||||
placeholder="Mindestens 6 Zeichen"
|
||||
value={formPassword}
|
||||
input={(e: any) => setFormPassword(e.value)}
|
||||
disabled={formBusy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||
Rolle *
|
||||
</label>
|
||||
<DropDownListComponent
|
||||
dataSource={availableRoles}
|
||||
fields={{ value: 'value', text: 'text' }}
|
||||
value={formRole}
|
||||
change={(e: any) => setFormRole(e.value)}
|
||||
disabled={formBusy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<CheckBoxComponent
|
||||
label="Benutzer ist aktiv"
|
||||
checked={formIsActive}
|
||||
change={(e: any) => setFormIsActive(e.checked)}
|
||||
disabled={formBusy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
|
||||
{/* Edit User Dialog */}
|
||||
<DialogComponent
|
||||
isModal={true}
|
||||
visible={showEditDialog}
|
||||
width="500px"
|
||||
header={`Benutzer bearbeiten: ${selectedUser?.username}`}
|
||||
showCloseIcon={true}
|
||||
close={() => setShowEditDialog(false)}
|
||||
footerTemplate={() => (
|
||||
<div>
|
||||
<ButtonComponent
|
||||
cssClass="e-flat"
|
||||
onClick={() => setShowEditDialog(false)}
|
||||
disabled={formBusy}
|
||||
>
|
||||
Abbrechen
|
||||
</ButtonComponent>
|
||||
<ButtonComponent
|
||||
cssClass="e-primary"
|
||||
onClick={handleEditSubmit}
|
||||
disabled={formBusy}
|
||||
>
|
||||
{formBusy ? 'Speichere...' : 'Speichern'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div style={{ padding: 16 }}>
|
||||
{selectedUser?.id === currentUser?.id && (
|
||||
<div
|
||||
style={{
|
||||
padding: 12,
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffc107',
|
||||
borderRadius: 4,
|
||||
marginBottom: 16,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
⚠️ Sie bearbeiten Ihr eigenes Konto. Sie können Ihre eigene Rolle oder Ihren aktiven Status nicht ändern.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||
Benutzername *
|
||||
</label>
|
||||
<TextBoxComponent
|
||||
placeholder="Benutzername eingeben"
|
||||
value={formUsername}
|
||||
input={(e: any) => setFormUsername(e.value)}
|
||||
disabled={formBusy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||
Rolle *
|
||||
</label>
|
||||
<DropDownListComponent
|
||||
dataSource={availableRoles}
|
||||
fields={{ value: 'value', text: 'text' }}
|
||||
value={formRole}
|
||||
change={(e: any) => setFormRole(e.value)}
|
||||
disabled={formBusy || selectedUser?.id === currentUser?.id}
|
||||
/>
|
||||
{selectedUser?.id === currentUser?.id && (
|
||||
<div style={{ fontSize: 12, color: '#6c757d', marginTop: 4 }}>
|
||||
Sie können Ihre eigene Rolle nicht ändern
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<CheckBoxComponent
|
||||
label="Benutzer ist aktiv"
|
||||
checked={formIsActive}
|
||||
change={(e: any) => setFormIsActive(e.checked)}
|
||||
disabled={formBusy || selectedUser?.id === currentUser?.id}
|
||||
/>
|
||||
{selectedUser?.id === currentUser?.id && (
|
||||
<div style={{ fontSize: 12, color: '#6c757d', marginTop: 4 }}>
|
||||
Sie können Ihr eigenes Konto nicht deaktivieren
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
|
||||
{/* Reset Password Dialog */}
|
||||
<DialogComponent
|
||||
isModal={true}
|
||||
visible={showPasswordDialog}
|
||||
width="500px"
|
||||
header={`Passwort zurücksetzen: ${selectedUser?.username}`}
|
||||
showCloseIcon={true}
|
||||
close={() => setShowPasswordDialog(false)}
|
||||
footerTemplate={() => (
|
||||
<div>
|
||||
<ButtonComponent
|
||||
cssClass="e-flat"
|
||||
onClick={() => setShowPasswordDialog(false)}
|
||||
disabled={formBusy}
|
||||
>
|
||||
Abbrechen
|
||||
</ButtonComponent>
|
||||
<ButtonComponent
|
||||
cssClass="e-warning"
|
||||
onClick={handlePasswordSubmit}
|
||||
disabled={formBusy}
|
||||
>
|
||||
{formBusy ? 'Setze zurück...' : 'Zurücksetzen'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div style={{ padding: 16 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||||
Neues Passwort *
|
||||
</label>
|
||||
<TextBoxComponent
|
||||
type="password"
|
||||
placeholder="Mindestens 6 Zeichen"
|
||||
value={formPassword}
|
||||
input={(e: any) => setFormPassword(e.value)}
|
||||
disabled={formBusy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: 12,
|
||||
backgroundColor: '#d1ecf1',
|
||||
border: '1px solid #bee5eb',
|
||||
borderRadius: 4,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
💡 Das neue Passwort wird sofort wirksam. Informieren Sie den Benutzer über das neue Passwort.
|
||||
</div>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
|
||||
{/* Delete User Dialog */}
|
||||
<DialogComponent
|
||||
isModal={true}
|
||||
visible={showDeleteDialog}
|
||||
width="500px"
|
||||
header="Benutzer löschen"
|
||||
showCloseIcon={true}
|
||||
close={() => setShowDeleteDialog(false)}
|
||||
footerTemplate={() => (
|
||||
<div>
|
||||
<ButtonComponent
|
||||
cssClass="e-flat"
|
||||
onClick={() => setShowDeleteDialog(false)}
|
||||
disabled={formBusy}
|
||||
>
|
||||
Abbrechen
|
||||
</ButtonComponent>
|
||||
<ButtonComponent
|
||||
cssClass="e-danger"
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={formBusy}
|
||||
>
|
||||
{formBusy ? 'Lösche...' : 'Endgültig löschen'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div style={{ padding: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
backgroundColor: '#f8d7da',
|
||||
border: '1px solid #f5c6cb',
|
||||
borderRadius: 4,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<strong>⚠️ Warnung: Diese Aktion kann nicht rückgängig gemacht werden!</strong>
|
||||
</div>
|
||||
|
||||
<p style={{ marginBottom: 16 }}>
|
||||
Möchten Sie den Benutzer <strong>{selectedUser?.username}</strong> wirklich endgültig löschen?
|
||||
</p>
|
||||
|
||||
<p style={{ margin: 0, fontSize: 14, color: '#6c757d' }}>
|
||||
Tipp: Statt zu löschen, können Sie den Benutzer auch deaktivieren, um das Konto zu sperren und
|
||||
gleichzeitig die Daten zu bewahren.
|
||||
</p>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
|
||||
{/* Details Dialog */}
|
||||
<DialogComponent
|
||||
isModal={true}
|
||||
visible={showDetailsDialog}
|
||||
width="600px"
|
||||
header={`Details: ${selectedUser?.username}`}
|
||||
showCloseIcon={true}
|
||||
close={() => setShowDetailsDialog(false)}
|
||||
footerTemplate={() => (
|
||||
<div>
|
||||
<ButtonComponent cssClass="e-flat" onClick={() => setShowDetailsDialog(false)}>
|
||||
Schließen
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
{/* Account Info */}
|
||||
<div>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: 14, fontWeight: 600, color: '#6c757d' }}>
|
||||
Kontoinformation
|
||||
</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#6c757d', marginBottom: 4 }}>Benutzer-ID</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{selectedUser?.id}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#6c757d', marginBottom: 4 }}>Benutzername</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>{selectedUser?.username}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#6c757d', marginBottom: 4 }}>Rolle</div>
|
||||
<div>{selectedUser ? getRoleBadge(selectedUser.role) : '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#6c757d', marginBottom: 4 }}>Status</div>
|
||||
<div>{selectedUser ? getStatusBadge(selectedUser.isActive) : '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security & Activity */}
|
||||
<div>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: 14, fontWeight: 600, color: '#6c757d' }}>
|
||||
Sicherheit & Aktivität
|
||||
</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 8 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: '#333' }}>Letzter Login:</div>
|
||||
<div style={{ fontSize: 13, color: '#666' }}>
|
||||
{selectedUser?.lastLoginAt ? formatDate(selectedUser.lastLoginAt) : 'Nie'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 8 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: '#333' }}>Passwort geändert:</div>
|
||||
<div style={{ fontSize: 13, color: '#666' }}>
|
||||
{selectedUser?.lastPasswordChangeAt ? formatDate(selectedUser.lastPasswordChangeAt) : 'Nie'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 8 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: '#333' }}>Fehlgeschlagene Logins:</div>
|
||||
<div style={{ fontSize: 13, color: '#666' }}>
|
||||
{selectedUser?.failedLoginAttempts || 0}
|
||||
</div>
|
||||
</div>
|
||||
{selectedUser?.lastFailedLoginAt && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 8 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: '#333' }}>Letzter Fehler:</div>
|
||||
<div style={{ fontSize: 13, color: '#666' }}>
|
||||
{formatDate(selectedUser.lastFailedLoginAt)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Deactivation Info (if applicable) */}
|
||||
{selectedUser && !selectedUser.isActive && selectedUser.deactivatedAt && (
|
||||
<div style={{ padding: 12, backgroundColor: '#fff3cd', border: '1px solid #ffc107', borderRadius: 4 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 4 }}>Konto deaktiviert</div>
|
||||
<div style={{ fontSize: 12, color: '#856404' }}>
|
||||
am {formatDate(selectedUser.deactivatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamps */}
|
||||
<div>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: 14, fontWeight: 600, color: '#6c757d' }}>
|
||||
Zeitleisten
|
||||
</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 8 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: '#333' }}>Erstellt:</div>
|
||||
<div style={{ fontSize: 13, color: '#666' }}>
|
||||
{selectedUser?.createdAt ? formatDate(selectedUser.createdAt) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 8 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: '#333' }}>Zuletzt geändert:</div>
|
||||
<div style={{ fontSize: 13, color: '#666' }}>
|
||||
{selectedUser?.updatedAt ? formatDate(selectedUser.updatedAt) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Benutzer;
|
||||
|
||||
Reference in New Issue
Block a user