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.
This commit is contained in:
UNITRONIX
2026-07-05 21:30:03 +02:00
parent d40a05964d
commit abe215dd98
3 changed files with 53 additions and 14 deletions
+1
View File
@@ -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.
+3 -14
View File
@@ -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;
+49
View File
@@ -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();
});
});