Dateien nach "frontend/src" hochladen
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user