diff --git a/AppImage/ProxMenux-1.2.2.2-beta.AppImage b/AppImage/ProxMenux-1.2.2.2-beta.AppImage index 5f7a3191..514f5cc7 100755 Binary files a/AppImage/ProxMenux-1.2.2.2-beta.AppImage and b/AppImage/ProxMenux-1.2.2.2-beta.AppImage differ diff --git a/AppImage/ProxMenux-Monitor.AppImage.sha256 b/AppImage/ProxMenux-Monitor.AppImage.sha256 index f0986472..8db7fb97 100644 --- a/AppImage/ProxMenux-Monitor.AppImage.sha256 +++ b/AppImage/ProxMenux-Monitor.AppImage.sha256 @@ -1 +1 @@ -0a8d1bf1c81a9f60926d5a4af1a570c4acfcd1b41581ce02377600b2b84bf749 ProxMenux-1.2.2.2-beta.AppImage +8b7f4694ade901c9b1058828efd32df05d6621f5648882a172b72d44d5fb6950 ProxMenux-1.2.2.2-beta.AppImage diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index 5bcb54d6..68cfbc14 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -335,6 +335,12 @@ export function NotificationSettings() { const [showHistory, setShowHistory] = useState(false) const [showAdvanced, setShowAdvanced] = useState(false) const [showSecrets, setShowSecrets] = useState>({}) + // Cleartext secrets cached only while the eye toggle is "on" for + // that field. Settings GET returns "************" for everything in + // SENSITIVE_KEYS; clicking eye fetches the real value via + // /api/notifications/reveal-secret and stores it here. Cleared when + // the user toggles eye off, or on every reload — never persists. + const [revealedSecrets, setRevealedSecrets] = useState>({}) const [editMode, setEditMode] = useState(false) const [hasChanges, setHasChanges] = useState(false) const [expandedCategories, setExpandedCategories] = useState>(new Set()) @@ -1065,8 +1071,86 @@ export function NotificationSettings() { } } - const toggleSecret = (key: string) => { - setShowSecrets(prev => ({ ...prev, [key]: !prev[key] })) + // Maps each eye-button local key to the body shape the backend + // expects for /api/notifications/reveal-secret. Centralised here so + // the JSX call site stays a one-liner — `toggleSecret(key)`. + const SECRET_REVEAL_TARGETS: Record> = { + tg_token: { channel: "telegram", field: "bot_token" }, + gt_token: { channel: "gotify", field: "token" }, + dc_hook: { channel: "discord", field: "webhook_url" }, + em_pass: { channel: "email", field: "password" }, + apprise_url: { channel: "apprise", field: "url" }, + } + + const toggleSecret = async (key: string) => { + // Turning eye OFF — drop the cached cleartext immediately. + if (showSecrets[key]) { + setShowSecrets(prev => ({ ...prev, [key]: false })) + setRevealedSecrets(prev => { + const next = { ...prev } + delete next[key] + return next + }) + return + } + + // Turning eye ON — resolve the backend body. ai_key is dynamic + // (depends on current provider); the rest are static. + let body: Record | null = null + if (key === "ai_key") { + const prov = (config.ai_provider || "").trim() + if (prov) body = { ai_provider: prov } + } else { + body = SECRET_REVEAL_TARGETS[key] || null + } + + if (!body) { + // Unknown eye-toggle — flip state and let the input render + // whatever it already has. Never silently skip the UI feedback. + setShowSecrets(prev => ({ ...prev, [key]: true })) + return + } + + try { + // Must go through `fetchApi` (not raw `fetch`) so the JWT + // Authorization header gets attached — the endpoint uses + // `@require_auth` and would reject a plain fetch with 401, which + // is exactly what produced the "still asterisks" behaviour seen + // on the first build. + const data = await fetchApi<{ value?: string }>("/api/notifications/reveal-secret", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }) + const value = typeof data?.value === "string" ? data.value : "" + setRevealedSecrets(prev => ({ ...prev, [key]: value })) + } catch { + // Network/auth failure → still flip the eye so the user sees + // the underlying input changing (and that it has the masked + // placeholder, which signals "not loaded"). Don't crash. + } + setShowSecrets(prev => ({ ...prev, [key]: true })) + } + + // Sentinel the backend sends in place of any populated sensitive + // value (see SENSITIVE_PLACEHOLDER in notification_manager.py). + const SECRET_PLACEHOLDER = "************" + + // Render value for a secret input: + // - if the eye is on AND the stored value is the masked placeholder + // AND we have a revealed cleartext for this key → show cleartext. + // - otherwise show whatever the input already has (the placeholder + // while masked, or the value the user is typing in editMode). + // This avoids overwriting an in-progress edit when the eye toggles. + const secretValue = (key: string, current: string): string => { + if ( + showSecrets[key] && + current === SECRET_PLACEHOLDER && + revealedSecrets[key] !== undefined + ) { + return revealedSecrets[key] + } + return current } if (loading) { @@ -1402,7 +1486,7 @@ export function NotificationSettings() { type={showSecrets["tg_token"] ? "text" : "password"} className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`} placeholder="7595377878:AAGE6Fb2cy... (with or without 'bot' prefix)" - value={config.channels.telegram?.bot_token || ""} + value={secretValue("tg_token", config.channels.telegram?.bot_token || "")} onChange={e => updateChannel("telegram", "bot_token", e.target.value)} disabled={!editMode} /> @@ -1519,7 +1603,7 @@ export function NotificationSettings() { type={showSecrets["gt_token"] ? "text" : "password"} className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`} placeholder="A_valid_gotify_token" - value={config.channels.gotify?.token || ""} + value={secretValue("gt_token", config.channels.gotify?.token || "")} onChange={e => updateChannel("gotify", "token", e.target.value)} disabled={!editMode} /> @@ -1597,7 +1681,7 @@ export function NotificationSettings() { type={showSecrets["dc_hook"] ? "text" : "password"} className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`} placeholder="https://discord.com/api/webhooks/..." - value={config.channels.discord?.webhook_url || ""} + value={secretValue("dc_hook", config.channels.discord?.webhook_url || "")} onChange={e => updateChannel("discord", "webhook_url", e.target.value)} disabled={!editMode} /> @@ -1729,7 +1813,7 @@ export function NotificationSettings() { type={showSecrets["em_pass"] ? "text" : "password"} className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`} placeholder="App password" - value={config.channels.email?.password || ""} + value={secretValue("em_pass", config.channels.email?.password || "")} onChange={e => updateChannel("email", "password", e.target.value)} disabled={!editMode} /> @@ -1840,14 +1924,14 @@ export function NotificationSettings() { type={showSecrets["apprise_url"] ? "text" : "password"} className={`h-7 text-xs font-mono min-w-0 flex-1 ${!editMode ? "opacity-50" : ""}`} placeholder="tgram://bottoken/ChatID" - value={config.channels.apprise?.url || ""} + value={secretValue("apprise_url", config.channels.apprise?.url || "")} onChange={e => updateChannel("apprise", "url", e.target.value)} disabled={!editMode} />