style(backend): clear the eslint backlog to zero

929 problems (928 errors, 1 warning) -> 0, exit 0.

Rule breakdown, which corrects the report's premise -- `indent` dominated, not
`quotes`: indent 719, quotes 68, no-unused-vars 54, no-empty 36,
no-useless-escape 22, no-case-declarations 17, no-inner-declarations 6,
no-control-regex 5, no-useless-catch 1, no-console 1 (warn).

--fix handled only indent + quotes (719+68 = exactly the "fixable" count).
no-useless-escape was NOT auto-fixable in this eslint version, so the one
genuinely risky class never went through the autofixer -- all 22 were done by
hand. Two mechanical proofs on the autofix diff: a token-level AST diff
(espree, before vs after) shows exactly 68 differing tokens, all quotes, with
the 719 indent fixes producing zero token changes; and a cooked-value diff of
every string/template/regex literal shows 0 differences.

Regex escapes: eslint was correctly conservative and did not flag the
load-bearing ones -- \- in [^a-zA-Z0-9_\-\.] (unescaping makes an invalid
reversed _ -> . range) or in [!@#$%^&*()_+\-=...] (would become a + -> = range
silently matching ",-."). Every removal was a \/ \[ or \. inside a character
class; all 11 old/new pairs were brute-forced over 794 inputs with 0
mismatches.

Manual fixes: no-empty were all deliberate best-effort catches around activity
logging, annotated rather than restructured; no-case-declarations braced in
two adminBackup switches; no-inner-declarations converted to const arrows
after checking no call precedes the declaration and no this/arguments use;
no-control-regex and no-console got targeted disables with stated reasons;
one `catch (e) { throw e; }` wrapper removed.

Two unused bindings were near-misses worth noting: secureStatic.js's
`fullPath` is a path-traversal guard (safePathJoin throws on escape) and
restoreService.js's `backupManifest` is the throw-on-corrupt-manifest gate
before a rollback -- deleting either would have silently removed a check. Only
the bindings were dropped; the calls stay.

Two real bugs found and deliberately preserved with a comment plus a narrow
disable rather than deleted, since deleting would erase the evidence:
_workflowSeedBoot.js's `booted` is written but never read, so the intended
once-per-process guard is missing its early return and workflows re-seed on
every call; and quoteService.js's VALID_QUOTE_TRANSITIONS is a full state
machine nothing consults, so quote status changes are unvalidated.

Backend test suite: 253 suites / 2552 tests passing, 0 failures, before and
after.

Refs testplan REPORT.md #22 (Part 1.2.02).
This commit is contained in:
Paul Nothaft
2026-09-01 16:46:34 +02:00
parent da9ceb14ca
commit 9143997f8e
77 changed files with 916 additions and 933 deletions
@@ -28,9 +28,9 @@ describe('sanitizeFilename — accented characters transliterate via NFD (#607)'
const legacyBroken = (s) =>
String(s).trim()
.replace(/\s+/g, '_')
.replace(/[^a-zA-Z0-9_\-\.]/g, '')
.replace(/[_\-]{2,}/g, '_')
.replace(/^[_\-]+|[_\-]+$/g, '');
.replace(/[^a-zA-Z0-9_\-.]/g, '')
.replace(/[_-]{2,}/g, '_')
.replace(/^[_-]+|[_-]+$/g, '');
it.each([
['Ägypten', 'Agypten'],
@@ -138,8 +138,8 @@ describe('sanitizeForContentDisposition — header-safe ASCII fallback', () => {
describe('buildContentDisposition — RFC 6266 / RFC 5987 dual form', () => {
it('emits both filename="..." (ASCII) and filename*=UTF-8\'\'... (unicode) for accented names', () => {
const header = buildContentDisposition('Ägypten.jpg');
expect(header).toContain("filename=\"gypten.jpg\"");
expect(header).toContain("filename*=UTF-8''%C3%84gypten.jpg");
expect(header).toContain('filename="gypten.jpg"');
expect(header).toContain('filename*=UTF-8\'\'%C3%84gypten.jpg');
expect(header.startsWith('attachment;')).toBe(true);
});
+31 -31
View File
@@ -79,37 +79,37 @@ async function loadSecurityConfigFromSettings() {
const value = parseStoredValue(row.setting_value);
switch (row.setting_key) {
case 'security_max_login_attempts': {
config.maxAttempts = normalizePositiveInteger(
'security_max_login_attempts',
value,
DEFAULT_SECURITY_CONFIG.maxAttempts,
{ min: 1, max: 50 }
);
break;
}
case 'security_lockout_duration_minutes': {
const minutes = normalizePositiveInteger(
'security_lockout_duration_minutes',
value,
DEFAULT_SECURITY_CONFIG.lockoutDurationMs / (60 * 1000),
{ min: 1, max: 24 * 60 }
);
config.lockoutDurationMs = minutes * 60 * 1000;
break;
}
case 'security_attempt_window_minutes': {
const minutes = normalizePositiveInteger(
'security_attempt_window_minutes',
value,
DEFAULT_SECURITY_CONFIG.attemptWindowMs / (60 * 1000),
{ min: 1, max: 24 * 60 }
);
config.attemptWindowMs = minutes * 60 * 1000;
break;
}
default:
break;
case 'security_max_login_attempts': {
config.maxAttempts = normalizePositiveInteger(
'security_max_login_attempts',
value,
DEFAULT_SECURITY_CONFIG.maxAttempts,
{ min: 1, max: 50 }
);
break;
}
case 'security_lockout_duration_minutes': {
const minutes = normalizePositiveInteger(
'security_lockout_duration_minutes',
value,
DEFAULT_SECURITY_CONFIG.lockoutDurationMs / (60 * 1000),
{ min: 1, max: 24 * 60 }
);
config.lockoutDurationMs = minutes * 60 * 1000;
break;
}
case 'security_attempt_window_minutes': {
const minutes = normalizePositiveInteger(
'security_attempt_window_minutes',
value,
DEFAULT_SECURITY_CONFIG.attemptWindowMs / (60 * 1000),
{ min: 1, max: 24 * 60 }
);
config.attemptWindowMs = minutes * 60 * 1000;
break;
}
default:
break;
}
});
+2
View File
@@ -58,6 +58,7 @@ function sanitizeCss(css) {
sanitized = sanitized.replace(pattern, '');
});
// eslint-disable-next-line no-control-regex -- intentional: strips control chars from untrusted CSS
sanitized = sanitized.replace(/[\u0000-\u001F\u007F]/g, '');
const MAX_LENGTH = 100 * 1024;
@@ -112,6 +113,7 @@ function sanitizeCSS(cssContent) {
sanitized = sanitized.replace(/<!--[\s\S]*?-->/g, '');
// Remove control characters
// eslint-disable-next-line no-control-regex -- intentional: strips control chars from untrusted CSS
sanitized = sanitized.replace(/[\u0000-\u001F\u007F]/g, '');
// Remove any remaining script-like content
+1
View File
@@ -89,6 +89,7 @@ function sanitizeComment(text) {
text = text.replace(/[\u200B-\u200D\uFEFF]/g, '');
// Remove control characters
// eslint-disable-next-line no-control-regex -- intentional: strips control chars from feedback text
text = text.replace(/[\x00-\x1F\x7F]/g, '');
// Limit consecutive special characters
+2 -1
View File
@@ -37,8 +37,9 @@ function safePathJoin(basePath, userPath) {
function isPathSafe(filePath) {
// Check for common path traversal patterns
const dangerousPatterns = [
/\.\.[\/\\]/, // ../ or ..\
/\.\.[/\\]/, // ../ or ..\
/^[A-Za-z]:/, // Windows drive letters
// eslint-disable-next-line no-control-regex -- intentional: detects control chars in paths
/[\x00-\x1f]/ // Control characters
];
+3 -3
View File
@@ -27,13 +27,13 @@ function sanitizeFilename(str, maxLength = 50) {
sanitized = sanitized.replace(/\s+/g, '_');
// Remove special characters except hyphens, underscores, and dots
sanitized = sanitized.replace(/[^a-zA-Z0-9_\-\.]/g, '');
sanitized = sanitized.replace(/[^a-zA-Z0-9_\-.]/g, '');
// Remove multiple consecutive underscores or hyphens
sanitized = sanitized.replace(/[_\-]{2,}/g, '_');
sanitized = sanitized.replace(/[_-]{2,}/g, '_');
// Remove leading/trailing underscores or hyphens
sanitized = sanitized.replace(/^[_\-]+|[_\-]+$/g, '');
sanitized = sanitized.replace(/^[_-]+|[_-]+$/g, '');
// Limit length
if (sanitized.length > maxLength) {
+1 -1
View File
@@ -93,7 +93,7 @@ function validatePasswordStrength(password) {
result.score += 1;
}
if (!/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)) {
if (!/[!@#$%^&*()_+\-=[\]{}|;:,.<>?]/.test(password)) {
result.messages.push('Password must contain special characters');
} else {
result.score += 1;
+2 -2
View File
@@ -66,7 +66,7 @@ function validatePassword(password, options = {}) {
}
// Check special character requirement
if (config.requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
if (config.requireSpecialChars && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) {
errors.push('Password must contain at least one special character');
}
@@ -230,7 +230,7 @@ async function validatePasswordInContext(password, context, userData = {}) {
// Only allow date-format passwords when complexity is 'simple'
if (complexityLevel === 'simple') {
const datePattern = /^\d{1,2}[.\/-]\d{1,2}[.\/-]\d{4}$/;
const datePattern = /^\d{1,2}[./-]\d{1,2}[./-]\d{4}$/;
if (datePattern.test(password)) {
return {
valid: true,
+1 -1
View File
@@ -25,7 +25,7 @@ function decodeEntities(s) {
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#0*39;|&#x0*27;|&apos;/gi, "'")
.replace(/&#0*39;|&#x0*27;|&apos;/gi, '\'')
.replace(/&amp;/g, '&');
}