- 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.
405 lines
14 KiB
JavaScript
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>
|
|
);
|
|
} |