Files
AdsPreview/frontend/src/components/UserManagement.js
Johannes b4758b4f26 security: clean repository without media files and sensitive data
- Removed area/ directory with 816MB of media files
- Removed sensitive FTP credentials from Git history
- Implemented .env.upload system for secure deployments
- Added comprehensive .gitignore for future protection

This commit represents a clean slate with all sensitive data removed.
2025-09-07 11:05:29 +02:00

405 lines
14 KiB
JavaScript

import React, { useEffect, useState } from 'react';
import { Table, Button, Spin, Alert, Modal, Form, Input, Select, App, Tooltip } from 'antd';
import { EditPencil, Trash } from 'iconoir-react';
import { getAll, create, update, remove } from '../services/entityService';
const { Option } = Select;
export default function UserManagement() {
const [users, setUsers] = useState([]);
const [clients, setClients] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingUser, setEditingUser] = useState(null);
const [modal, modalContextHolder] = Modal.useModal();
const { message } = App.useApp();
useEffect(() => {
fetchUsers();
fetchClients();
}, []);
function fetchUsers() {
setLoading(true);
getAll('users').then(res => {
if (res.success && Array.isArray(res.users)) {
setUsers(res.users);
} else {
setError(res.error?.message || 'Fehler beim Laden der Benutzer');
}
}).catch(() => setError('Serverfehler')).finally(() => setLoading(false));
}
function fetchClients() {
getAll('clients').then(res => {
if (res.success && res.clients) {
// Extrahiere Client-Verzeichnisnamen (dir) aus der Response
const clientDirs = Object.values(res.clients).map(client => client.dir);
// Entferne Duplikate falls vorhanden
const uniqueClientDirs = [...new Set(clientDirs)];
setClients(uniqueClientDirs);
}
}).catch(err => {
console.warn('Fehler beim Laden der Clients:', err);
setClients([]);
});
}
async function handleCreateUser(values) {
setSaving(true);
try {
// Stelle sicher, dass disallowedClients als Array gespeichert wird
if (values.disallowedClients && !Array.isArray(values.disallowedClients)) {
values.disallowedClients = [];
}
const data = await create('users', values);
if (data.success) {
message.success('Benutzer angelegt');
setModalOpen(false);
form.resetFields();
fetchUsers();
} else {
message.error(data.error?.message || 'Fehler beim Anlegen');
}
} catch (e) {
message.error('Netzwerkfehler');
} finally {
setSaving(false);
}
}
function handleDeleteUser(user) {
modal.confirm({
title: `Benutzer wirklich löschen?`,
content: `Soll der Benutzer "${user.username}" wirklich gelöscht werden?`,
okText: 'Löschen',
okType: 'danger',
cancelText: 'Abbrechen',
onOk: async () => {
const data = await remove('users', user.id);
if (data.success) {
message.success('Benutzer gelöscht');
fetchUsers();
} else {
message.error(data.error?.message || 'Fehler beim Löschen');
}
}
});
}
function openEditModal(user) {
setEditingUser(user);
setEditModalOpen(true);
}
async function handleEditUser(values) {
setSaving(true);
try {
// Separater API-Call für Passwort-Update falls angegeben
if (values.password) {
const token = localStorage.getItem('jwt');
const passwordResponse = await fetch(`/api/admin/users/${editingUser.id}/password`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ password: values.password })
});
const passwordResult = await passwordResponse.json();
if (!passwordResult.success) {
message.error(passwordResult.error?.message || 'Fehler beim Ändern des Passworts');
setSaving(false);
return;
}
}
// Standard User-Daten aktualisieren (ohne Passwort)
const { password, confirmPassword, ...userData } = values;
// Stelle sicher, dass disallowedClients als Array gespeichert wird
if (userData.disallowedClients && !Array.isArray(userData.disallowedClients)) {
userData.disallowedClients = [];
}
const data = await update('users', editingUser.id, userData);
if (data.success) {
message.success('Benutzer erfolgreich aktualisiert' + (values.password ? ' (inklusive Passwort)' : ''));
setEditModalOpen(false);
fetchUsers();
} else {
message.error(data.error?.message || 'Fehler beim Bearbeiten');
}
} catch (e) {
message.error('Netzwerkfehler');
} finally {
setSaving(false);
}
}
const columns = [
{ title: 'Benutzername', dataIndex: 'username', key: 'username' },
{ title: 'Rolle', dataIndex: 'role', key: 'role' },
{ title: 'E-Mail', dataIndex: 'email', key: 'email' },
{
title: 'Gesperrte Clients',
key: 'disallowedClients',
render: (_, record) => {
if (record.role !== 'admin' || !record.disallowedClients || record.disallowedClients.length === 0) {
return <span style={{ color: '#888', fontStyle: 'italic' }}>Keine</span>;
}
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{record.disallowedClients.map(client => (
<span
key={client}
style={{
background: '#ff4d4f',
color: 'white',
padding: '2px 8px',
borderRadius: 12,
fontSize: 12,
fontWeight: 500
}}
>
{client}
</span>
))}
</div>
);
}
},
{
title: 'Aktionen',
key: 'actions',
render: (_, record) => (
<span style={{ display: 'flex', gap: '8px' }}>
<Tooltip title="Benutzer bearbeiten">
<Button
type="text"
size="small"
icon={<EditPencil />}
onClick={() => openEditModal(record)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px'
}}
/>
</Tooltip>
<Tooltip title="Benutzer löschen">
<Button
type="text"
size="small"
icon={<Trash />}
onClick={() => handleDeleteUser(record)}
danger
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px'
}}
/>
</Tooltip>
</span>
)
}
];
if (loading) {
return <Spin tip="Lade Benutzer..." fullscreen />;
}
if (error) {
return <Alert type="error" message={error} showIcon />;
}
return (
<div>
{modalContextHolder}
<Button type="primary" style={{ marginBottom: 16 }} onClick={() => setModalOpen(true)}>
Neuen Benutzer anlegen
</Button>
<Table columns={columns} dataSource={users} rowKey="id" />
<Modal
title="Neuen Benutzer anlegen"
open={modalOpen}
onCancel={() => {
setModalOpen(false);
form.resetFields();
}}
footer={null}
destroyOnHidden={true}
>
<Form form={form} layout="vertical" onFinish={handleCreateUser}>
<Form.Item name="username" label="Benutzername" rules={[{ required: true, message: 'Bitte Benutzernamen angeben' }]}>
<Input autoFocus />
</Form.Item>
<Form.Item name="email" label="E-Mail" rules={[{ type: 'email', message: 'Ungültige E-Mail' }]}>
<Input />
</Form.Item>
<Form.Item name="role" label="Rolle" rules={[{ required: true, message: 'Bitte Rolle wählen' }]}>
<Select placeholder="Rolle wählen">
<Option value="admin">Admin</Option>
<Option value="client">Client</Option>
</Select>
</Form.Item>
{/* Client-Berechtigungen nur für Admins anzeigen */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.role !== currentValues.role}
>
{({ getFieldValue }) =>
getFieldValue('role') === 'admin' ? (
<Form.Item
name="disallowedClients"
label="Gesperrte Clients"
tooltip="Wählen Sie die Clients aus, auf die dieser Administrator keinen Zugriff haben soll"
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
placeholder="Clients auswählen, die gesperrt werden sollen"
options={clients.map(client => ({
label: client,
value: client
}))}
/>
</Form.Item>
) : null
}
</Form.Item>
<Form.Item
name="password"
label="Passwort"
rules={[
{ min: 6, message: 'Passwort muss mindestens 6 Zeichen lang sein' }
]}
>
<Input.Password placeholder="Passwort eingeben (optional)" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={saving} block>
Anlegen
</Button>
</Form.Item>
</Form>
</Modal>
<Modal
title="Benutzer bearbeiten"
open={editModalOpen}
onCancel={() => setEditModalOpen(false)}
footer={null}
destroyOnHidden={true}
>
<Form
key={editingUser?.id}
initialValues={{
username: editingUser?.username,
email: editingUser?.email,
role: editingUser?.role,
disallowedClients: editingUser?.disallowedClients || []
// password und confirmPassword werden bewusst nicht gesetzt, damit sie leer beginnen
}}
layout="vertical"
onFinish={handleEditUser}
>
<Form.Item name="username" label="Benutzername" rules={[{ required: true, message: 'Bitte Benutzernamen angeben' }]}>
<Input />
</Form.Item>
<Form.Item name="email" label="E-Mail" rules={[{ type: 'email', message: 'Ungültige E-Mail' }]}>
<Input />
</Form.Item>
<Form.Item name="role" label="Rolle" rules={[{ required: true, message: 'Bitte Rolle wählen' }]}>
<Select placeholder="Rolle wählen">
<Option value="admin">Admin</Option>
<Option value="client">Client</Option>
</Select>
</Form.Item>
{/* Client-Berechtigungen nur für Admins anzeigen */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.role !== currentValues.role}
>
{({ getFieldValue }) =>
getFieldValue('role') === 'admin' ? (
<Form.Item
name="disallowedClients"
label="Gesperrte Clients"
tooltip="Wählen Sie die Clients aus, auf die dieser Administrator keinen Zugriff haben soll"
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
placeholder="Clients auswählen, die gesperrt werden sollen"
options={clients.map(client => ({
label: client,
value: client
}))}
/>
</Form.Item>
) : null
}
</Form.Item>
<div style={{ marginTop: 24, marginBottom: 16, borderTop: '1px solid #f0f0f0', paddingTop: 16 }}>
<h4>Passwort ändern (optional)</h4>
</div>
<Form.Item
name="password"
label="Neues Passwort"
rules={[
{ min: 6, message: 'Passwort muss mindestens 6 Zeichen lang sein' }
]}
>
<Input.Password placeholder="Neues Passwort eingeben (leer lassen für keine Änderung)" />
</Form.Item>
<Form.Item
name="confirmPassword"
label="Passwort bestätigen"
dependencies={['password']}
rules={[
({ getFieldValue }) => ({
validator(_, value) {
const password = getFieldValue('password');
if (!password && !value) {
return Promise.resolve();
}
if (password && !value) {
return Promise.reject(new Error('Bitte Passwort bestätigen'));
}
if (password && value && password !== value) {
return Promise.reject(new Error('Passwörter stimmen nicht überein'));
}
return Promise.resolve();
}
})
]}
>
<Input.Password placeholder="Passwort wiederholen" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={saving} block>
Benutzer speichern
</Button>
</Form.Item>
</Form>
</Modal>
</div>
);
}