Return actual sensitive values from API for visibility toggle

Backend:
- Update /settings/notifications to return actual tokens/keys
- Update /settings/ai to return actual API keys

Frontend:
- Update NotificationSettings and AISettings types
- Populate form fields with actual saved values on load
- Eye toggle now reveals actual stored values
- Always show toggle button for consistent UX

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
clucraft 2026-01-22 20:45:06 -05:00
parent b885e4ef57
commit a4da43c127
5 changed files with 63 additions and 67 deletions

View file

@ -18,14 +18,14 @@ router.get('/notifications', async (req: AuthRequest, res: Response) => {
return;
}
// Don't expose full tokens, just indicate if they're set
res.json({
telegram_configured: !!(settings.telegram_bot_token && settings.telegram_chat_id),
telegram_chat_id: settings.telegram_chat_id,
telegram_bot_token: settings.telegram_bot_token || null,
telegram_chat_id: settings.telegram_chat_id || null,
telegram_enabled: settings.telegram_enabled ?? true,
discord_configured: !!settings.discord_webhook_url,
discord_webhook_url: settings.discord_webhook_url || null,
discord_enabled: settings.discord_enabled ?? true,
pushover_configured: !!(settings.pushover_user_key && settings.pushover_app_token),
pushover_user_key: settings.pushover_user_key || null,
pushover_app_token: settings.pushover_app_token || null,
pushover_enabled: settings.pushover_enabled ?? true,
});
} catch (error) {
@ -66,12 +66,13 @@ router.put('/notifications', async (req: AuthRequest, res: Response) => {
}
res.json({
telegram_configured: !!(settings.telegram_bot_token && settings.telegram_chat_id),
telegram_chat_id: settings.telegram_chat_id,
telegram_bot_token: settings.telegram_bot_token || null,
telegram_chat_id: settings.telegram_chat_id || null,
telegram_enabled: settings.telegram_enabled ?? true,
discord_configured: !!settings.discord_webhook_url,
discord_webhook_url: settings.discord_webhook_url || null,
discord_enabled: settings.discord_enabled ?? true,
pushover_configured: !!(settings.pushover_user_key && settings.pushover_app_token),
pushover_user_key: settings.pushover_user_key || null,
pushover_app_token: settings.pushover_app_token || null,
pushover_enabled: settings.pushover_enabled ?? true,
message: 'Notification settings updated successfully',
});
@ -196,13 +197,11 @@ router.get('/ai', async (req: AuthRequest, res: Response) => {
return;
}
// Don't expose full API keys, just indicate if they're set
res.json({
ai_enabled: settings.ai_enabled || false,
ai_provider: settings.ai_provider || null,
anthropic_configured: !!settings.anthropic_api_key,
openai_configured: !!settings.openai_api_key,
ollama_configured: !!(settings.ollama_base_url && settings.ollama_model),
anthropic_api_key: settings.anthropic_api_key || null,
openai_api_key: settings.openai_api_key || null,
ollama_base_url: settings.ollama_base_url || null,
ollama_model: settings.ollama_model || null,
});
@ -235,9 +234,8 @@ router.put('/ai', async (req: AuthRequest, res: Response) => {
res.json({
ai_enabled: settings.ai_enabled || false,
ai_provider: settings.ai_provider || null,
anthropic_configured: !!settings.anthropic_api_key,
openai_configured: !!settings.openai_api_key,
ollama_configured: !!(settings.ollama_base_url && settings.ollama_model),
anthropic_api_key: settings.anthropic_api_key || null,
openai_api_key: settings.openai_api_key || null,
ollama_base_url: settings.ollama_base_url || null,
ollama_model: settings.ollama_model || null,
message: 'AI settings updated successfully',

View file

@ -149,12 +149,13 @@ export const stockHistoryApi = {
// Settings API
export interface NotificationSettings {
telegram_configured: boolean;
telegram_bot_token: string | null;
telegram_chat_id: string | null;
telegram_enabled: boolean;
discord_configured: boolean;
discord_webhook_url: string | null;
discord_enabled: boolean;
pushover_configured: boolean;
pushover_user_key: string | null;
pushover_app_token: string | null;
pushover_enabled: boolean;
}
@ -206,9 +207,8 @@ export const settingsApi = {
export interface AISettings {
ai_enabled: boolean;
ai_provider: 'anthropic' | 'openai' | 'ollama' | null;
anthropic_configured: boolean;
openai_configured: boolean;
ollama_configured: boolean;
anthropic_api_key: string | null;
openai_api_key: string | null;
ollama_base_url: string | null;
ollama_model: string | null;
}

View file

@ -4,24 +4,22 @@ interface PasswordInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
// All standard input props are inherited
}
export default function PasswordInput({ style, value, ...props }: PasswordInputProps) {
export default function PasswordInput({ style, ...props }: PasswordInputProps) {
const [visible, setVisible] = useState(false);
const hasValue = value !== undefined && value !== null && String(value).length > 0;
return (
<div style={{ position: 'relative' }}>
<input
{...props}
value={value}
type={visible ? 'text' : 'password'}
style={{
...style,
width: '100%',
paddingRight: hasValue ? '2.5rem' : '0.75rem',
paddingRight: '2.5rem',
boxSizing: 'border-box',
}}
/>
{hasValue && <button
<button
type="button"
onClick={(e) => {
e.preventDefault();
@ -79,7 +77,7 @@ export default function PasswordInput({ style, value, ...props }: PasswordInputP
<circle cx="12" cy="12" r="3" />
</svg>
)}
</button>}
</button>
</div>
);
}

View file

@ -525,9 +525,9 @@ export default function ProductDetail() {
<StockTimeline productId={productId} days={30} />
{notificationSettings && (
(notificationSettings.telegram_configured && notificationSettings.telegram_enabled) ||
(notificationSettings.discord_configured && notificationSettings.discord_enabled) ||
(notificationSettings.pushover_configured && notificationSettings.pushover_enabled)
((notificationSettings.telegram_bot_token && notificationSettings.telegram_chat_id) && notificationSettings.telegram_enabled) ||
(notificationSettings.discord_webhook_url && notificationSettings.discord_enabled) ||
((notificationSettings.pushover_user_key && notificationSettings.pushover_app_token) && notificationSettings.pushover_enabled)
) && (
<>
<style>{`
@ -676,13 +676,13 @@ export default function ProductDetail() {
<span className="notification-settings-icon">🔔</span>
<h2 className="notification-settings-title">Notification Settings</h2>
<div className="notification-settings-channels">
{notificationSettings.telegram_configured && notificationSettings.telegram_enabled && (
{(notificationSettings.telegram_bot_token && notificationSettings.telegram_chat_id) && notificationSettings.telegram_enabled && (
<span className="notification-channel-badge">Telegram</span>
)}
{notificationSettings.discord_configured && notificationSettings.discord_enabled && (
{notificationSettings.discord_webhook_url && notificationSettings.discord_enabled && (
<span className="notification-channel-badge">Discord</span>
)}
{notificationSettings.pushover_configured && notificationSettings.pushover_enabled && (
{(notificationSettings.pushover_user_key && notificationSettings.pushover_app_token) && notificationSettings.pushover_enabled && (
<span className="notification-channel-badge">Pushover</span>
)}
</div>

View file

@ -81,23 +81,25 @@ export default function Settings() {
setProfile(profileRes.data);
setProfileName(profileRes.data.name || '');
setNotificationSettings(notificationsRes.data);
if (notificationsRes.data.telegram_chat_id) {
setTelegramChatId(notificationsRes.data.telegram_chat_id);
}
// Populate notification fields with actual values
setTelegramBotToken(notificationsRes.data.telegram_bot_token || '');
setTelegramChatId(notificationsRes.data.telegram_chat_id || '');
setTelegramEnabled(notificationsRes.data.telegram_enabled ?? true);
setDiscordWebhookUrl(notificationsRes.data.discord_webhook_url || '');
setDiscordEnabled(notificationsRes.data.discord_enabled ?? true);
setPushoverUserKey(notificationsRes.data.pushover_user_key || '');
setPushoverAppToken(notificationsRes.data.pushover_app_token || '');
setPushoverEnabled(notificationsRes.data.pushover_enabled ?? true);
// Populate AI fields with actual values
setAISettings(aiRes.data);
setAIEnabled(aiRes.data.ai_enabled);
if (aiRes.data.ai_provider) {
setAIProvider(aiRes.data.ai_provider);
}
if (aiRes.data.ollama_base_url) {
setOllamaBaseUrl(aiRes.data.ollama_base_url);
}
if (aiRes.data.ollama_model) {
setOllamaModel(aiRes.data.ollama_model);
}
setAnthropicApiKey(aiRes.data.anthropic_api_key || '');
setOpenaiApiKey(aiRes.data.openai_api_key || '');
setOllamaBaseUrl(aiRes.data.ollama_base_url || '');
setOllamaModel(aiRes.data.ollama_model || '');
} catch {
setError('Failed to load settings');
} finally {
@ -958,8 +960,8 @@ export default function Settings() {
<div className="settings-section-header">
<span className="settings-section-icon">📱</span>
<h2 className="settings-section-title">Telegram Notifications</h2>
<span className={`settings-section-status ${notificationSettings?.telegram_configured ? 'configured' : 'not-configured'}`}>
{notificationSettings?.telegram_configured ? 'Configured' : 'Not configured'}
<span className={`settings-section-status ${notificationSettings?.telegram_bot_token && notificationSettings?.telegram_chat_id ? 'configured' : 'not-configured'}`}>
{notificationSettings?.telegram_bot_token && notificationSettings?.telegram_chat_id ? 'Configured' : 'Not configured'}
</span>
</div>
<p className="settings-section-description">
@ -967,7 +969,7 @@ export default function Settings() {
and get your chat ID.
</p>
{notificationSettings?.telegram_configured && (
{notificationSettings?.telegram_bot_token && notificationSettings?.telegram_chat_id && (
<div className="settings-toggle">
<div className="settings-toggle-label">
<span className="settings-toggle-title">Enable Telegram Notifications</span>
@ -987,7 +989,7 @@ export default function Settings() {
<PasswordInput
value={telegramBotToken}
onChange={(e) => setTelegramBotToken(e.target.value)}
placeholder={notificationSettings?.telegram_configured ? 'Saved - enter new value to replace' : 'Enter your bot token'}
placeholder="Enter your bot token"
/>
<p className="hint">Create a bot via @BotFather on Telegram to get a token</p>
</div>
@ -1011,7 +1013,7 @@ export default function Settings() {
>
{isSavingNotifications ? 'Saving...' : 'Save Telegram Settings'}
</button>
{notificationSettings?.telegram_configured && (
{notificationSettings?.telegram_bot_token && notificationSettings?.telegram_chat_id && (
<button
className="btn btn-secondary"
onClick={handleTestTelegram}
@ -1027,8 +1029,8 @@ export default function Settings() {
<div className="settings-section-header">
<span className="settings-section-icon">💬</span>
<h2 className="settings-section-title">Discord Notifications</h2>
<span className={`settings-section-status ${notificationSettings?.discord_configured ? 'configured' : 'not-configured'}`}>
{notificationSettings?.discord_configured ? 'Configured' : 'Not configured'}
<span className={`settings-section-status ${notificationSettings?.discord_webhook_url ? 'configured' : 'not-configured'}`}>
{notificationSettings?.discord_webhook_url ? 'Configured' : 'Not configured'}
</span>
</div>
<p className="settings-section-description">
@ -1036,7 +1038,7 @@ export default function Settings() {
Discord server settings.
</p>
{notificationSettings?.discord_configured && (
{notificationSettings?.discord_webhook_url && (
<div className="settings-toggle">
<div className="settings-toggle-label">
<span className="settings-toggle-title">Enable Discord Notifications</span>
@ -1056,7 +1058,7 @@ export default function Settings() {
<PasswordInput
value={discordWebhookUrl}
onChange={(e) => setDiscordWebhookUrl(e.target.value)}
placeholder={notificationSettings?.discord_configured ? 'Saved - enter new value to replace' : 'https://discord.com/api/webhooks/...'}
placeholder="https://discord.com/api/webhooks/..."
/>
<p className="hint">Server Settings Integrations Webhooks New Webhook</p>
</div>
@ -1069,7 +1071,7 @@ export default function Settings() {
>
{isSavingNotifications ? 'Saving...' : 'Save Discord Settings'}
</button>
{notificationSettings?.discord_configured && (
{notificationSettings?.discord_webhook_url && (
<button
className="btn btn-secondary"
onClick={handleTestDiscord}
@ -1085,8 +1087,8 @@ export default function Settings() {
<div className="settings-section-header">
<span className="settings-section-icon">🔔</span>
<h2 className="settings-section-title">Pushover Notifications</h2>
<span className={`settings-section-status ${notificationSettings?.pushover_configured ? 'configured' : 'not-configured'}`}>
{notificationSettings?.pushover_configured ? 'Configured' : 'Not configured'}
<span className={`settings-section-status ${notificationSettings?.pushover_user_key && notificationSettings?.pushover_app_token ? 'configured' : 'not-configured'}`}>
{notificationSettings?.pushover_user_key && notificationSettings?.pushover_app_token ? 'Configured' : 'Not configured'}
</span>
</div>
<p className="settings-section-description">
@ -1094,7 +1096,7 @@ export default function Settings() {
and an application to get your keys.
</p>
{notificationSettings?.pushover_configured && (
{notificationSettings?.pushover_user_key && notificationSettings?.pushover_app_token && (
<div className="settings-toggle">
<div className="settings-toggle-label">
<span className="settings-toggle-title">Enable Pushover Notifications</span>
@ -1114,7 +1116,7 @@ export default function Settings() {
<PasswordInput
value={pushoverUserKey}
onChange={(e) => setPushoverUserKey(e.target.value)}
placeholder={notificationSettings?.pushover_configured ? 'Saved - enter new value to replace' : 'Enter your user key'}
placeholder="Enter your user key"
/>
<p className="hint">Find your User Key on the Pushover dashboard after logging in</p>
</div>
@ -1124,7 +1126,7 @@ export default function Settings() {
<PasswordInput
value={pushoverAppToken}
onChange={(e) => setPushoverAppToken(e.target.value)}
placeholder={notificationSettings?.pushover_configured ? 'Saved - enter new value to replace' : 'Enter your app token'}
placeholder="Enter your app token"
/>
<p className="hint">Create an application at pushover.net/apps to get an API token</p>
</div>
@ -1137,7 +1139,7 @@ export default function Settings() {
>
{isSavingNotifications ? 'Saving...' : 'Save Pushover Settings'}
</button>
{notificationSettings?.pushover_configured && (
{notificationSettings?.pushover_user_key && notificationSettings?.pushover_app_token && (
<button
className="btn btn-secondary"
onClick={handleTestPushover}
@ -1208,15 +1210,14 @@ export default function Settings() {
<PasswordInput
value={anthropicApiKey}
onChange={(e) => setAnthropicApiKey(e.target.value)}
placeholder={aiSettings?.anthropic_configured ? 'Saved - enter new value to replace' : 'sk-ant-...'}
placeholder="sk-ant-..."
/>
<p className="hint">
Get your API key from{' '}
<a href="https://console.anthropic.com/" target="_blank" rel="noopener noreferrer">
console.anthropic.com
</a>
{aiSettings?.anthropic_configured && ' (key already saved)'}
</p>
</p>
</div>
)}
@ -1226,15 +1227,14 @@ export default function Settings() {
<PasswordInput
value={openaiApiKey}
onChange={(e) => setOpenaiApiKey(e.target.value)}
placeholder={aiSettings?.openai_configured ? 'Saved - enter new value to replace' : 'sk-...'}
placeholder="sk-..."
/>
<p className="hint">
Get your API key from{' '}
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">
platform.openai.com
</a>
{aiSettings?.openai_configured && ' (key already saved)'}
</p>
</p>
</div>
)}
@ -1298,7 +1298,7 @@ export default function Settings() {
? 'Select from available models or test connection to refresh list'
: 'Enter model name or test connection to see available models'
}
{aiSettings?.ollama_configured && ` (currently: ${aiSettings.ollama_model})`}
{aiSettings?.ollama_base_url && aiSettings?.ollama_model && ` (currently: ${aiSettings.ollama_model})`}
</p>
</div>
</>
@ -1317,7 +1317,7 @@ export default function Settings() {
</div>
</div>
{aiSettings?.ai_enabled && (aiSettings.anthropic_configured || aiSettings.openai_configured || aiSettings.ollama_configured) && (
{aiSettings?.ai_enabled && (aiSettings.anthropic_api_key || aiSettings.openai_api_key || (aiSettings.ollama_base_url && aiSettings.ollama_model)) && (
<div className="settings-section">
<div className="settings-section-header">
<span className="settings-section-icon">🧪</span>