push newest version
This commit is contained in:
385
frontend/src/pages/Users.jsx
Normal file
385
frontend/src/pages/Users.jsx
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user