From abe215dd9846404a3c2de02ae7cbca2203b320f9 Mon Sep 17 00:00:00 2001 From: UNITRONIX <36471318+UNITRONIX@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:30:03 +0200 Subject: [PATCH] fix(settings): use Utils.api for SMTP settings to fix HTTP 415 (Fixes #240) Settings Email save/test/load now send Content-Type application/json required by panel API middleware. Adds regression tests for POST without Content-Type. --- CHANGELOG.md | 1 + web-nodejs/public/js/settings.js | 17 ++-------- web-nodejs/tests/smtpSettings.test.js | 49 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc68e2e..e45afd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## [3.3.109] — 2026-07-05 ### Fixed +- **Email/SMTP settings (Fixes #240):** Settings → Email “Test connection” no longer returns HTTP 415 — SMTP save/test/load now use `Utils.api()` so requests include `Content-Type: application/json` required by panel API middleware. - **Panel tabs redirect to dashboard (401):** RustDesk client API routes (`GET /api/devices`, `/api/strategies`) no longer shadow panel session routes — browser requests without Bearer token fall through to panel handlers; `users.js` uses `/api/panel/strategies`; `Utils.api` no longer redirects logged-in users to `/login` (which bounced to dashboard) on incidental 401. - **Devices/Users 429 rate limit:** extended panel poll whitelist (`/api/folders`, `/api/tags`, `/api/device-groups`, `/api/bd/notifications`, `/api/panel/*`); dedicated limiter for `POST /api/desktop/layout`; staggered Devices page API loads; desktop widget layout saves gated when desktop mode is inactive. diff --git a/web-nodejs/public/js/settings.js b/web-nodejs/public/js/settings.js index e891e314..92a5e72b 100644 --- a/web-nodejs/public/js/settings.js +++ b/web-nodejs/public/js/settings.js @@ -4935,13 +4935,7 @@ if (!confirmed) return; } try { - const res = await fetch('/api/settings/email/smtp', { - method: 'PUT', - headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.BetterDesk?.csrfToken || '' }, - body: JSON.stringify(body), - }); - const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Save failed'); + await Utils.api('/api/settings/email/smtp', { method: 'PUT', body }); Notifications.success(_('settings.email.smtp_saved')); document.getElementById('email-smtp-pass').value = ''; await loadEmailSmtpConfig(); @@ -4952,11 +4946,7 @@ testBtn.addEventListener('click', async () => { try { - const res = await fetch('/api/settings/email/smtp/test', { - method: 'POST', - headers: { 'X-CSRF-Token': window.BetterDesk?.csrfToken || '' }, - }); - const data = await res.json(); + const data = await Utils.api('/api/settings/email/smtp/test', { method: 'POST', body: {} }); if (data.success) { Notifications.success(_('settings.email.smtp_test_success')); } else { @@ -4970,8 +4960,7 @@ async function loadEmailSmtpConfig() { try { - const res = await fetch('/api/settings/email/smtp'); - const config = await res.json(); + const config = await Utils.api('/api/settings/email/smtp'); if (!config.configured) { _smtpWasConfigured = false; return; diff --git a/web-nodejs/tests/smtpSettings.test.js b/web-nodejs/tests/smtpSettings.test.js index 4b915db3..085664aa 100644 --- a/web-nodejs/tests/smtpSettings.test.js +++ b/web-nodejs/tests/smtpSettings.test.js @@ -91,3 +91,52 @@ describe('SMTP settings handlers', () => { expect(res.body.success).toBe(true); }); }); + +/** Same logic as routes/index.js requireJsonContentType */ +function requireJsonContentType(req, res, next) { + if (['GET', 'DELETE', 'OPTIONS', 'HEAD'].includes(req.method)) { + return next(); + } + if (!req.path.startsWith('/api/')) { + return next(); + } + if (req.path.includes('/upload') || req.path.includes('/import')) { + return next(); + } + if (!req.is('application/json')) { + return res.status(415).json({ + success: false, + error: 'Content-Type must be application/json', + }); + } + next(); +} + +describe('SMTP test route JSON Content-Type enforcement', () => { + let app; + + beforeEach(() => { + jest.clearAllMocks(); + app = createTestApp(); + withAuth(app, { id: 1, username: 'admin', role: 'server_admin' }); + app.use(requireJsonContentType); + app.post('/api/settings/email/smtp/test', testSmtpSettings); + }); + + it('returns 415 when POST has no Content-Type header', async () => { + const res = await request(app).post('/api/settings/email/smtp/test'); + expect(res.status).toBe(415); + expect(res.body.error).toBe('Content-Type must be application/json'); + expect(emailService.testConnection).not.toHaveBeenCalled(); + }); + + it('allows POST with application/json Content-Type', async () => { + const res = await request(app) + .post('/api/settings/email/smtp/test') + .set('Content-Type', 'application/json') + .send({}); + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(emailService.testConnection).toHaveBeenCalled(); + }); +});