push newest version

This commit is contained in:
2025-11-20 17:59:34 +01:00
parent c0e2df2430
commit 97ccd7bfbf
21 changed files with 3978 additions and 65 deletions

View File

@@ -0,0 +1,385 @@
import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext'
const Users = () => {
const { authFetch } = useAuth()
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [showForm, setShowForm] = useState(false)
const [editingUser, setEditingUser] = useState(null)
const [formData, setFormData] = useState({
username: '',
email: '',
oldPassword: '',
password: '',
confirmPassword: ''
})
useEffect(() => {
fetchUsers()
}, [])
const fetchUsers = async () => {
try {
setError('')
const response = await authFetch('/api/users')
if (response.ok) {
const data = await response.json()
setUsers(Array.isArray(data) ? data : [])
} else {
setError('Fehler beim Abrufen der Benutzer')
}
} catch (err) {
setError('Fehler beim Abrufen der Benutzer')
console.error('Error fetching users:', err)
}
}
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
setLoading(true)
// Validierung: Passwort-Bestätigung muss übereinstimmen
if (formData.password && formData.password !== formData.confirmPassword) {
setError('Die Passwörter stimmen nicht überein')
setLoading(false)
return
}
// Validierung: Wenn Passwort geändert wird, muss altes Passwort vorhanden sein
if (editingUser && formData.password && !formData.oldPassword) {
setError('Bitte geben Sie das alte Passwort ein, um das Passwort zu ändern')
setLoading(false)
return
}
try {
const url = editingUser
? `/api/users/${editingUser.id}`
: '/api/users'
const method = editingUser ? 'PUT' : 'POST'
const body = editingUser
? {
...(formData.username && { username: formData.username }),
...(formData.email && { email: formData.email }),
...(formData.password && {
password: formData.password,
oldPassword: formData.oldPassword
})
}
: {
username: formData.username,
email: formData.email,
password: formData.password
}
const response = await authFetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (response.ok) {
await fetchUsers()
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
setShowForm(false)
setEditingUser(null)
} else {
const errorData = await response.json()
setError(errorData.error || 'Fehler beim Speichern des Benutzers')
}
} catch (err) {
setError('Fehler beim Speichern des Benutzers')
console.error('Error saving user:', err)
} finally {
setLoading(false)
}
}
const handleEdit = (user) => {
setEditingUser(user)
setFormData({
username: user.username,
email: user.email,
oldPassword: '',
password: '',
confirmPassword: ''
})
setShowForm(true)
}
const handleDelete = async (userId) => {
if (!window.confirm('Möchten Sie diesen Benutzer wirklich löschen?')) {
return
}
try {
const response = await authFetch(`/api/users/${userId}`, {
method: 'DELETE',
})
if (response.ok) {
await fetchUsers()
} else {
const errorData = await response.json()
alert(errorData.error || 'Fehler beim Löschen des Benutzers')
}
} catch (err) {
alert('Fehler beim Löschen des Benutzers')
console.error('Error deleting user:', err)
}
}
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
return (
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
<div className="max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-4xl font-bold text-white mb-2">Benutzerverwaltung</h1>
<p className="text-lg text-slate-200">
Verwalten Sie lokale Benutzer und deren Zugangsdaten.
</p>
</div>
<button
onClick={() => {
setShowForm(!showForm)
setEditingUser(null)
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
}}
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200"
>
{showForm ? 'Abbrechen' : '+ Neuer Benutzer'}
</button>
</div>
{/* Create/Edit User Form */}
{showForm && (
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6 mb-6">
<h2 className="text-2xl font-semibold text-white mb-4">
{editingUser ? 'Benutzer bearbeiten' : 'Neuen Benutzer erstellen'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-slate-200 mb-2">
Benutzername {!editingUser && '*'}
</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
required={!editingUser}
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Geben Sie einen Benutzernamen ein"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2">
E-Mail {!editingUser && '*'}
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required={!editingUser}
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Geben Sie eine E-Mail-Adresse ein"
/>
</div>
{editingUser && (
<div>
<label htmlFor="oldPassword" className="block text-sm font-medium text-slate-200 mb-2">
Altes Passwort {formData.password && '*'}
</label>
<input
type="password"
id="oldPassword"
name="oldPassword"
value={formData.oldPassword}
onChange={handleChange}
required={!!formData.password}
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Geben Sie Ihr aktuelles Passwort ein"
/>
</div>
)}
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-200 mb-2">
{editingUser ? 'Neues Passwort' : 'Passwort'} {!editingUser && '*'}
{editingUser && <span className="text-xs text-slate-400 ml-2">(leer lassen, um nicht zu ändern)</span>}
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required={!editingUser}
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder={editingUser ? "Geben Sie ein neues Passwort ein" : "Geben Sie ein Passwort ein"}
/>
{/* Passwortrichtlinie - nur anzeigen wenn Passwort eingegeben wird */}
{(formData.password || !editingUser) && (
<div className="mt-2 p-3 bg-slate-700/30 border border-slate-600/50 rounded-lg">
<p className="text-xs font-semibold text-slate-300 mb-2">Passwortrichtlinie:</p>
<ul className="text-xs text-slate-400 space-y-1">
<li className={`flex items-center gap-2 ${formData.password.length >= 8 ? 'text-green-400' : ''}`}>
{formData.password.length >= 8 ? '✓' : '○'} Mindestens 8 Zeichen
</li>
<li className={`flex items-center gap-2 ${/[A-Z]/.test(formData.password) ? 'text-green-400' : ''}`}>
{/[A-Z]/.test(formData.password) ? '✓' : '○'} Mindestens ein Großbuchstabe
</li>
<li className={`flex items-center gap-2 ${/[a-z]/.test(formData.password) ? 'text-green-400' : ''}`}>
{/[a-z]/.test(formData.password) ? '✓' : '○'} Mindestens ein Kleinbuchstabe
</li>
<li className={`flex items-center gap-2 ${/[0-9]/.test(formData.password) ? 'text-green-400' : ''}`}>
{/[0-9]/.test(formData.password) ? '✓' : '○'} Mindestens eine Zahl
</li>
<li className={`flex items-center gap-2 ${/[^A-Za-z0-9]/.test(formData.password) ? 'text-green-400' : ''}`}>
{/[^A-Za-z0-9]/.test(formData.password) ? '✓' : '○'} Mindestens ein Sonderzeichen
</li>
</ul>
</div>
)}
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-slate-200 mb-2">
{editingUser ? 'Neues Passwort bestätigen' : 'Passwort bestätigen'} {!editingUser && '*'}
</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required={!editingUser || !!formData.password}
className={`w-full px-4 py-2 bg-slate-700/50 border rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
formData.confirmPassword && formData.password !== formData.confirmPassword
? 'border-red-500'
: formData.confirmPassword && formData.password === formData.confirmPassword
? 'border-green-500'
: 'border-slate-600'
}`}
placeholder={editingUser ? "Bestätigen Sie das neue Passwort" : "Bestätigen Sie das Passwort"}
/>
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
<p className="mt-1 text-xs text-red-400">Die Passwörter stimmen nicht überein</p>
)}
{formData.confirmPassword && formData.password === formData.confirmPassword && formData.password && (
<p className="mt-1 text-xs text-green-400"> Passwörter stimmen überein</p>
)}
</div>
{error && (
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm">
{error}
</div>
)}
<div className="flex gap-3">
<button
type="submit"
disabled={loading}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors duration-200"
>
{loading ? 'Wird gespeichert...' : editingUser ? 'Aktualisieren' : 'Benutzer erstellen'}
</button>
<button
type="button"
onClick={() => {
setShowForm(false)
setEditingUser(null)
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
setError('')
}}
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
>
Abbrechen
</button>
</div>
</form>
</div>
)}
{/* Users List */}
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6">
<h2 className="text-2xl font-semibold text-white mb-4">
Benutzer
</h2>
{error && !showForm && (
<div className="mb-4 p-4 bg-red-500/20 border border-red-500/50 rounded-lg">
<p className="text-red-300 mb-2">{error}</p>
<button
onClick={fetchUsers}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
>
Erneut versuchen
</button>
</div>
)}
{users.length === 0 ? (
<p className="text-slate-300 text-center py-8">
Noch keine Benutzer vorhanden. Erstellen Sie Ihren ersten Benutzer!
</p>
) : (
<div className="space-y-4">
{users.map((user) => (
<div
key={user.id}
className="bg-slate-700/50 rounded-lg p-4 border border-slate-600/50 hover:border-slate-500 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-xl font-semibold text-white mb-2">
{user.username}
</h3>
<p className="text-slate-300 mb-2">{user.email}</p>
<p className="text-xs text-slate-400">
Erstellt: {user.createdAt ? new Date(user.createdAt).toLocaleString('de-DE') : 'Unbekannt'}
</p>
{user.id && (
<p className="text-xs text-slate-500 font-mono mt-1">
ID: {user.id}
</p>
)}
</div>
<div className="flex gap-2 ml-4">
<button
onClick={() => handleEdit(user)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors"
>
Bearbeiten
</button>
<button
onClick={() => handleDelete(user.id)}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors"
>
Löschen
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)
}
export default Users