Add ntfy authentication support (username/password and API key)

- Settings table gets ntfy_username, ntfy_password, ntfy_api_key columns
- Backend applies Basic or Bearer auth header when sending notifications
- Settings page UI lets you toggle between no auth, basic, or token auth
- Masked credential display on load to avoid exposing stored secrets
- README updated with auth modes documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 23:49:54 -07:00
parent 1ff44e4276
commit 1bed02ebb5
8 changed files with 109 additions and 16 deletions

View File

@@ -628,3 +628,9 @@ a:hover { text-decoration: underline; }
.main-content { margin-left: 0; padding: 1rem; }
.stats-row { grid-template-columns: 1fr 1fr; }
}
/* Ntfy auth toggle */
.hidden { display: none !important; }
.auth-toggle { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.auth-toggle-option { display: flex; align-items: center; gap: 0.35rem; cursor: pointer; font-size: 0.9rem; }
.auth-toggle-option input[type="radio"] { accent-color: var(--green-mid); }

View File

@@ -182,6 +182,37 @@
<input type="text" id="s-ntfy-topic" class="form-input" placeholder="my-garden-alerts" />
<span class="form-hint">Subscribe to this topic in the ntfy app</span>
</div>
<div class="form-group">
<label class="form-label">Authentication</label>
<div class="auth-toggle">
<label class="auth-toggle-option">
<input type="radio" name="ntfy-auth-type" value="none" checked onchange="App.toggleNtfyAuth(this.value)" /> None
</label>
<label class="auth-toggle-option">
<input type="radio" name="ntfy-auth-type" value="basic" onchange="App.toggleNtfyAuth(this.value)" /> Username &amp; Password
</label>
<label class="auth-toggle-option">
<input type="radio" name="ntfy-auth-type" value="token" onchange="App.toggleNtfyAuth(this.value)" /> API Key / Token
</label>
</div>
</div>
<div id="ntfy-basic-auth" class="hidden">
<div class="form-group">
<label class="form-label">Username</label>
<input type="text" id="s-ntfy-username" class="form-input" placeholder="ntfy username" autocomplete="off" />
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" id="s-ntfy-password" class="form-input" placeholder="ntfy password" autocomplete="off" />
</div>
</div>
<div id="ntfy-token-auth" class="hidden">
<div class="form-group">
<label class="form-label">API Key / Access Token</label>
<input type="password" id="s-ntfy-api-key" class="form-input" placeholder="tk_..." autocomplete="off" />
<span class="form-hint">Generate in ntfy account settings</span>
</div>
</div>
<div class="form-group">
<label class="form-label">Daily Summary Time</label>
<input type="time" id="s-notif-time" class="form-input" />

View File

@@ -389,6 +389,11 @@ function renderGarden() {
function filterBatches() { renderGarden(); }
// ===== Settings =====
function toggleNtfyAuth(type) {
document.getElementById('ntfy-basic-auth').classList.toggle('hidden', type !== 'basic');
document.getElementById('ntfy-token-auth').classList.toggle('hidden', type !== 'token');
}
async function loadSettings() {
try {
state.settings = await api.get('/settings/');
@@ -400,6 +405,16 @@ async function loadSettings() {
document.getElementById('s-ntfy-server').value = s.ntfy_server || 'https://ntfy.sh';
document.getElementById('s-ntfy-topic').value = s.ntfy_topic || '';
document.getElementById('s-notif-time').value = s.notification_time || '07:00';
// Auth type
let authType = 'none';
if (s.ntfy_api_key) authType = 'token';
else if (s.ntfy_username) authType = 'basic';
document.querySelector(`input[name="ntfy-auth-type"][value="${authType}"]`).checked = true;
toggleNtfyAuth(authType);
document.getElementById('s-ntfy-username').value = s.ntfy_username || '';
document.getElementById('s-ntfy-password').value = s.ntfy_password ? '••••••••' : '';
document.getElementById('s-ntfy-api-key').value = s.ntfy_api_key ? '••••••••' : '';
} catch (e) {
toast('Failed to load settings: ' + e.message, true);
}
@@ -407,15 +422,26 @@ async function loadSettings() {
async function saveSettings() {
try {
const authType = document.querySelector('input[name="ntfy-auth-type"]:checked').value;
const rawPassword = document.getElementById('s-ntfy-password').value;
const rawApiKey = document.getElementById('s-ntfy-api-key').value;
// If the placeholder mask is still showing, don't overwrite
const isMask = v => v === '••••••••';
const payload = {
location_name: document.getElementById('s-location').value || null,
last_frost_date: document.getElementById('s-last-frost').value || null,
location_name: document.getElementById('s-location').value || null,
last_frost_date: document.getElementById('s-last-frost').value || null,
first_frost_fall_date: document.getElementById('s-first-frost').value || null,
timezone: document.getElementById('s-timezone').value || 'UTC',
ntfy_server: document.getElementById('s-ntfy-server').value || 'https://ntfy.sh',
ntfy_topic: document.getElementById('s-ntfy-topic').value || null,
notification_time: document.getElementById('s-notif-time').value || '07:00',
timezone: document.getElementById('s-timezone').value || 'UTC',
ntfy_server: document.getElementById('s-ntfy-server').value || 'https://ntfy.sh',
ntfy_topic: document.getElementById('s-ntfy-topic').value || null,
notification_time: document.getElementById('s-notif-time').value || '07:00',
ntfy_username: authType === 'basic' ? (document.getElementById('s-ntfy-username').value || null) : null,
ntfy_password: authType === 'basic' && !isMask(rawPassword) ? (rawPassword || null) : (authType === 'basic' ? undefined : null),
ntfy_api_key: authType === 'token' && !isMask(rawApiKey) ? (rawApiKey || null) : (authType === 'token' ? undefined : null),
};
// Remove undefined keys so they are excluded from the PUT (server keeps existing value)
Object.keys(payload).forEach(k => payload[k] === undefined && delete payload[k]);
await api.put('/settings/', payload);
toast('Settings saved!');
document.getElementById('settings-status').textContent = 'Saved!';
@@ -779,7 +805,7 @@ window.App = {
showAddVarietyModal, showEditVarietyModal, submitAddVariety, submitEditVariety, deleteVariety,
showAddBatchModal, showEditBatchModal, submitAddBatch, submitEditBatch, deleteBatch,
filterVarieties, filterBatches,
saveSettings, sendTestNotification, sendDailySummary,
saveSettings, toggleNtfyAuth, sendTestNotification, sendDailySummary,
closeModal: (e) => closeModal(e),
};