fix: remove role gate from Settings, all strings in English

This commit is contained in:
Brückner
2026-06-03 16:08:05 +02:00
parent d364aea4c1
commit f7999cbe55
4 changed files with 31 additions and 44 deletions

View File

@ -156,7 +156,7 @@ async function startServer() {
app.get('/api/auth/azure', async (_req, res) => { app.get('/api/auth/azure', async (_req, res) => {
const msalClient = getMsalClient(); const msalClient = getMsalClient();
if (!msalClient) { if (!msalClient) {
return res.redirect('/?auth_error=Azure+Login+nicht+konfiguriert'); return res.redirect('/?auth_error=Azure+login+not+configured');
} }
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`; const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`; const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
@ -168,7 +168,7 @@ async function startServer() {
res.redirect(authCodeUrl); res.redirect(authCodeUrl);
} catch (err: any) { } catch (err: any) {
console.error('[Azure Auth] getAuthCodeUrl error:', err); console.error('[Azure Auth] getAuthCodeUrl error:', err);
res.redirect('/?auth_error=Microsoft+Login+konnte+nicht+gestartet+werden'); res.redirect('/?auth_error=Failed+to+start+Microsoft+login');
} }
}); });
@ -180,11 +180,11 @@ async function startServer() {
return res.redirect(`/?auth_error=${msg}`); return res.redirect(`/?auth_error=${msg}`);
} }
if (!code) { if (!code) {
return res.redirect('/?auth_error=Kein+Autorisierungscode+erhalten'); return res.redirect('/?auth_error=No+authorization+code+received');
} }
const msalClient = getMsalClient(); const msalClient = getMsalClient();
if (!msalClient) { if (!msalClient) {
return res.redirect('/?auth_error=Azure+Login+nicht+konfiguriert'); return res.redirect('/?auth_error=Azure+login+not+configured');
} }
const appUrl = process.env.APP_URL || `http://localhost:${PORT}`; const appUrl = process.env.APP_URL || `http://localhost:${PORT}`;
const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`; const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`;
@ -197,7 +197,7 @@ async function startServer() {
const email = (result.account?.username ?? '').toLowerCase(); const email = (result.account?.username ?? '').toLowerCase();
const name = result.account?.name || email; const name = result.account?.name || email;
if (!email) { if (!email) {
return res.redirect('/?auth_error=Keine+E-Mail+von+Microsoft+erhalten'); return res.redirect('/?auth_error=No+email+returned+by+Microsoft');
} }
let user = db.prepare('SELECT id, name, role, email FROM users WHERE email = ?').get(email) as User | undefined; let user = db.prepare('SELECT id, name, role, email FROM users WHERE email = ?').get(email) as User | undefined;
if (!user) { if (!user) {
@ -210,14 +210,14 @@ async function startServer() {
res.redirect(`/?token=${encodeURIComponent(token)}`); res.redirect(`/?token=${encodeURIComponent(token)}`);
} catch (err: any) { } catch (err: any) {
console.error('[Azure Auth] acquireTokenByCode error:', err); console.error('[Azure Auth] acquireTokenByCode error:', err);
res.redirect('/?auth_error=Authentifizierung+fehlgeschlagen'); res.redirect('/?auth_error=Authentication+failed');
} }
}); });
// ------------------------------------------------------------- // -------------------------------------------------------------
// RESTFUL API: Settings (admin only) // RESTFUL API: Settings (admin only)
// ------------------------------------------------------------- // -------------------------------------------------------------
app.get('/api/settings', requireAuth, requireAdmin, (_req, res) => { app.get('/api/settings', requireAuth, (_req, res) => {
try { try {
res.json(maskSettings(getAllSettings())); res.json(maskSettings(getAllSettings()));
} catch (err: any) { } catch (err: any) {
@ -225,7 +225,7 @@ async function startServer() {
} }
}); });
app.put('/api/settings', requireAuth, requireAdmin, (req, res) => { app.put('/api/settings', requireAuth, (req, res) => {
try { try {
const allowed = ['azure_enabled', 'azure_client_id', 'azure_tenant_id', 'azure_client_secret', const allowed = ['azure_enabled', 'azure_client_id', 'azure_tenant_id', 'azure_client_secret',
'azure_redirect_uri', 'checkmk_api_url', 'checkmk_api_secret', 'checkmk_sync_interval_ms']; 'azure_redirect_uri', 'checkmk_api_url', 'checkmk_api_secret', 'checkmk_sync_interval_ms'];

View File

@ -92,10 +92,10 @@ export default function App() {
saveSession(urlToken, user); saveSession(urlToken, user);
setCurrentUser(user); setCurrentUser(user);
} else { } else {
setOauthError('Anmeldung fehlgeschlagen. Bitte erneut versuchen.'); setOauthError('Login failed. Please try again.');
} }
} catch { } catch {
setOauthError('Server nicht erreichbar.'); setOauthError('Server unreachable. Please try again.');
} finally { } finally {
setAuthChecked(true); setAuthChecked(true);
} }
@ -371,8 +371,6 @@ export default function App() {
setActiveTab('devices'); setActiveTab('devices');
}; };
const isAdmin = currentUser?.role?.toLowerCase() === 'admin';
const navigationGroups: { label: string | null; items: { id: string; label: string; icon: React.ReactNode }[] }[] = [ const navigationGroups: { label: string | null; items: { id: string; label: string; icon: React.ReactNode }[] }[] = [
{ {
label: null, label: null,
@ -401,12 +399,12 @@ export default function App() {
{ id: 'logs', label: 'Logbook', icon: <History className="w-4 h-4 shrink-0" /> }, { id: 'logs', label: 'Logbook', icon: <History className="w-4 h-4 shrink-0" /> },
], ],
}, },
...(isAdmin ? [{ {
label: 'System', label: 'System',
items: [ items: [
{ id: 'settings', label: 'Einstellungen', icon: <Settings2 className="w-4 h-4 shrink-0" /> }, { id: 'settings', label: 'Settings', icon: <Settings2 className="w-4 h-4 shrink-0" /> },
], ],
}] : []), },
]; ];
// Startup check not done yet // Startup check not done yet

View File

@ -150,7 +150,7 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
<> <>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex-1 h-px bg-slate-800" /> <div className="flex-1 h-px bg-slate-800" />
<span className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">oder</span> <span className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">or</span>
<div className="flex-1 h-px bg-slate-800" /> <div className="flex-1 h-px bg-slate-800" />
</div> </div>
<button <button

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { authFetch } from '../lib/auth'; import { authFetch } from '../lib/auth';
import { User } from '../types'; import { User } from '../types';
import { import {
Shield, Activity, Save, CheckCircle, AlertCircle, Eye, EyeOff, Settings2, Lock, Shield, Activity, Save, CheckCircle, AlertCircle, Eye, EyeOff, Settings2,
} from 'lucide-react'; } from 'lucide-react';
const SECRET_SENTINEL = '__SET__'; const SECRET_SENTINEL = '__SET__';
@ -50,14 +50,13 @@ function TextInput({
} }
function SecretInput({ function SecretInput({
value, onChange, alreadySet, show, onToggleShow, placeholder, value, onChange, alreadySet, show, onToggleShow,
}: { }: {
value: string; value: string;
onChange: (v: string) => void; onChange: (v: string) => void;
alreadySet: boolean; alreadySet: boolean;
show: boolean; show: boolean;
onToggleShow: () => void; onToggleShow: () => void;
placeholder?: string;
}) { }) {
return ( return (
<div className="relative"> <div className="relative">
@ -65,7 +64,7 @@ function SecretInput({
type={show ? 'text' : 'password'} type={show ? 'text' : 'password'}
value={value} value={value}
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
placeholder={alreadySet ? 'Bereits konfiguriert leer lassen zum Behalten' : (placeholder ?? 'Neuen Wert eingeben')} placeholder={alreadySet ? 'Already configured leave blank to keep' : 'Enter value'}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 pr-10 text-sm text-white placeholder-slate-500 font-mono focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all" className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 pr-10 text-sm text-white placeholder-slate-500 font-mono focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
/> />
<button <button
@ -80,7 +79,7 @@ function SecretInput({
); );
} }
export default function Settings({ currentUser }: SettingsProps) { export default function Settings({ currentUser: _currentUser }: SettingsProps) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@ -109,7 +108,7 @@ export default function Settings({ currentUser }: SettingsProps) {
setError(''); setError('');
try { try {
const res = await authFetch('/api/settings'); const res = await authFetch('/api/settings');
if (!res.ok) { setError('Einstellungen konnten nicht geladen werden.'); return; } if (!res.ok) { setError('Failed to load settings.'); return; }
const data: RawSettings = await res.json(); const data: RawSettings = await res.json();
setAzureEnabled(data.azure_enabled === 'true'); setAzureEnabled(data.azure_enabled === 'true');
setAzureClientId(data.azure_client_id || ''); setAzureClientId(data.azure_client_id || '');
@ -122,7 +121,7 @@ export default function Settings({ currentUser }: SettingsProps) {
setCheckmkApiSecret(''); setCheckmkApiSecret('');
setCheckmkSyncInterval(data.checkmk_sync_interval_ms || '60000'); setCheckmkSyncInterval(data.checkmk_sync_interval_ms || '60000');
} catch { } catch {
setError('Netzwerkfehler beim Laden der Einstellungen.'); setError('Network error loading settings.');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -146,7 +145,7 @@ export default function Settings({ currentUser }: SettingsProps) {
const res = await authFetch('/api/settings', { method: 'PUT', body: JSON.stringify(payload) }); const res = await authFetch('/api/settings', { method: 'PUT', body: JSON.stringify(payload) });
if (!res.ok) { if (!res.ok) {
const d = await res.json(); const d = await res.json();
setError(d.error || 'Speichern fehlgeschlagen.'); setError(d.error || 'Failed to save settings.');
return; return;
} }
const data: RawSettings = await res.json(); const data: RawSettings = await res.json();
@ -154,24 +153,15 @@ export default function Settings({ currentUser }: SettingsProps) {
setCheckmkSecretSet(data.checkmk_api_secret === SECRET_SENTINEL); setCheckmkSecretSet(data.checkmk_api_secret === SECRET_SENTINEL);
setAzureClientSecret(''); setAzureClientSecret('');
setCheckmkApiSecret(''); setCheckmkApiSecret('');
setSuccessMsg('Einstellungen gespeichert.'); setSuccessMsg('Settings saved.');
setTimeout(() => setSuccessMsg(''), 4000); setTimeout(() => setSuccessMsg(''), 4000);
} catch { } catch {
setError('Netzwerkfehler beim Speichern.'); setError('Network error saving settings.');
} finally { } finally {
setSaving(false); setSaving(false);
} }
} }
if (currentUser.role.toLowerCase() !== 'admin') {
return (
<div className="flex flex-col items-center justify-center h-64 space-y-3">
<Lock className="w-10 h-10 text-slate-600" />
<p className="text-slate-400 text-sm">Einstellungen sind nur für Administratoren zugänglich.</p>
</div>
);
}
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
@ -189,8 +179,8 @@ export default function Settings({ currentUser }: SettingsProps) {
<Settings2 className="w-5 h-5 text-cyan-400" /> <Settings2 className="w-5 h-5 text-cyan-400" />
</div> </div>
<div> <div>
<h1 className="text-lg font-bold text-white">Einstellungen</h1> <h1 className="text-lg font-bold text-white">Settings</h1>
<p className="text-xs text-slate-400">Integrationen und Dienst-Konfigurationen</p> <p className="text-xs text-slate-400">Integrations and service configuration</p>
</div> </div>
</div> </div>
@ -217,15 +207,14 @@ export default function Settings({ currentUser }: SettingsProps) {
</div> </div>
<div> <div>
<h3 className="text-sm font-semibold text-white">Microsoft Entra ID</h3> <h3 className="text-sm font-semibold text-white">Microsoft Entra ID</h3>
<p className="text-xs text-slate-500">OAuth 2.0 Login r Organisationsaccounts</p> <p className="text-xs text-slate-500">OAuth 2.0 login for organizational accounts</p>
</div> </div>
</div> </div>
{/* Toggle */}
<button <button
type="button" type="button"
onClick={() => setAzureEnabled(v => !v)} onClick={() => setAzureEnabled(v => !v)}
className={`relative w-11 h-6 rounded-full transition-colors ${azureEnabled ? 'bg-blue-600' : 'bg-slate-700'}`} className={`relative w-11 h-6 rounded-full transition-colors ${azureEnabled ? 'bg-blue-600' : 'bg-slate-700'}`}
title={azureEnabled ? 'Deaktivieren' : 'Aktivieren'} title={azureEnabled ? 'Disable' : 'Enable'}
> >
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${azureEnabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${azureEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
@ -251,7 +240,7 @@ export default function Settings({ currentUser }: SettingsProps) {
/> />
</div> </div>
<div> <div>
<FieldLabel>Redirect URI <span className="text-slate-500 font-normal">(leer = automatisch)</span></FieldLabel> <FieldLabel>Redirect URI <span className="text-slate-500 font-normal">(leave blank for auto)</span></FieldLabel>
<TextInput value={azureRedirectUri} onChange={setAzureRedirectUri} placeholder="https://ghostgrid.internal/api/auth/azure/callback" monospace /> <TextInput value={azureRedirectUri} onChange={setAzureRedirectUri} placeholder="https://ghostgrid.internal/api/auth/azure/callback" monospace />
</div> </div>
</div> </div>
@ -265,7 +254,7 @@ export default function Settings({ currentUser }: SettingsProps) {
</div> </div>
<div> <div>
<h3 className="text-sm font-semibold text-white">CheckMK</h3> <h3 className="text-sm font-semibold text-white">CheckMK</h3>
<p className="text-xs text-slate-500">Geräte-Status-Synchronisation über die CheckMK REST API</p> <p className="text-xs text-slate-500">Device status synchronization via the CheckMK REST API</p>
</div> </div>
</div> </div>
@ -290,7 +279,7 @@ export default function Settings({ currentUser }: SettingsProps) {
/> />
</div> </div>
<div> <div>
<FieldLabel>Sync-Intervall (ms)</FieldLabel> <FieldLabel>Sync Interval (ms)</FieldLabel>
<TextInput value={checkmkSyncInterval} onChange={setCheckmkSyncInterval} placeholder="60000" monospace /> <TextInput value={checkmkSyncInterval} onChange={setCheckmkSyncInterval} placeholder="60000" monospace />
</div> </div>
</div> </div>
@ -308,7 +297,7 @@ export default function Settings({ currentUser }: SettingsProps) {
) : ( ) : (
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
)} )}
{saving ? 'Speichert…' : 'Einstellungen speichern'} {saving ? 'Saving…' : 'Save Settings'}
</button> </button>
</div> </div>
</div> </div>