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.
This commit is contained in:
405
frontend/src/components/UserManagement.js
Normal file
405
frontend/src/components/UserManagement.js
Normal file
@@ -0,0 +1,405 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user