Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-26 13:53:13 +02:00
parent bd6e656d73
commit a2f51f0d81

View File

@@ -701,6 +701,137 @@ const Sec = ({title, children}) => (
</div> </div>
); );
// ── Benutzerverwaltung ────────────────────────────────────────────────────────
function UserManagement({ toast }) {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [form, setForm] = useState({ username:'', password:'', role:'user' });
const [resetId, setResetId] = useState(null);
const [resetPw, setResetPw] = useState('');
const [delId, setDelId] = useState(null);
const { confirm, ConfirmDialog } = useConfirm();
const load = () => {
setLoading(true);
api('/admin/users').then(setUsers).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
};
useEffect(()=>{ load(); },[]);
const create = async () => {
if (!form.username.trim()||!form.password) { toast('Alle Felder ausfüllen','error'); return; }
try {
await api('/admin/users',{body:form});
toast(`Benutzer "${form.username}" angelegt`);
setForm({username:'',password:'',role:'user'}); load();
} catch(e) { toast(e.message,'error'); }
};
const del = async id => {
if (!await confirm('Benutzer und alle seine Daten wirklich löschen?')) return;
try { await api(`/admin/users/${id}`,{method:'DELETE'}); toast('Gelöscht'); load(); }
catch(e) { toast(e.message,'error'); }
};
const resetPassword = async () => {
if (!resetPw||resetPw.length<8) { toast('Mind. 8 Zeichen','error'); return; }
try {
await api(`/admin/users/${resetId}/reset-password`,{method:'PUT',body:{newPassword:resetPw}});
toast('Passwort zurückgesetzt'); setResetId(null); setResetPw('');
} catch(e) { toast(e.message,'error'); }
};
return (
<div>
<ConfirmDialog/>
{/* Neuen Benutzer anlegen */}
<Sec title="NEUEN BENUTZER ANLEGEN">
<Field label="BENUTZERNAME" value={form.username} onChange={e=>setForm(f=>({...f,username:e.target.value}))} autoCapitalize="none"/>
<Field label="PASSWORT (mind. 8 Zeichen)" type="password" value={form.password} onChange={e=>setForm(f=>({...f,password:e.target.value}))}/>
<div style={{marginBottom:12}}>
<label style={{...S.head,display:'block',marginBottom:4}}>ROLLE</label>
<select value={form.role} onChange={e=>setForm(f=>({...f,role:e.target.value}))} style={{...S.inp,fontSize:14}}>
<option value="user" style={{background:'#1a1a1e'}}>Benutzer</option>
<option value="admin" style={{background:'#1a1a1e'}}>Administrator</option>
</select>
</div>
<button onClick={create} style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'11px 0'}}>
+ Benutzer anlegen
</button>
</Sec>
{/* Passwort-Reset Modal */}
{resetId && (
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:5000,
display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
<div style={{...S.card,width:'100%',maxWidth:360}}>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:14,fontWeight:700,marginBottom:16}}>
Passwort zurücksetzen
</div>
<Field label="NEUES PASSWORT" type="password" value={resetPw} onChange={e=>setResetPw(e.target.value)}/>
<div style={{display:'flex',gap:8}}>
<button onClick={resetPassword} style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0'}}> Setzen</button>
<button onClick={()=>{setResetId(null);setResetPw('');}} style={{...S.btn('#ff6b9d'),flex:1,textAlign:'center',padding:'10px 0'}}>Abbrechen</button>
</div>
</div>
</div>
)}
{/* Benutzerliste */}
<Sec title="BENUTZER">
{loading && <div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>Lädt</div>}
{users.map(u => (
<div key={u.id} style={{
padding:'12px 0', borderBottom:'1px solid rgba(255,255,255,0.06)',
}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<div>
<span style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700}}>{u.username}</span>
<span style={{
marginLeft:8, fontSize:9, fontFamily:'monospace',
color: u.role==='admin'?'#ffe66d':'#4ecdc4',
background: u.role==='admin'?'rgba(255,230,109,0.1)':'rgba(78,205,196,0.1)',
border:`1px solid ${u.role==='admin'?'rgba(255,230,109,0.3)':'rgba(78,205,196,0.3)'}`,
borderRadius:4, padding:'1px 6px',
}}>{u.role}</span>
</div>
<div style={{display:'flex',gap:5}}>
<button onClick={()=>{setResetId(u.id);setResetPw('');}}
style={{...S.btn('#ffe66d',true),fontSize:10}}>🔑 PW</button>
{u.role !== 'admin' && (
<button onClick={()=>del(u.id)} style={{...S.btn('#ff6b9d',true)}}>
<TrashIcon size={13} color="#ff6b9d"/>
</button>
)}
</div>
</div>
{/* Statistik */}
<div style={{display:'flex',flexWrap:'wrap',gap:6}}>
{[
['◈ Archiv', u.archiv],
['📦 Bestellungen', u.bestellung],
['✓ Todos', u.todos],
['🔗 Links', u.links],
[`${u.noteLen} Zeichen`, null],
].map(([label, val]) => (
<span key={label} style={{
fontSize:10, fontFamily:'monospace',
color:'rgba(255,255,255,0.55)',
background:'rgba(255,255,255,0.04)',
border:'1px solid rgba(255,255,255,0.08)',
borderRadius:8, padding:'2px 8px',
}}>
{label}{val !== null ? `: ${val}` : ''}
</span>
))}
</div>
</div>
))}
</Sec>
</div>
);
}
// Field außerhalb definiert → Tastatur bleibt beim Tippen erhalten // Field außerhalb definiert → Tastatur bleibt beim Tippen erhalten
const Field = ({ label, type='text', value, onChange, placeholder='', autoCapitalize='off' }) => ( const Field = ({ label, type='text', value, onChange, placeholder='', autoCapitalize='off' }) => (
<div style={{ marginBottom:12 }}> <div style={{ marginBottom:12 }}>
@@ -711,7 +842,7 @@ const Field = ({ label, type='text', value, onChange, placeholder='', autoCapita
</div> </div>
); );
function AdminPanel({ toast, mobile }) { function AdminPanel({ toast, mobile, user }) {
const [section, setSection] = useState('profil'); const [section, setSection] = useState('profil');
const [pw, setPw] = useState({ current:'', newPw:'', confirm:'' }); const [pw, setPw] = useState({ current:'', newPw:'', confirm:'' });
const [avatar, setAvatar] = useState(localStorage.getItem('dd_avatar') || null); const [avatar, setAvatar] = useState(localStorage.getItem('dd_avatar') || null);
@@ -765,8 +896,9 @@ function AdminPanel({ toast, mobile }) {
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:560 }}> <div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:560 }}>
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile?17:22, marginBottom:16 }}>Einstellungen</h1> <h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile?17:22, marginBottom:16 }}>Einstellungen</h1>
<div style={{ display:'flex', gap:6, marginBottom:18 }}> <div style={{ display:'flex', gap:6, marginBottom:18, flexWrap:'wrap' }}>
{[['profil','Profil'],['backup','Backup']].map(([k,l]) => ( {[['profil','Profil'], ...(user?.role==='admin'?[['benutzer','Benutzer'],['backup','Backup']]:[])]
.map(([k,l]) => (
<button key={k} onClick={()=>setSection(k)} style={{ <button key={k} onClick={()=>setSection(k)} style={{
padding:'6px 16px', borderRadius:20, fontFamily:'monospace', fontSize:12, cursor:'pointer', padding:'6px 16px', borderRadius:20, fontFamily:'monospace', fontSize:12, cursor:'pointer',
border:`1px solid ${section===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.15)'}`, border:`1px solid ${section===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.15)'}`,
@@ -812,13 +944,13 @@ function AdminPanel({ toast, mobile }) {
{avatar && <button onClick={()=>{setAvatar(null);localStorage.removeItem('dd_avatar');toast('Avatar entfernt');}} {avatar && <button onClick={()=>{setAvatar(null);localStorage.removeItem('dd_avatar');toast('Avatar entfernt');}}
style={{...S.btn('#ff6b9d',true)}}> Entfernen</button>} style={{...S.btn('#ff6b9d',true)}}> Entfernen</button>}
</Sec> </Sec>
<Sec title="BENUTZERNAME ÄNDERN"> {user?.role==='admin' && <Sec title="BENUTZERNAME ÄNDERN">
<Field label="NEUER BENUTZERNAME" value={un.newUsername} onChange={e=>setUn(p=>({...p,newUsername:e.target.value}))} /> <Field label="NEUER BENUTZERNAME" value={un.newUsername} onChange={e=>setUn(p=>({...p,newUsername:e.target.value}))} />
<Field label="PASSWORT ZUR BESTÄTIGUNG" type="password" value={un.unPw} onChange={e=>setUn(p=>({...p,unPw:e.target.value}))} /> <Field label="PASSWORT ZUR BESTÄTIGUNG" type="password" value={un.unPw} onChange={e=>setUn(p=>({...p,unPw:e.target.value}))} />
<button onClick={changeUn} disabled={busy.un} style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'11px 0' }}> <button onClick={changeUn} disabled={busy.un} style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'11px 0' }}>
{busy.un ? '…' : '✓ Benutzername ändern'} {busy.un ? '…' : '✓ Benutzername ändern'}
</button> </button>
</Sec> </Sec>}
<Sec title="PASSWORT ÄNDERN"> <Sec title="PASSWORT ÄNDERN">
<Field label="AKTUELLES PASSWORT" type="password" value={pw.current} onChange={e=>setPw(p=>({...p,current:e.target.value}))} /> <Field label="AKTUELLES PASSWORT" type="password" value={pw.current} onChange={e=>setPw(p=>({...p,current:e.target.value}))} />
<Field label="NEUES PASSWORT" type="password" value={pw.newPw} onChange={e=>setPw(p=>({...p,newPw:e.target.value}))} /> <Field label="NEUES PASSWORT" type="password" value={pw.newPw} onChange={e=>setPw(p=>({...p,newPw:e.target.value}))} />
@@ -830,6 +962,10 @@ function AdminPanel({ toast, mobile }) {
</div> </div>
)} )}
{section === 'benutzer' && user?.role==='admin' && (
<div><UserManagement toast={toast}/></div>
)}
{section === 'backup' && ( {section === 'backup' && (
<div> <div>
<Sec title="DATENBANK"> <Sec title="DATENBANK">
@@ -917,7 +1053,7 @@ export default function App() {
)} )}
<main style={mainStyle}> <main style={mainStyle}>
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile}/>} {active==='dashboard' && <Dashboard toast={toast} mobile={mobile}/>}
{active==='admin' && user?.role==='admin' && <AdminPanel toast={toast} mobile={mobile}/>} {active==='admin' && user?.role==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user}/>}
{activeTool && <activeTool.component toast={toast} mobile={mobile}/>} {activeTool && <activeTool.component toast={toast} mobile={mobile}/>}
</main> </main>
</div> </div>