diff --git a/routes/about.js b/routes/about.cts similarity index 72% rename from routes/about.js rename to routes/about.cts index 4263caa..2aa2154 100644 --- a/routes/about.js +++ b/routes/about.cts @@ -1,10 +1,12 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); let pkg; try { pkg = require('../package.json'); } catch { pkg = { version: '0.1.0' }; } -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { res.json({ name: 'Bill Tracker', version: pkg.version, diff --git a/routes/aboutAdmin.js b/routes/aboutAdmin.cts similarity index 97% rename from routes/aboutAdmin.js rename to routes/aboutAdmin.cts index 4ee78b0..a683f4e 100644 --- a/routes/aboutAdmin.js +++ b/routes/aboutAdmin.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const fs = require('fs'); const path = require('path'); @@ -415,7 +417,7 @@ async function fetchForgejoIssues() { } // Admin-only endpoint to serve FUTURE.md and DEVELOPMENT_LOG.md content (raw markdown, backward compat) -router.get('/', requireAuth, requireAdmin, (req, res) => { +router.get('/', requireAuth, requireAdmin, (req: Req, res: Res) => { try { // Read both files directly from the allowlist const futureContent = fs.readFileSync(ALLOWED_FILES['FUTURE.md'], 'utf-8'); @@ -441,7 +443,7 @@ router.get('/', requireAuth, requireAdmin, (req, res) => { }); // Admin-only endpoint: open issues from Forgejo (5-min cache, ?refresh=1 to bypass) -router.get('/roadmap', requireAuth, requireAdmin, async (req, res) => { +router.get('/roadmap', requireAuth, requireAdmin, async (req: Req, res: Res) => { const now = Date.now(); const refresh = req.query.refresh === '1'; try { @@ -460,7 +462,7 @@ router.get('/roadmap', requireAuth, requireAdmin, async (req, res) => { }); // Admin-only endpoint: parsed dev log entries from DEVELOPMENT_LOG.md -router.get('/dev-log', requireAuth, requireAdmin, (req, res) => { +router.get('/dev-log', requireAuth, requireAdmin, (req: Req, res: Res) => { try { const devLogContent = fs.readFileSync(ALLOWED_FILES['DEVELOPMENT_LOG.md'], 'utf-8'); const sanitized = redactSensitiveContent(devLogContent); @@ -478,7 +480,7 @@ router.get('/dev-log', requireAuth, requireAdmin, (req, res) => { const { checkForUpdates } = require('../services/updateCheckService.cts'); // POST /api/about-admin/check-updates — force a fresh update check, bypassing cache -router.post('/check-updates', requireAuth, requireAdmin, async (req, res) => { +router.post('/check-updates', requireAuth, requireAdmin, async (req: Req, res: Res) => { try { const result = await checkForUpdates(true); res.json(result); @@ -491,12 +493,12 @@ router.post('/check-updates', requireAuth, requireAdmin, async (req, res) => { const { getSetting, setSetting } = require('../db/database'); // GET /api/about-admin/update-check-setting — is the external version check enabled? -router.get('/update-check-setting', requireAuth, requireAdmin, (req, res) => { +router.get('/update-check-setting', requireAuth, requireAdmin, (req: Req, res: Res) => { res.json({ enabled: getSetting('update_check_enabled') !== 'false' }); }); // PUT /api/about-admin/update-check-setting — enable/disable the external version check -router.put('/update-check-setting', requireAuth, requireAdmin, (req, res) => { +router.put('/update-check-setting', requireAuth, requireAdmin, (req: Req, res: Res) => { const { enabled } = req.body || {}; if (typeof enabled !== 'boolean') { return res.status(400).json({ error: 'enabled must be a boolean' }); diff --git a/routes/admin.js b/routes/admin.cts similarity index 91% rename from routes/admin.js rename to routes/admin.cts index 00ba43a..522a29f 100644 --- a/routes/admin.js +++ b/routes/admin.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getDb, rollbackMigration } = require('../db/database'); @@ -40,13 +42,13 @@ function sendError(res, err) { } // GET /api/admin/has-users -router.get('/has-users', (req, res) => { +router.get('/has-users', (req: Req, res: Res) => { const count = getDb().prepare('SELECT COUNT(*) AS n FROM users WHERE id != ?').get(req.user.id).n; res.json({ has_users: count > 0 }); }); // GET /api/admin/users -router.get('/users', (req, res) => { +router.get('/users', (req: Req, res: Res) => { res.json( getDb().prepare( 'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users ORDER BY is_default_admin DESC, role DESC, username ASC' @@ -55,7 +57,7 @@ router.get('/users', (req, res) => { }); // POST /api/admin/backups -router.post('/backups', backupOperationLimiter, async (req, res) => { +router.post('/backups', backupOperationLimiter, async (req: Req, res: Res) => { try { const backup = await createBackup(); res.status(201).json(backup); @@ -65,7 +67,7 @@ router.post('/backups', backupOperationLimiter, async (req, res) => { }); // GET /api/admin/backups -router.get('/backups', backupOperationLimiter, (req, res) => { +router.get('/backups', backupOperationLimiter, (req: Req, res: Res) => { try { res.json({ backups: listBackups() }); } catch (err) { @@ -74,7 +76,7 @@ router.get('/backups', backupOperationLimiter, (req, res) => { }); // GET /api/admin/backups/settings -router.get('/backups/settings', backupOperationLimiter, (req, res) => { +router.get('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => { try { res.json(getScheduleStatus()); } catch (err) { @@ -83,7 +85,7 @@ router.get('/backups/settings', backupOperationLimiter, (req, res) => { }); // PUT /api/admin/backups/settings -router.put('/backups/settings', backupOperationLimiter, (req, res) => { +router.put('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => { try { res.json(saveBackupScheduleSettings(req.body)); } catch (err) { @@ -92,7 +94,7 @@ router.put('/backups/settings', backupOperationLimiter, (req, res) => { }); // POST /api/admin/backups/run-scheduled-now -router.post('/backups/run-scheduled-now', backupOperationLimiter, async (req, res) => { +router.post('/backups/run-scheduled-now', backupOperationLimiter, async (req: Req, res: Res) => { try { res.status(201).json(await runScheduledBackupNow()); } catch (err) { @@ -108,7 +110,7 @@ router.post( type: ['application/octet-stream', 'application/x-sqlite3', 'application/vnd.sqlite3'], limit: '100mb', }), - async (req, res) => { + async (req: Req, res: Res) => { try { // Extract expected checksum from request headers or query const expectedChecksum = req.headers['x-checksum-sha256'] || req.query.checksum; @@ -124,7 +126,7 @@ router.post( ); // GET /api/admin/backups/:id/download -router.get('/backups/:id/download', (req, res) => { +router.get('/backups/:id/download', (req: Req, res: Res) => { try { const backup = getBackupFile(req.params.id); res.setHeader('Content-Type', 'application/octet-stream'); @@ -138,7 +140,7 @@ router.get('/backups/:id/download', (req, res) => { }); // POST /api/admin/backups/:id/restore -router.post('/backups/:id/restore', backupOperationLimiter, async (req, res) => { +router.post('/backups/:id/restore', backupOperationLimiter, async (req: Req, res: Res) => { try { res.json(await restoreBackup(req.params.id)); } catch (err) { @@ -147,7 +149,7 @@ router.post('/backups/:id/restore', backupOperationLimiter, async (req, res) => }); // DELETE /api/admin/backups/:id -router.delete('/backups/:id', backupOperationLimiter, (req, res) => { +router.delete('/backups/:id', backupOperationLimiter, (req: Req, res: Res) => { try { res.json(deleteBackup(req.params.id)); } catch (err) { @@ -156,7 +158,7 @@ router.delete('/backups/:id', backupOperationLimiter, (req, res) => { }); // POST /api/admin/users -router.post('/users', async (req, res) => { +router.post('/users', async (req: Req, res: Res) => { const { username, password } = req.body; if (!username || username.length < 3) return res.status(400).json({ error: 'Username must be at least 3 characters' }); @@ -192,7 +194,7 @@ router.post('/users', async (req, res) => { }); // PUT /api/admin/users/:id/password -router.put('/users/:id/password', async (req, res) => { +router.put('/users/:id/password', async (req: Req, res: Res) => { const targetId = parseUserId(req.params); if (!targetId) return res.status(400).json({ error: 'Invalid user ID' }); @@ -227,7 +229,7 @@ router.put('/users/:id/password', async (req, res) => { // PUT /api/admin/users/:id/role // Promote/demote an existing user. Prevents removing the last admin or // changing your own role mid-session. -router.put('/users/:id/role', (req, res) => { +router.put('/users/:id/role', (req: Req, res: Res) => { const targetId = parseUserId(req.params); if (!targetId) return res.status(400).json({ error: 'Invalid user ID' }); @@ -269,7 +271,7 @@ router.put('/users/:id/role', (req, res) => { }); // PUT /api/admin/users/:id/active -router.put('/users/:id/active', (req, res) => { +router.put('/users/:id/active', (req: Req, res: Res) => { const targetId = parseUserId(req.params); if (!targetId) return res.status(400).json({ error: 'Invalid user ID' }); @@ -290,7 +292,7 @@ router.put('/users/:id/active', (req, res) => { }); // PUT /api/admin/users/:id/username -router.put('/users/:id/username', (req, res) => { +router.put('/users/:id/username', (req: Req, res: Res) => { const { username } = req.body; if (!username || typeof username !== 'string') { return res.status(400).json({ error: 'username is required' }); @@ -333,7 +335,7 @@ router.put('/users/:id/username', (req, res) => { }); // DELETE /api/admin/users/:id -router.delete('/users/:id', (req, res) => { +router.delete('/users/:id', (req: Req, res: Res) => { const targetId = parseUserId(req.params); if (!targetId) return res.status(400).json({ error: 'Invalid user ID' }); @@ -366,7 +368,7 @@ router.delete('/users/:id', (req, res) => { // GET /api/admin/cleanup // Returns current cleanup settings and the result of the last cleanup run. -router.get('/cleanup', (req, res) => { +router.get('/cleanup', (req: Req, res: Res) => { try { res.json(getCleanupStatus()); } catch (err) { @@ -383,7 +385,7 @@ router.get('/cleanup', (req, res) => { // backup_partials_enabled boolean prune orphaned .partial/.upload backup files // import_history_enabled boolean prune old import history rows (disabled by default) // import_history_max_age_days 30–3650 age threshold for import history rows -router.put('/cleanup', (req, res) => { +router.put('/cleanup', (req: Req, res: Res) => { try { res.json(applyCleanupSettings(req.body)); } catch (err) { @@ -393,7 +395,7 @@ router.put('/cleanup', (req, res) => { // POST /api/admin/cleanup/run // Runs all enabled cleanup tasks immediately and returns the result. -router.post('/cleanup/run', backupOperationLimiter, async (req, res) => { +router.post('/cleanup/run', backupOperationLimiter, async (req: Req, res: Res) => { try { const result = await runAllCleanup(); res.json(result); @@ -412,14 +414,14 @@ const { } = require('../services/oidcService.cts'); // GET /api/admin/auth-mode -router.get('/auth-mode', (req, res) => { +router.get('/auth-mode', (req: Req, res: Res) => { res.json(buildAuthModeStatus()); }); // POST /api/admin/auth-mode/oidc-test // Tests submitted or saved OIDC provider settings with OIDC discovery. // Never returns client secret or token material. -router.post('/auth-mode/oidc-test', async (req, res) => { +router.post('/auth-mode/oidc-test', async (req: Req, res: Res) => { const config = buildSubmittedOidcConfig(req.body || {}); const result = await testOidcConfiguration(config); res.status(result.ok ? 200 : 400).json(result); @@ -428,7 +430,7 @@ router.post('/auth-mode/oidc-test', async (req, res) => { // PUT /api/admin/auth-mode // Accepts legacy auth_mode/default_user_id fields plus new auth method settings. // Validates lockout protection before saving. -router.put('/auth-mode', (req, res) => { +router.put('/auth-mode', (req: Req, res: Res) => { try { res.json(applyAuthModeSettings(req.body || {})); } catch (err) { @@ -439,12 +441,12 @@ router.put('/auth-mode', (req, res) => { // ── Bank Sync Config ────────────────────────────────────────────────────────── // GET /api/admin/bank-sync-config -router.get('/bank-sync-config', (req, res) => { +router.get('/bank-sync-config', (req: Req, res: Res) => { res.json({ ...getBankSyncConfig(), worker: getBankSyncWorkerStatus(), encryption_key_source: isEnvKeyActive() ? 'env' : 'db', key_fingerprint: keyFingerprint() }); }); // PUT /api/admin/bank-sync-config -router.put('/bank-sync-config', (req, res) => { +router.put('/bank-sync-config', (req: Req, res: Res) => { const { enabled, sync_interval_hours, sync_days, debug_logging } = req.body || {}; try { let config = getBankSyncConfig(); @@ -459,7 +461,7 @@ router.put('/bank-sync-config', (req, res) => { }); // ── Migration Rollback ──────────────────────────────────────────────────────── -router.post('/migrations/rollback', async (req, res) => { +router.post('/migrations/rollback', async (req: Req, res: Res) => { const { version } = req.body; if (!version) { return res.status(400).json({ error: 'Version is required' }); diff --git a/routes/analytics.js b/routes/analytics.cts similarity index 64% rename from routes/analytics.js rename to routes/analytics.cts index 952fd44..eb5a434 100644 --- a/routes/analytics.js +++ b/routes/analytics.cts @@ -1,9 +1,11 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { standardizeError } = require('../middleware/errorFormatter'); const { getAnalyticsSummary } = require('../services/analyticsService.cts'); -router.get('/summary', (req, res) => { +router.get('/summary', (req: Req, res: Res) => { const result = getAnalyticsSummary(req.user.id, req.query); if (result.error) return res.status(400).json(standardizeError(result.error, 'VALIDATION_ERROR', 'month')); res.json(result); diff --git a/routes/auth.js b/routes/auth.cts similarity index 92% rename from routes/auth.js rename to routes/auth.cts index 23e47c2..43ee8de 100644 --- a/routes/auth.js +++ b/routes/auth.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); @@ -25,12 +27,12 @@ const { logAudit } = require('../services/auditService.cts'); // ───────────────────────────────────────── // POST /api/auth/login -router.post('/login', (req, res, next) => { +router.post('/login', (req: Req, res: Res, next: Next) => { // Exempt login from CSRF - no session exists yet to hijack // CSRF validation happens on all other authenticated routes req.csrfSkip = true; next(); -}, async (req, res) => { +}, async (req: Req, res: Res) => { // Respect admin-configured login method toggle if (getSetting('local_login_enabled') === 'false') { return res.status(403).json(standardizeError('Local username/password login is not enabled on this server.', 'FORBIDDEN')); @@ -72,13 +74,13 @@ router.post('/login', (req, res, next) => { // Public — returns the CSRF token from the httpOnly cookie so the SPA can // store it in memory and send it in the x-csrf-token header for mutations. // Cross-origin access is prevented by the same-origin fetch policy (no CORS). -router.get('/csrf-token', (req, res) => { +router.get('/csrf-token', (req: Req, res: Res) => { const token = getCsrfToken(req, res); res.json({ token }); }); // POST /api/auth/logout -router.post('/logout', requireAuth, (req, res) => { +router.post('/logout', requireAuth, (req: Req, res: Res) => { logout(req.cookies?.[COOKIE_NAME]); logAudit({ user_id: req.user.id, action: 'logout', ip_address: req.ip, user_agent: req.get('user-agent') }); res.clearCookie(COOKIE_NAME, { path: '/', ...cookieOpts(req), maxAge: undefined }); @@ -86,7 +88,7 @@ router.post('/logout', requireAuth, (req, res) => { }); // POST /api/auth/logout-all -router.post('/logout-all', requireAuth, (req, res) => { +router.post('/logout-all', requireAuth, (req: Req, res: Res) => { // Delete ALL sessions for this user invalidateOtherSessions(req.user.id, null); // null means delete all sessions @@ -100,14 +102,14 @@ router.post('/logout-all', requireAuth, (req, res) => { }); // POST /api/auth/logout-others — sign out every OTHER device, keep this one signed in. -router.post('/logout-others', requireAuth, (req, res) => { +router.post('/logout-others', requireAuth, (req: Req, res: Res) => { const result = invalidateOtherSessions(req.user.id, req.cookies?.[COOKIE_NAME]); logAudit({ user_id: req.user.id, action: 'logout.others', ip_address: req.ip, user_agent: req.get('user-agent') }); res.json({ success: true, count: result?.changes || 0 }); }); // GET /api/auth/me -router.get('/me', requireAuth, (req, res) => { +router.get('/me', requireAuth, (req: Req, res: Res) => { const currentVersion = getAppVersion(); res.json({ user: req.user, @@ -119,7 +121,7 @@ router.get('/me', requireAuth, (req, res) => { }); // GET /api/auth/login-history — last 10 logins for the authenticated user (encrypted at rest, decrypted here) -router.get('/login-history', requireAuth, (req, res) => { +router.get('/login-history', requireAuth, (req: Req, res: Res) => { const db = getDb(); const rows = db.prepare(` SELECT id, logged_in_at, ip_address, user_agent, @@ -176,7 +178,7 @@ const { encryptSecret: encTotpSecret } = require('../services/encryptionService. // POST /api/auth/totp/challenge — second step of login when TOTP is enabled. // Takes challenge_token (from first login step) + totp_code, creates a session. -router.post('/totp/challenge', async (req, res) => { +router.post('/totp/challenge', async (req: Req, res: Res) => { req.csrfSkip = true; const { challenge_token, code, recovery_code } = req.body || {}; if (!challenge_token) return res.status(400).json(standardizeError('challenge_token is required', 'VALIDATION_ERROR')); @@ -222,7 +224,7 @@ router.post('/totp/challenge', async (req, res) => { // GET /api/auth/totp/setup — generate a new pending secret + QR code for the authenticated user. // The secret is NOT saved yet; the user must confirm a valid code via /totp/enable. -router.get('/totp/setup', requireAuth, async (req, res) => { +router.get('/totp/setup', requireAuth, async (req: Req, res: Res) => { if (req.singleUserMode) return res.status(400).json(standardizeError('TOTP is not available in single-user mode.', 'VALIDATION_ERROR')); try { const secret = generateSecret(); @@ -236,7 +238,7 @@ router.get('/totp/setup', requireAuth, async (req, res) => { }); // POST /api/auth/totp/enable — verify a code against the submitted secret, then enable TOTP. -router.post('/totp/enable', requireAuth, (req, res) => { +router.post('/totp/enable', requireAuth, (req: Req, res: Res) => { if (req.singleUserMode) return res.status(400).json(standardizeError('TOTP is not available in single-user mode.', 'VALIDATION_ERROR')); const { secret, code } = req.body || {}; if (!secret || !code) return res.status(400).json(standardizeError('secret and code are required', 'VALIDATION_ERROR')); @@ -255,7 +257,7 @@ router.post('/totp/enable', requireAuth, (req, res) => { }); // POST /api/auth/totp/disable — disable TOTP. Requires a valid TOTP code or recovery code. -router.post('/totp/disable', requireAuth, (req, res) => { +router.post('/totp/disable', requireAuth, (req: Req, res: Res) => { const { code, recovery_code } = req.body || {}; const db = getDb(); const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id); @@ -276,13 +278,13 @@ router.post('/totp/disable', requireAuth, (req, res) => { }); // GET /api/auth/totp/status — is TOTP enabled for the current user? -router.get('/totp/status', requireAuth, (req, res) => { +router.get('/totp/status', requireAuth, (req: Req, res: Res) => { const user = getDb().prepare('SELECT totp_enabled FROM users WHERE id = ?').get(req.user.id); res.json({ enabled: !!user?.totp_enabled }); }); // POST /api/auth/totp/acknowledge-version — user has seen the release notes -router.post('/acknowledge-version', requireAuth, (req, res) => { +router.post('/acknowledge-version', requireAuth, (req: Req, res: Res) => { const currentVersion = getAppVersion(); getDb() .prepare("UPDATE users SET last_seen_version = ?, updated_at = datetime('now') WHERE id = ?") @@ -293,7 +295,7 @@ router.post('/acknowledge-version', requireAuth, (req, res) => { // GET /api/auth/mode // Public — tells the login page which options are available. // Never returns secrets. local_enabled/oidc_enabled reflect admin settings. -router.get('/mode', (req, res) => { +router.get('/mode', (req: Req, res: Res) => { const oidcInfo = getPublicOidcInfo(); const localEnabled = getSetting('local_login_enabled') !== 'false'; res.json({ @@ -307,7 +309,7 @@ router.get('/mode', (req, res) => { // Recovery path for single-user mode. In single-user mode requireAuth attaches // the configured default user, so this lets that Settings page restore normal // login without needing access to Admin routes. -router.post('/restore-multi-user-mode', requireAuth, (req, res) => { +router.post('/restore-multi-user-mode', requireAuth, (req: Req, res: Res) => { if (!req.singleUserMode && getSetting('auth_mode') !== 'single') { return res.status(400).json(standardizeError('Single-user mode is not enabled.', 'VALIDATION_ERROR', 'auth_mode')); } @@ -319,7 +321,7 @@ router.post('/restore-multi-user-mode', requireAuth, (req, res) => { }); // POST /api/auth/acknowledge-privacy -router.post('/acknowledge-privacy', requireAuth, (req, res) => { +router.post('/acknowledge-privacy', requireAuth, (req: Req, res: Res) => { getDb().prepare( "UPDATE users SET first_login = 0, updated_at = datetime('now') WHERE id = ?" ).run(req.user.id); @@ -330,7 +332,7 @@ router.post('/acknowledge-privacy', requireAuth, (req, res) => { // POST /api/auth/change-password // Password change endpoint with dedicated rate limiter // CSRF protected via csrfMiddleware on /api/auth mount -router.post('/change-password', passwordLimiter, requireAuth, async (req, res) => { +router.post('/change-password', passwordLimiter, requireAuth, async (req: Req, res: Res) => { const { current_password, new_password } = req.body; if (!new_password || new_password.length < 8) { @@ -379,7 +381,7 @@ router.post('/change-password', passwordLimiter, requireAuth, async (req, res) = // ───────────────────────────────────────── // GET /api/admin/has-users -router.get('/has-users', (req, res) => { +router.get('/has-users', (req: Req, res: Res) => { const count = getDb() .prepare("SELECT COUNT(*) AS n FROM users WHERE role = 'user'") .get().n; @@ -388,7 +390,7 @@ router.get('/has-users', (req, res) => { }); // GET /api/admin/users -router.get('/users', requireAuth, requireAdmin, (req, res) => { +router.get('/users', requireAuth, requireAdmin, (req: Req, res: Res) => { const users = getDb().prepare( "SELECT id, username, role, must_change_password, first_login, created_at FROM users ORDER BY role DESC, username ASC" ).all(); @@ -397,7 +399,7 @@ router.get('/users', requireAuth, requireAdmin, (req, res) => { }); // POST /api/admin/users -router.post('/users', requireAuth, requireAdmin, async (req, res) => { +router.post('/users', requireAuth, requireAdmin, async (req: Req, res: Res) => { const { username, password } = req.body; if (!username || username.length < 3) { @@ -440,7 +442,7 @@ const { } = require('../services/webauthnService.cts'); // GET /api/auth/webauthn/status -router.get('/webauthn/status', requireAuth, (req, res) => { +router.get('/webauthn/status', requireAuth, (req: Req, res: Res) => { if (req.singleUserMode) return res.json({ enabled: false, credential_count: 0 }); const db = getDb(); const user = db.prepare('SELECT webauthn_enabled FROM users WHERE id = ?').get(req.user.id); @@ -449,13 +451,13 @@ router.get('/webauthn/status', requireAuth, (req, res) => { }); // GET /api/auth/webauthn/credentials -router.get('/webauthn/credentials', requireAuth, (req, res) => { +router.get('/webauthn/credentials', requireAuth, (req: Req, res: Res) => { if (req.singleUserMode) return res.json({ credentials: [] }); res.json({ credentials: getCredentials(getDb(), req.user.id) }); }); // GET /api/auth/webauthn/setup — begin registration -router.get('/webauthn/setup', requireAuth, async (req, res) => { +router.get('/webauthn/setup', requireAuth, async (req: Req, res: Res) => { if (req.singleUserMode) return res.status(400).json(standardizeError('WebAuthn is not available in single-user mode.', 'VALIDATION_ERROR')); try { @@ -468,7 +470,7 @@ router.get('/webauthn/setup', requireAuth, async (req, res) => { }); // POST /api/auth/webauthn/enable — complete registration -router.post('/webauthn/enable', requireAuth, async (req, res) => { +router.post('/webauthn/enable', requireAuth, async (req: Req, res: Res) => { if (req.singleUserMode) return res.status(400).json(standardizeError('WebAuthn is not available in single-user mode.', 'VALIDATION_ERROR')); const { challengeId, response, credential_name } = req.body || {}; @@ -492,7 +494,7 @@ router.post('/webauthn/enable', requireAuth, async (req, res) => { }); // DELETE /api/auth/webauthn/credentials/:credentialId — remove one key -router.delete('/webauthn/credentials/:credentialId', requireAuth, async (req, res) => { +router.delete('/webauthn/credentials/:credentialId', requireAuth, async (req: Req, res: Res) => { const { password } = req.body || {}; if (!password) return res.status(400).json(standardizeError('password is required', 'VALIDATION_ERROR')); @@ -521,7 +523,7 @@ router.delete('/webauthn/credentials/:credentialId', requireAuth, async (req, re }); // POST /api/auth/webauthn/disable — remove all keys, disable WebAuthn -router.post('/webauthn/disable', requireAuth, async (req, res) => { +router.post('/webauthn/disable', requireAuth, async (req: Req, res: Res) => { const { password } = req.body || {}; if (!password) return res.status(400).json(standardizeError('password is required', 'VALIDATION_ERROR')); @@ -549,7 +551,7 @@ router.post('/webauthn/disable', requireAuth, async (req, res) => { // POST /api/auth/webauthn/challenge — second step of login when WebAuthn is enabled. // Mirrors POST /totp/challenge exactly. -router.post('/webauthn/challenge', async (req, res) => { +router.post('/webauthn/challenge', async (req: Req, res: Res) => { req.csrfSkip = true; const { challenge_token, response } = req.body || {}; if (!challenge_token || !response) diff --git a/routes/authOidc.js b/routes/authOidc.cts similarity index 94% rename from routes/authOidc.js rename to routes/authOidc.cts index 94bbc2e..23f8525 100644 --- a/routes/authOidc.js +++ b/routes/authOidc.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; 'use strict'; /** @@ -29,7 +31,7 @@ const NOT_ACTIVE = { error: 'OIDC authentication is not configured or not enable // ── GET /api/auth/oidc/login ────────────────────────────────────────────────── // Validates that OIDC is active, generates PKCE + nonce + state, stores in DB, // then redirects the user to the identity provider's authorization endpoint. -router.get('/login', async (req, res) => { +router.get('/login', async (req: Req, res: Res) => { if (!isOidcLoginActive()) return res.status(501).json(NOT_ACTIVE); const config = getOidcConfig(); if (!config) return res.status(501).json(NOT_ACTIVE); @@ -62,7 +64,7 @@ router.get('/login', async (req, res) => { // On success: creates a local session, sets cookie, redirects to frontend. // On any failure: redirects with a safe oidc_error query parameter. // Tokens are never logged or forwarded. -router.get('/callback', async (req, res) => { +router.get('/callback', async (req: Req, res: Res) => { if (!isOidcLoginActive()) return res.redirect('/?oidc_error=not_configured'); const config = getOidcConfig(); if (!config) return res.redirect('/?oidc_error=not_configured'); diff --git a/routes/bills.js b/routes/bills.cts similarity index 96% rename from routes/bills.js rename to routes/bills.cts index 78b27bc..d91f0f4 100644 --- a/routes/bills.js +++ b/routes/bills.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getDb, ensureUserDefaultCategories } = require('../db/database'); @@ -26,7 +28,7 @@ const { localDateString, todayLocal } = require('../utils/dates.mts'); const { roundMoney, sumMoney, toCents, fromCents } = require('../utils/money.mts'); // ── GET /api/bills ──────────────────────────────────────────────────────────── -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const db = getDb(); ensureUserDefaultCategories(req.user.id); const includeInactive = req.query.inactive === 'true'; @@ -54,7 +56,7 @@ router.get('/', (req, res) => { // retention GC purges them), newest deletion first. Powers the "Recently // deleted" restore view. Must be declared before GET /:id. const BILL_RETENTION_DAYS = 30; // matches pruneSoftDeletedFinancialRecords() -router.get('/deleted', (req, res) => { +router.get('/deleted', (req: Req, res: Res) => { const db = getDb(); const rows = db.prepare(` SELECT b.*, c.name AS category_name, @@ -75,7 +77,7 @@ router.get('/deleted', (req, res) => { }); // ── PUT /api/bills/reorder ─────────────────────────────────────────────────── -router.put('/reorder', (req, res) => { +router.put('/reorder', (req: Req, res: Res) => { const db = getDb(); const entries = Object.entries(req.body || {}).map(([billId, sortOrder]) => ({ billId: Number(billId), @@ -122,7 +124,7 @@ router.put('/reorder', (req, res) => { }); // ── GET /api/bills/audit?inactive=true ─────────────────────────────────────── -router.get('/audit', (req, res) => { +router.get('/audit', (req: Req, res: Res) => { const db = getDb(); ensureUserDefaultCategories(req.user.id); const includeInactive = req.query.inactive === 'true'; @@ -130,7 +132,7 @@ router.get('/audit', (req, res) => { }); // ── GET /api/bills/drift-report ────────────────────────────────────────────── -router.get('/drift-report', (req, res) => { +router.get('/drift-report', (req: Req, res: Res) => { const { getDriftReport } = require('../services/driftService.cts'); try { res.json(getDriftReport(req.user.id)); @@ -140,7 +142,7 @@ router.get('/drift-report', (req, res) => { }); // GET /api/bills/merchant-rules — all rules for this user across all bills -router.get('/merchant-rules', (req, res) => { +router.get('/merchant-rules', (req: Req, res: Res) => { try { const rules = getDb().prepare(` SELECT bmr.id, bmr.merchant, bmr.auto_attribute_late, bmr.created_at, @@ -159,7 +161,7 @@ router.get('/merchant-rules', (req, res) => { // ── POST /api/bills/:id/snooze-drift ───────────────────────────────────────── // Registered early (before /:id) but path has suffix so no conflict -router.post('/:id/snooze-drift', (req, res) => { +router.post('/:id/snooze-drift', (req: Req, res: Res) => { const db = getDb(); const id = parseInt(req.params.id, 10); if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: 'Invalid id' }); @@ -185,7 +187,7 @@ function serializeTemplateData(data) { } // ── GET /api/bills/templates ───────────────────────────────────────────────── -router.get('/templates', (req, res) => { +router.get('/templates', (req: Req, res: Res) => { const db = getDb(); const rows = db.prepare(` SELECT id, name, data, created_at, updated_at @@ -201,7 +203,7 @@ router.get('/templates', (req, res) => { }); // ── POST /api/bills/templates ──────────────────────────────────────────────── -router.post('/templates', (req, res) => { +router.post('/templates', (req: Req, res: Res) => { const db = getDb(); const name = String(req.body.name || '').trim(); if (name.length < 2) { @@ -243,7 +245,7 @@ router.post('/templates', (req, res) => { }); // ── DELETE /api/bills/templates/:templateId ────────────────────────────────── -router.delete('/templates/:templateId', (req, res) => { +router.delete('/templates/:templateId', (req: Req, res: Res) => { const db = getDb(); const templateId = parseInt(req.params.templateId, 10); if (!Number.isInteger(templateId)) { @@ -255,7 +257,7 @@ router.delete('/templates/:templateId', (req, res) => { }); // ── POST /api/bills/:id/duplicate ──────────────────────────────────────────── -router.post('/:id/duplicate', (req, res) => { +router.post('/:id/duplicate', (req: Req, res: Res) => { const db = getDb(); const body = req.body || {}; const billId = parseInt(req.params.id, 10); @@ -284,7 +286,7 @@ router.post('/:id/duplicate', (req, res) => { }); // ── GET /api/bills/:id/monthly-state?year=&month= ───────────────────────────── -router.get('/:id/monthly-state', (req, res) => { +router.get('/:id/monthly-state', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(billId, req.user.id)) @@ -312,7 +314,7 @@ router.get('/:id/monthly-state', (req, res) => { }); // ── PUT /api/bills/:id/monthly-state ────────────────────────────────────────── -router.put('/:id/monthly-state', (req, res) => { +router.put('/:id/monthly-state', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(billId, req.user.id)) @@ -378,7 +380,7 @@ router.put('/:id/monthly-state', (req, res) => { }); // ── GET /api/bills/:id ──────────────────────────────────────────────────────── -router.get('/:id', (req, res) => { +router.get('/:id', (req: Req, res: Res) => { const db = getDb(); const bill = db.prepare(` SELECT b.*, c.name AS category_name, @@ -419,7 +421,7 @@ router.get('/:id', (req, res) => { }); // ── POST /api/bills/:id/verify-autopay ─────────────────────────────────────── -router.post('/:id/verify-autopay', (req, res) => { +router.post('/:id/verify-autopay', (req: Req, res: Res) => { const db = getDb(); const id = parseInt(req.params.id, 10); if (!Number.isInteger(id) || id <= 0) { @@ -436,7 +438,7 @@ router.post('/:id/verify-autopay', (req, res) => { }); // ── POST /api/bills ─────────────────────────────────────────────────────────── -router.post('/', (req, res) => { +router.post('/', (req: Req, res: Res) => { const db = getDb(); const body = req.body || {}; let payload = body; @@ -473,7 +475,7 @@ router.post('/', (req, res) => { }); // ── PUT /api/bills/:id ──────────────────────────────────────────────────────── -router.put('/:id', (req, res) => { +router.put('/:id', (req: Req, res: Res) => { const db = getDb(); const existing = db.prepare('SELECT * FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id); if (!existing) return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id')); @@ -550,7 +552,7 @@ router.put('/:id', (req, res) => { }); // ── PUT /api/bills/:id/archived ────────────────────────────────────────────── -router.put('/:id/archived', (req, res) => { +router.put('/:id/archived', (req: Req, res: Res) => { const db = getDb(); const id = parseInt(req.params.id, 10); if (!Number.isInteger(id) || id <= 0) { @@ -568,7 +570,7 @@ router.put('/:id/archived', (req, res) => { }); // ── DELETE /api/bills/:id — soft delete for 30-day recovery ─────────────────── -router.delete('/:id', (req, res) => { +router.delete('/:id', (req: Req, res: Res) => { const db = getDb(); const bill = db.prepare('SELECT id, name FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id); if (!bill) return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id')); @@ -585,7 +587,7 @@ router.delete('/:id', (req, res) => { }); // POST /api/bills/:id/restore — undo bill soft delete -router.post('/:id/restore', (req, res) => { +router.post('/:id/restore', (req: Req, res: Res) => { const db = getDb(); const bill = db.prepare('SELECT id, name FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NOT NULL').get(req.params.id, req.user.id); if (!bill) return res.status(404).json(standardizeError('Deleted bill not found', 'NOT_FOUND', 'bill_id')); @@ -599,7 +601,7 @@ router.post('/:id/restore', (req, res) => { // POST /api/bills/:id/sync-simplefin-payments // Scan unmatched SimpleFIN transactions for this bill's merchant rules and // backfill any missing payments. -router.post('/:id/sync-simplefin-payments', (req, res) => { +router.post('/:id/sync-simplefin-payments', (req: Req, res: Res) => { const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId)) return res.status(400).json(standardizeError('Invalid bill id', 'VALIDATION_ERROR')); const db = getDb(); @@ -614,7 +616,7 @@ router.post('/:id/sync-simplefin-payments', (req, res) => { }); // ── GET /api/bills/:id/payments?page=1&limit=20 ─────────────────────────────── -router.get('/:id/payments', (req, res) => { +router.get('/:id/payments', (req: Req, res: Res) => { const db = getDb(); const bill = db.prepare('SELECT id, name FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id); if (!bill) return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id')); @@ -643,7 +645,7 @@ router.get('/:id/payments', (req, res) => { }); // ── GET /api/bills/:id/transactions ────────────────────────────────────────── -router.get('/:id/transactions', (req, res) => { +router.get('/:id/transactions', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId)) { @@ -701,7 +703,7 @@ router.get('/:id/transactions', (req, res) => { }); // ── POST /api/bills/:id/toggle-paid — toggle Paid/Unpaid status ────────────── -router.post('/:id/toggle-paid', (req, res) => { +router.post('/:id/toggle-paid', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); @@ -798,7 +800,7 @@ router.post('/:id/toggle-paid', (req, res) => { }); // ── GET /api/bills/:id/history-ranges ──────────────────────────────────────── -router.get('/:id/history-ranges', (req, res) => { +router.get('/:id/history-ranges', (req: Req, res: Res) => { const db = getDb(); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id)) return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id')); @@ -813,7 +815,7 @@ router.get('/:id/history-ranges', (req, res) => { }); // ── POST /api/bills/:id/history-ranges ─────────────────────────────────────── -router.post('/:id/history-ranges', (req, res) => { +router.post('/:id/history-ranges', (req: Req, res: Res) => { const db = getDb(); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id)) return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id')); @@ -858,7 +860,7 @@ router.post('/:id/history-ranges', (req, res) => { }); // ── PUT /api/bills/:id/history-ranges/:rangeId ─────────────────────────────── -router.put('/:id/history-ranges/:rangeId', (req, res) => { +router.put('/:id/history-ranges/:rangeId', (req: Req, res: Res) => { const db = getDb(); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id)) return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id')); @@ -902,7 +904,7 @@ router.put('/:id/history-ranges/:rangeId', (req, res) => { }); // ── DELETE /api/bills/:id/history-ranges/:rangeId ──────────────────────────── -router.delete('/:id/history-ranges/:rangeId', (req, res) => { +router.delete('/:id/history-ranges/:rangeId', (req: Req, res: Res) => { const db = getDb(); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id)) return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id')); @@ -918,7 +920,7 @@ router.delete('/:id/history-ranges/:rangeId', (req, res) => { }); // ── GET /api/bills/:id/amortization — full month-by-month schedule for a debt bill ── -router.get('/:id/amortization', (req, res) => { +router.get('/:id/amortization', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); const bill = db.prepare('SELECT * FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(billId, req.user.id); @@ -970,7 +972,7 @@ router.get('/:id/amortization', (req, res) => { }); // ── PATCH /api/bills/:id/snowball — update only snowball_include / snowball_exempt ── -router.patch('/:id/snowball', (req, res) => { +router.patch('/:id/snowball', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(billId, req.user.id)) { @@ -989,7 +991,7 @@ router.patch('/:id/snowball', (req, res) => { }); // ── PATCH /api/bills/:id/balance — lightweight balance-only update ──────────── -router.patch('/:id/balance', (req, res) => { +router.patch('/:id/balance', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(billId, req.user.id)) { @@ -1047,7 +1049,7 @@ function findConflicts(db, userId, billId, normalized) { // ── GET /api/bills/:id/merchant-rules ──────────────────────────────────────── -router.get('/:id/merchant-rules', (req, res) => { +router.get('/:id/merchant-rules', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId) || billId < 1) @@ -1085,7 +1087,7 @@ router.get('/:id/merchant-rules', (req, res) => { // ── GET /api/bills/:id/merchant-rules/preview ───────────────────────────────── -router.get('/:id/merchant-rules/preview', (req, res) => { +router.get('/:id/merchant-rules/preview', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId) || billId < 1) @@ -1106,7 +1108,7 @@ router.get('/:id/merchant-rules/preview', (req, res) => { // ── POST /api/bills/:id/merchant-rules ─────────────────────────────────────── -router.post('/:id/merchant-rules', (req, res) => { +router.post('/:id/merchant-rules', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId) || billId < 1) @@ -1146,7 +1148,7 @@ router.post('/:id/merchant-rules', (req, res) => { // All transactions matching this bill's merchant rules — any match_status. // Each item includes the current status so the user knows what will happen. -router.get('/:id/merchant-rules/candidates', (req, res) => { +router.get('/:id/merchant-rules/candidates', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId) || billId < 1) @@ -1221,7 +1223,7 @@ router.get('/:id/merchant-rules/candidates', (req, res) => { // ── POST /api/bills/:id/merchant-rules/import-historical ────────────────────── // Import a specific list of transaction IDs as payments for this bill. -router.post('/:id/merchant-rules/import-historical', (req, res) => { +router.post('/:id/merchant-rules/import-historical', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId) || billId < 1) @@ -1301,7 +1303,7 @@ router.post('/:id/merchant-rules/import-historical', (req, res) => { // PATCH /api/bills/:id/merchant-rules/:ruleId/auto-attribute // Toggle the auto_attribute_late flag for a single merchant rule. -router.patch('/:id/merchant-rules/:ruleId/auto-attribute', (req, res) => { +router.patch('/:id/merchant-rules/:ruleId/auto-attribute', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); const ruleId = parseInt(req.params.ruleId, 10); @@ -1319,7 +1321,7 @@ router.patch('/:id/merchant-rules/:ruleId/auto-attribute', (req, res) => { res.json({ id: ruleId, auto_attribute_late: enabled === 1 }); }); -router.delete('/:id/merchant-rules/:ruleId', (req, res) => { +router.delete('/:id/merchant-rules/:ruleId', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); const ruleId = parseInt(req.params.ruleId, 10); diff --git a/routes/calendar.js b/routes/calendar.cts similarity index 94% rename from routes/calendar.js rename to routes/calendar.cts index 586942f..f710350 100644 --- a/routes/calendar.js +++ b/routes/calendar.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const { standardizeError } = require('../middleware/errorFormatter'); const router = express.Router(); @@ -64,30 +66,30 @@ function tokenPayload(req, tokenRow) { } // GET /api/calendar/feed — current user's calendar feed token and URL -router.get('/feed', (req, res) => { +router.get('/feed', (req: Req, res: Res) => { res.json(tokenPayload(req, getActiveToken(req.user.id))); }); // POST /api/calendar/feed — create the feed token if one does not exist -router.post('/feed', (req, res) => { +router.post('/feed', (req: Req, res: Res) => { const token = getOrCreateToken(req.user.id); res.status(201).json(tokenPayload(req, token)); }); // POST /api/calendar/feed/regenerate — revoke old token and issue a new URL -router.post('/feed/regenerate', (req, res) => { +router.post('/feed/regenerate', (req: Req, res: Res) => { const token = regenerateToken(req.user.id); res.json(tokenPayload(req, token)); }); // DELETE /api/calendar/feed — revoke the active feed URL -router.delete('/feed', (req, res) => { +router.delete('/feed', (req: Req, res: Res) => { revokeToken(req.user.id); res.json(tokenPayload(req, null)); }); // GET /api/calendar/feed/preview — next generated events shown before subscribing -router.get('/feed/preview', (req, res) => { +router.get('/feed/preview', (req: Req, res: Res) => { const limit = Math.min(Math.max(parseInt(req.query.limit || '10', 10) || 10, 1), 50); res.json({ events: previewFeed(req.user.id, { limit }), @@ -95,7 +97,7 @@ router.get('/feed/preview', (req, res) => { }); // GET /api/calendar?year=2026&month=5 -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const db = getDb(); const now = new Date(); const year = parseInt(req.query.year || now.getFullYear(), 10); diff --git a/routes/calendarFeed.js b/routes/calendarFeed.cts similarity index 79% rename from routes/calendarFeed.js rename to routes/calendarFeed.cts index 0d81217..69b8010 100644 --- a/routes/calendarFeed.js +++ b/routes/calendarFeed.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; 'use strict'; const express = require('express'); @@ -10,7 +12,7 @@ const { // GET /api/calendar/feed.ics?token=... // Public by design: calendar clients cannot use the app's session cookies. -router.get('/feed.ics', (req, res) => { +router.get('/feed.ics', (req: Req, res: Res) => { const token = String(req.query.token || ''); const tokenRow = getTokenRecord(token); if (!tokenRow) { diff --git a/routes/categories.js b/routes/categories.cts similarity index 94% rename from routes/categories.js rename to routes/categories.cts index bd49835..49814b0 100644 --- a/routes/categories.js +++ b/routes/categories.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const { standardizeError } = require('../middleware/errorFormatter'); const router = express.Router(); @@ -5,7 +7,7 @@ const { getDb, ensureUserDefaultCategories } = require('../db/database'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); // GET /api/categories -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { try { const db = getDb(); ensureUserDefaultCategories(req.user.id); @@ -76,7 +78,7 @@ router.get('/', (req, res) => { }); // PUT /api/categories/reorder -router.put('/reorder', (req, res) => { +router.put('/reorder', (req: Req, res: Res) => { const db = getDb(); const entries = Object.entries(req.body || {}).map(([categoryId, sortOrder]) => ({ categoryId: Number(categoryId), @@ -114,7 +116,7 @@ router.put('/reorder', (req, res) => { }); // POST /api/categories -router.post('/', (req, res) => { +router.post('/', (req: Req, res: Res) => { const db = getDb(); const { name } = req.body; if (!name) return res.status(400).json(standardizeError('name is required', 'VALIDATION_ERROR', 'name')); @@ -132,7 +134,7 @@ router.post('/', (req, res) => { }); // PUT /api/categories/:id -router.put('/:id', (req, res) => { +router.put('/:id', (req: Req, res: Res) => { const db = getDb(); const { name, spending_enabled, group_id } = req.body; if (!name) return res.status(400).json(standardizeError('name is required', 'VALIDATION_ERROR', 'name')); @@ -166,7 +168,7 @@ router.put('/:id', (req, res) => { // ── Category groups ────────────────────────────────────────────────────────── // GET /api/category-groups -router.get('/groups', (req, res) => { +router.get('/groups', (req: Req, res: Res) => { const db = getDb(); const groups = db.prepare(` SELECT id, name, sort_order, created_at, updated_at @@ -178,7 +180,7 @@ router.get('/groups', (req, res) => { }); // POST /api/category-groups -router.post('/groups', (req, res) => { +router.post('/groups', (req: Req, res: Res) => { const db = getDb(); const { name } = req.body; if (!name?.trim()) return res.status(400).json(standardizeError('name is required', 'VALIDATION_ERROR', 'name')); @@ -199,7 +201,7 @@ router.post('/groups', (req, res) => { }); // PUT /api/category-groups/:id -router.put('/groups/:id', (req, res) => { +router.put('/groups/:id', (req: Req, res: Res) => { const db = getDb(); const { name, sort_order } = req.body; @@ -228,7 +230,7 @@ router.put('/groups/:id', (req, res) => { }); // DELETE /api/category-groups/:id -router.delete('/groups/:id', (req, res) => { +router.delete('/groups/:id', (req: Req, res: Res) => { const db = getDb(); const group = db.prepare('SELECT id FROM category_groups WHERE id = ? AND user_id = ?').get(req.params.id, req.user.id); if (!group) return res.status(404).json(standardizeError('Category group not found', 'NOT_FOUND', 'id')); @@ -238,7 +240,7 @@ router.delete('/groups/:id', (req, res) => { }); // PATCH /api/categories/:id/spending — toggle spending_enabled without requiring name -router.patch('/:id/spending', (req, res) => { +router.patch('/:id/spending', (req: Req, res: Res) => { const db = getDb(); const cat = db.prepare('SELECT id, spending_enabled FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id); if (!cat) return res.status(404).json(standardizeError('Category not found', 'NOT_FOUND', 'id')); @@ -255,7 +257,7 @@ router.patch('/:id/spending', (req, res) => { }); // DELETE /api/categories/:id -router.delete('/:id', (req, res) => { +router.delete('/:id', (req: Req, res: Res) => { const db = getDb(); const cat = db.prepare('SELECT id, name FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(req.params.id, req.user.id); if (!cat) return res.status(404).json(standardizeError('Category not found', 'NOT_FOUND', 'id')); @@ -273,7 +275,7 @@ router.delete('/:id', (req, res) => { }); // POST /api/categories/:id/restore — undo category soft delete -router.post('/:id/restore', (req, res) => { +router.post('/:id/restore', (req: Req, res: Res) => { const db = getDb(); const cat = db.prepare('SELECT id, name FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NOT NULL').get(req.params.id, req.user.id); if (!cat) return res.status(404).json(standardizeError('Deleted category not found', 'NOT_FOUND', 'id')); diff --git a/routes/dataSources.js b/routes/dataSources.cts similarity index 94% rename from routes/dataSources.js rename to routes/dataSources.cts index 47b5634..524476a 100644 --- a/routes/dataSources.js +++ b/routes/dataSources.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; 'use strict'; const router = require('express').Router(); @@ -26,7 +28,7 @@ function safeError(err, fallback) { // Returns all financial accounts for the user across all sources. // Used by the bank tracking account picker. -router.get('/accounts/all', (req, res) => { +router.get('/accounts/all', (req: Req, res: Res) => { try { const db = getDb(); const accounts = db.prepare(` @@ -52,7 +54,7 @@ router.get('/accounts/all', (req, res) => { // ─── GET /api/data-sources ──────────────────────────────────────────────────── -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const db = getDb(); ensureManualDataSource(db, req.user.id); @@ -90,7 +92,7 @@ router.get('/', (req, res) => { // ─── GET /api/data-sources/simplefin/status ────────────────────────────────── -router.get('/simplefin/status', (req, res) => { +router.get('/simplefin/status', (req: Req, res: Res) => { const { enabled, sync_days, seed_days } = getBankSyncConfig(); const db = getDb(); @@ -108,7 +110,7 @@ router.get('/simplefin/status', (req, res) => { // ─── POST /api/data-sources/simplefin/connect ──────────────────────────────── -router.post('/simplefin/connect', async (req, res) => { +router.post('/simplefin/connect', async (req: Req, res: Res) => { if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } @@ -130,7 +132,7 @@ router.post('/simplefin/connect', async (req, res) => { // ─── GET /api/data-sources/:sourceId/accounts ──────────────────────────────── -router.get('/:sourceId/accounts', (req, res) => { +router.get('/:sourceId/accounts', (req: Req, res: Res) => { const sourceId = parseInt(req.params.sourceId, 10); if (!Number.isInteger(sourceId) || sourceId < 1) { return res.status(400).json(standardizeError('Invalid data source id', 'VALIDATION_ERROR', 'sourceId')); @@ -180,7 +182,7 @@ router.get('/:sourceId/accounts', (req, res) => { // ─── PUT /api/data-sources/:sourceId/accounts/:accountId ───────────────────── -router.put('/:sourceId/accounts/:accountId', (req, res) => { +router.put('/:sourceId/accounts/:accountId', (req: Req, res: Res) => { const sourceId = parseInt(req.params.sourceId, 10); const accountId = parseInt(req.params.accountId, 10); if (!Number.isInteger(sourceId) || sourceId < 1 || !Number.isInteger(accountId) || accountId < 1) { @@ -209,7 +211,7 @@ router.put('/:sourceId/accounts/:accountId', (req, res) => { // ─── POST /api/data-sources/:id/sync ───────────────────────────────────────── -router.post('/:id/sync', syncLimiter, async (req, res) => { +router.post('/:id/sync', syncLimiter, async (req: Req, res: Res) => { if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } @@ -232,7 +234,7 @@ router.post('/:id/sync', syncLimiter, async (req, res) => { // ─── POST /api/data-sources/sync-all ───────────────────────────────────────── // Syncs every SimpleFIN source for the current user. Returns aggregated stats. -router.post('/sync-all', syncLimiter, async (req, res) => { +router.post('/sync-all', syncLimiter, async (req: Req, res: Res) => { if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } @@ -286,7 +288,7 @@ router.post('/sync-all', syncLimiter, async (req, res) => { // ─── POST /api/data-sources/:id/backfill ───────────────────────────────────── -router.post('/:id/backfill', syncLimiter, async (req, res) => { +router.post('/:id/backfill', syncLimiter, async (req: Req, res: Res) => { if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } @@ -308,7 +310,7 @@ router.post('/:id/backfill', syncLimiter, async (req, res) => { // ─── DELETE /api/data-sources/:id ──────────────────────────────────────────── -router.delete('/:id', (req, res) => { +router.delete('/:id', (req: Req, res: Res) => { if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } diff --git a/routes/export.js b/routes/export.cts similarity index 97% rename from routes/export.js rename to routes/export.cts index b622436..6d2babe 100644 --- a/routes/export.js +++ b/routes/export.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const { standardizeError } = require('../middleware/errorFormatter'); const router = express.Router(); @@ -10,7 +12,7 @@ const { getDb } = require('../db/database'); const { fromCents } = require('../utils/money.mts'); // GET /api/export?year=2026&format=csv — or a date range: ?from=YYYY-MM-DD&to=YYYY-MM-DD -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const db = getDb(); const format = (req.query.format || 'csv').toLowerCase(); const { from, to } = req.query; @@ -175,7 +177,7 @@ function getUserExportData(userId) { return { metadata, categories, bills, payments, monthly_bill_state: monthlyState, monthly_starting_amounts: monthlyStartingAmounts, bill_history_ranges: historyRanges, notes }; } -router.get('/user-excel', (req, res) => { +router.get('/user-excel', (req: Req, res: Res) => { const data = getUserExportData(req.user.id); const wb = xlsx.utils.book_new(); xlsx.utils.book_append_sheet(wb, xlsx.utils.json_to_sheet([data.metadata]), 'Export Metadata'); @@ -239,7 +241,7 @@ function buildUserDbExportFile(userId) { return file; } -router.get('/user-db', (req, res) => { +router.get('/user-db', (req: Req, res: Res) => { const file = buildUserDbExportFile(req.user.id); res.download(file, 'bill-tracker-user-export.sqlite', () => { try { fs.unlinkSync(file); } catch {} @@ -247,7 +249,7 @@ router.get('/user-db', (req, res) => { }); // Full portable JSON export of the user's data (same assembly as SQLite/Excel). -router.get('/user-json', (req, res) => { +router.get('/user-json', (req: Req, res: Res) => { const data = getUserExportData(req.user.id); res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Disposition', 'attachment; filename="bill-tracker-user-export.json"'); diff --git a/routes/import.js b/routes/import.cts similarity index 95% rename from routes/import.js rename to routes/import.cts index 18254ce..cb6233d 100644 --- a/routes/import.js +++ b/routes/import.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; 'use strict'; const express = require('express'); @@ -74,7 +76,7 @@ router.post( ], limit: '10mb', }), - async (req, res) => { + async (req: Req, res: Res) => { try { if (!Buffer.isBuffer(req.body) || req.body.length === 0) { return res.status(400).json({ @@ -116,7 +118,7 @@ router.post( // Each decision must have: row_id, action, and action-specific fields. // Only writes data for explicitly confirmed decisions; skips ambiguous rows. -router.post('/spreadsheet/apply', requireDataImportEnabled, express.json({ limit: '2mb' }), async (req, res) => { +router.post('/spreadsheet/apply', requireDataImportEnabled, express.json({ limit: '2mb' }), async (req: Req, res: Res) => { try { const { import_session_id, decisions, options } = req.body || {}; @@ -159,7 +161,7 @@ router.post( ], limit: '50mb', }), - async (req, res) => { + async (req: Req, res: Res) => { try { const rawFilename = req.headers['x-filename']; const originalFilename = rawFilename @@ -177,7 +179,7 @@ router.post( // Applies a previously previewed user SQLite export session. User ownership is // derived from req.user only; existing data is skipped by default. -router.post('/user-db/apply', requireDataImportEnabled, express.json({ limit: '1mb' }), async (req, res) => { +router.post('/user-db/apply', requireDataImportEnabled, express.json({ limit: '1mb' }), async (req: Req, res: Res) => { try { const { import_session_id, options } = req.body || {}; if (!import_session_id || typeof import_session_id !== 'string') { @@ -207,7 +209,7 @@ router.post( ], limit: '10mb', }), - async (req, res) => { + async (req: Req, res: Res) => { try { const rawFilename = req.headers['x-filename']; const originalFilename = rawFilename @@ -225,7 +227,7 @@ router.post( // Commits a previously-previewed CSV import session using the confirmed column // mapping. Writes normalized rows into the shared transactions table. -router.post('/csv/commit', requireDataImportEnabled, express.json({ limit: '1mb' }), async (req, res) => { +router.post('/csv/commit', requireDataImportEnabled, express.json({ limit: '1mb' }), async (req: Req, res: Res) => { try { const { import_session_id, mapping, options } = req.body || {}; if (!import_session_id || typeof import_session_id !== 'string') { @@ -258,7 +260,7 @@ router.post( ], limit: '10mb', }), - (req, res) => { + (req: Req, res: Res) => { try { const rawFilename = req.headers['x-filename']; const originalFilename = rawFilename @@ -274,7 +276,7 @@ router.post( // ─── POST /api/import/ofx/commit ───────────────────────────────────────────── // Commits a previewed OFX/QFX session (no mapping — the file is structured). -router.post('/ofx/commit', requireDataImportEnabled, express.json({ limit: '1mb' }), (req, res) => { +router.post('/ofx/commit', requireDataImportEnabled, express.json({ limit: '1mb' }), (req: Req, res: Res) => { try { const { import_session_id, options } = req.body || {}; if (!import_session_id || typeof import_session_id !== 'string') { @@ -290,7 +292,7 @@ router.post('/ofx/commit', requireDataImportEnabled, express.json({ limit: '1mb' // ─── GET /api/import/history ────────────────────────────────────────────────── // Returns the authenticated user's import history (last 100 imports). -router.get('/history', requireDataImportEnabled, (req, res) => { +router.get('/history', requireDataImportEnabled, (req: Req, res: Res) => { try { const history = getImportHistory(req.user.id); res.json({ history }); diff --git a/routes/matches.js b/routes/matches.cts similarity index 93% rename from routes/matches.js rename to routes/matches.cts index a3bdc5c..879b788 100644 --- a/routes/matches.js +++ b/routes/matches.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const router = require('express').Router(); const { standardizeError } = require('../middleware/errorFormatter'); const { getDb } = require('../db/database'); @@ -20,7 +22,7 @@ function sendMatchError(res, err, fallbackMessage = 'Match operation failed') { } // GET /api/matches/suggestions -router.get('/suggestions', (req, res) => { +router.get('/suggestions', (req: Req, res: Res) => { try { res.json(listMatchSuggestions(req.user.id, req.query)); } catch (err) { @@ -29,7 +31,7 @@ router.get('/suggestions', (req, res) => { }); // POST /api/matches/:id/reject -router.post('/:id/reject', (req, res) => { +router.post('/:id/reject', (req: Req, res: Res) => { try { res.json(rejectMatchSuggestion(req.user.id, req.params.id)); } catch (err) { @@ -38,7 +40,7 @@ router.post('/:id/reject', (req, res) => { }); // POST /api/matches/confirm — link a transaction to a bill and record a payment -router.post('/confirm', (req, res) => { +router.post('/confirm', (req: Req, res: Res) => { const txId = parseInt(req.body?.transaction_id, 10); const billId = parseInt(req.body?.bill_id, 10); if (!Number.isInteger(txId) || !Number.isInteger(billId)) { @@ -93,7 +95,7 @@ router.post('/confirm', (req, res) => { }); // POST /api/matches/:transactionId/unmatch — remove a manual match -router.post('/:transactionId/unmatch', (req, res) => { +router.post('/:transactionId/unmatch', (req: Req, res: Res) => { const txId = parseInt(req.params.transactionId, 10); if (!Number.isInteger(txId)) { return res.status(400).json(standardizeError('transactionId must be an integer', 'VALIDATION_ERROR')); diff --git a/routes/monthly-starting-amounts.js b/routes/monthly-starting-amounts.cts similarity index 95% rename from routes/monthly-starting-amounts.js rename to routes/monthly-starting-amounts.cts index 56ae44e..b084d7a 100644 --- a/routes/monthly-starting-amounts.js +++ b/routes/monthly-starting-amounts.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getDb } = require('../db/database'); @@ -121,7 +123,7 @@ function buildStartingAmountsResponse(db, userId, year, month) { }; } -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const parsed = parseYearMonth(req.query); if (parsed.error) return res.status(400).json({ error: parsed.error }); @@ -129,7 +131,7 @@ router.get('/', (req, res) => { res.json(buildStartingAmountsResponse(db, req.user.id, parsed.year, parsed.month)); }); -router.put('/', (req, res) => { +router.put('/', (req: Req, res: Res) => { const parsed = parseYearMonth(req.body || {}); if (parsed.error) return res.status(400).json({ error: parsed.error }); diff --git a/routes/notifications.js b/routes/notifications.cts similarity index 90% rename from routes/notifications.js rename to routes/notifications.cts index a15b3f7..e1c63a4 100644 --- a/routes/notifications.js +++ b/routes/notifications.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getDb, getSetting, setSetting } = require('../db/database'); @@ -10,7 +12,7 @@ const { encryptSecret } = require('../services/encryptionService.cts'); // ── Admin: SMTP configuration ───────────────────────────────────────────────── // GET /api/notifications/admin — read all SMTP settings (password masked) -router.get('/admin', requireAuth, requireAdmin, (req, res) => { +router.get('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => { const keys = [ 'notify_smtp_enabled', 'notify_sender_name', 'notify_sender_address', 'notify_smtp_host', 'notify_smtp_port', 'notify_smtp_encryption', @@ -26,7 +28,7 @@ router.get('/admin', requireAuth, requireAdmin, (req, res) => { }); // PUT /api/notifications/admin — save SMTP settings -router.put('/admin', requireAuth, requireAdmin, (req, res) => { +router.put('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => { const allowed = [ 'notify_smtp_enabled', 'notify_sender_name', 'notify_sender_address', 'notify_smtp_host', 'notify_smtp_port', 'notify_smtp_encryption', @@ -55,7 +57,7 @@ router.put('/admin', requireAuth, requireAdmin, (req, res) => { }); // POST /api/notifications/test — send a test email -router.post('/test', requireAuth, requireAdmin, async (req, res) => { +router.post('/test', requireAuth, requireAdmin, async (req: Req, res: Res) => { const { to } = req.body; if (!to) return res.status(400).json({ error: 'Recipient address required' }); try { @@ -69,7 +71,7 @@ router.post('/test', requireAuth, requireAdmin, async (req, res) => { // ── User: notification preferences ─────────────────────────────────────────── // GET /api/notifications/me — user prefs + whether admin has enabled it -router.get('/me', requireAuth, requireUser, (req, res) => { +router.get('/me', requireAuth, requireUser, (req: Req, res: Res) => { const db = getDb(); const user = db.prepare(` SELECT notification_email, notifications_enabled, @@ -91,7 +93,7 @@ router.get('/me', requireAuth, requireUser, (req, res) => { }); // PUT /api/notifications/me — save user prefs -router.put('/me', requireAuth, requireUser, (req, res) => { +router.put('/me', requireAuth, requireUser, (req: Req, res: Res) => { const db = getDb(); const { notification_email, notifications_enabled, @@ -124,7 +126,7 @@ router.put('/me', requireAuth, requireUser, (req, res) => { }); // POST /api/notifications/test-push — send a test push notification to the current user -router.post('/test-push', requireAuth, requireUser, async (req, res) => { +router.post('/test-push', requireAuth, requireUser, async (req: Req, res: Res) => { const db = getDb(); const user = db.prepare(` SELECT notify_push_enabled, push_channel, push_url, push_token, push_chat_id diff --git a/routes/payments.js b/routes/payments.cts similarity index 97% rename from routes/payments.js rename to routes/payments.cts index 92cca7b..fae1abc 100644 --- a/routes/payments.js +++ b/routes/payments.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const { standardizeError } = require('../middleware/errorFormatter'); const router = require('express').Router(); @@ -73,7 +75,7 @@ function getAutopaySuggestionContext(db, userId, billId, year, month) { } // GET /api/payments?bill_id=&year=&month= -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const db = getDb(); const { bill_id, year, month } = req.query; @@ -113,7 +115,7 @@ router.get('/', (req, res) => { }); // GET /api/payments/recent-auto — provider_sync payments with a linked tx, last 7 days -router.get('/recent-auto', (req, res) => { +router.get('/recent-auto', (req: Req, res: Res) => { const db = getDb(); const rows = db.prepare(` SELECT p.id, p.bill_id, p.amount, p.paid_date, p.payment_source, @@ -136,7 +138,7 @@ router.get('/recent-auto', (req, res) => { }); // GET /api/payments/:id -router.get('/:id', (req, res) => { +router.get('/:id', (req: Req, res: Res) => { const db = getDb(); const payment = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); if (!payment) return res.status(404).json(standardizeError('Payment not found', 'NOT_FOUND', 'id')); @@ -144,7 +146,7 @@ router.get('/:id', (req, res) => { }); // POST /api/payments/:id/undo-auto — reverse a provider_sync auto-match -router.post('/:id/undo-auto', (req, res) => { +router.post('/:id/undo-auto', (req: Req, res: Res) => { const db = getDb(); const payment = db.prepare(` SELECT p.* FROM payments p @@ -188,7 +190,7 @@ router.post('/:id/undo-auto', (req, res) => { }); // POST /api/payments — create single payment -router.post('/', (req, res) => { +router.post('/', (req: Req, res: Res) => { const db = getDb(); const { bill_id, amount, paid_date, method, notes, payment_source, autopay_failure, allow_duplicate } = req.body; @@ -240,7 +242,7 @@ router.post('/', (req, res) => { }); // POST /api/payments/quick — pay a bill (expected amount, today) -router.post('/quick', (req, res) => { +router.post('/quick', (req: Req, res: Res) => { const db = getDb(); const { bill_id, amount, paid_date, method, notes, payment_source } = req.body; @@ -296,7 +298,7 @@ router.post('/quick', (req, res) => { }); // POST /api/payments/autopay-suggestions/:billId/confirm -router.post('/autopay-suggestions/:billId/confirm', (req, res) => { +router.post('/autopay-suggestions/:billId/confirm', (req: Req, res: Res) => { const db = getDb(); const ym = parseYearMonth(req.body); if (ym.error) return res.status(400).json(ym.error); @@ -369,7 +371,7 @@ router.post('/autopay-suggestions/:billId/confirm', (req, res) => { }); // POST /api/payments/autopay-suggestions/:billId/dismiss -router.post('/autopay-suggestions/:billId/dismiss', (req, res) => { +router.post('/autopay-suggestions/:billId/dismiss', (req: Req, res: Res) => { const db = getDb(); const ym = parseYearMonth(req.body); if (ym.error) return res.status(400).json(ym.error); @@ -400,7 +402,7 @@ router.post('/autopay-suggestions/:billId/dismiss', (req, res) => { // - Each item requires: bill_id (integer), paid_date (valid date), amount (positive number) // - Duplicate payments (same bill_id + paid_date + amount) are skipped, not created // - Returns { created: [...], skipped: [...], errors: [...] } -router.post('/bulk', (req, res) => { +router.post('/bulk', (req: Req, res: Res) => { const db = getDb(); const { payments } = req.body; @@ -477,7 +479,7 @@ router.post('/bulk', (req, res) => { }); // PUT /api/payments/:id -router.put('/:id', (req, res) => { +router.put('/:id', (req: Req, res: Res) => { const db = getDb(); const existing = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); if (!existing) return res.status(404).json(standardizeError('Payment not found', 'NOT_FOUND', 'id')); @@ -562,7 +564,7 @@ router.put('/:id', (req, res) => { }); // DELETE /api/payments/:id — soft delete (sets deleted_at) -router.delete('/:id', (req, res) => { +router.delete('/:id', (req: Req, res: Res) => { const db = getDb(); const payment = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); if (!payment) return res.status(404).json(standardizeError('Payment not found', 'NOT_FOUND', 'id')); @@ -593,7 +595,7 @@ router.delete('/:id', (req, res) => { }); // POST /api/payments/:id/restore — undo soft delete -router.post('/:id/restore', (req, res) => { +router.post('/:id/restore', (req: Req, res: Res) => { const db = getDb(); const payment = db.prepare('SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.deleted_at IS NOT NULL AND b.user_id = ? AND b.deleted_at IS NULL').get(req.params.id, req.user.id); if (!payment) return res.status(404).json(standardizeError('Deleted payment not found', 'NOT_FOUND', 'id')); @@ -629,7 +631,7 @@ router.post('/:id/restore', (req, res) => { // Changes only the paid_date of a provider_sync payment to move it into the // correct billing period when it posted just after month end. // Does not touch the amount or balance_delta. -router.patch('/:id/attribute-to-month', (req, res) => { +router.patch('/:id/attribute-to-month', (req: Req, res: Res) => { const db = getDb(); const paymentId = parseInt(req.params.id, 10); if (!Number.isInteger(paymentId) || paymentId < 1) { diff --git a/routes/privacy.js b/routes/privacy.cts similarity index 93% rename from routes/privacy.js rename to routes/privacy.cts index 5c71ee2..24a21d6 100644 --- a/routes/privacy.js +++ b/routes/privacy.cts @@ -1,10 +1,12 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); let pkg; try { pkg = require('../package.json'); } catch { pkg = { version: '0.1.0' }; } -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { res.json({ name: 'Privacy', version: pkg.version, diff --git a/routes/profile.js b/routes/profile.cts similarity index 96% rename from routes/profile.js rename to routes/profile.cts index bf43641..b7bacae 100644 --- a/routes/profile.js +++ b/routes/profile.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; 'use strict'; const express = require('express'); @@ -47,7 +49,7 @@ function profileResponse(user) { // ── GET /api/profile ────────────────────────────────────────────────────────── // Returns safe profile data for the signed-in user. // Never returns password_hash, session tokens, or secrets. -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const db = getDb(); const user = db.prepare(` SELECT id, username, display_name, role, active, is_default_admin, @@ -81,7 +83,7 @@ router.get('/', (req, res) => { // ── PATCH /api/profile ──────────────────────────────────────────────────────── // Updates safe profile fields: username and display_name. // Ignores any unknown or restricted fields. -router.patch('/', (req, res) => { +router.patch('/', (req: Req, res: Res) => { const { username } = req.body; const displayNameInput = req.body.display_name !== undefined ? req.body.display_name @@ -142,7 +144,7 @@ router.patch('/', (req, res) => { // ── GET /api/profile/settings ───────────────────────────────────────────────── // Returns user-owned notification preferences from the users table. // Does not return admin/global/SMTP settings. -router.get('/settings', (req, res) => { +router.get('/settings', (req: Req, res: Res) => { const db = getDb(); const user = db.prepare(` SELECT notification_email, notifications_enabled, @@ -179,7 +181,7 @@ router.get('/settings', (req, res) => { // Cannot modify global/admin/SMTP settings through this endpoint. const VALID_PUSH_CHANNELS = new Set(['ntfy', 'gotify', 'discord', 'telegram']); -router.patch('/settings', (req, res) => { +router.patch('/settings', (req: Req, res: Res) => { const db = getDb(); const { notification_email, email, notifications_enabled, @@ -269,7 +271,7 @@ router.patch('/settings', (req, res) => { // Always requires: current_password, new_password, confirm_new_password. // Never bypasses current_password verification regardless of must_change_password. // Never accepts user_id from the request body. -router.post('/change-password', passwordLimiter, async (req, res) => { +router.post('/change-password', passwordLimiter, async (req: Req, res: Res) => { const { current_password, new_password, confirm_new_password } = req.body; if (!current_password) { @@ -332,7 +334,7 @@ router.post('/change-password', passwordLimiter, async (req, res) => { // ── GET /api/profile/exports ────────────────────────────────────────────────── // Returns metadata about available user export capabilities. // Does not trigger any download; links point to the actual export endpoints. -router.get('/exports', (req, res) => { +router.get('/exports', (req: Req, res: Res) => { res.json({ exports: [ { @@ -356,7 +358,7 @@ router.get('/exports', (req, res) => { // ── GET /api/profile/import-history ────────────────────────────────────────── // Returns the signed-in user's import history. // Delegates to the same service as GET /api/import/history. -router.get('/import-history', requireDataImportEnabled, (req, res) => { +router.get('/import-history', requireDataImportEnabled, (req: Req, res: Res) => { try { const history = getImportHistory(req.user.id); res.json({ history }); diff --git a/routes/settings.js b/routes/settings.cts similarity index 61% rename from routes/settings.js rename to routes/settings.cts index f476dac..9631dd1 100644 --- a/routes/settings.js +++ b/routes/settings.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; 'use strict'; const express = require('express'); @@ -5,12 +7,12 @@ const router = express.Router(); const { getUserSettings, setUserSettings } = require('../services/userSettings.cts'); // GET /api/settings — returns only user-facing app preferences -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { res.json(getUserSettings(req.user.id)); }); // PUT /api/settings — updates only allowed user-facing keys for this user -router.put('/', (req, res) => { +router.put('/', (req: Req, res: Res) => { res.json(setUserSettings(req.user.id, req.body)); }); diff --git a/routes/snowball.js b/routes/snowball.cts similarity index 95% rename from routes/snowball.js rename to routes/snowball.cts index 89626bd..71cf080 100644 --- a/routes/snowball.js +++ b/routes/snowball.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getDb } = require('../db/database'); @@ -84,13 +86,13 @@ function getDebtBills(userId, ramseyMode) { } // GET /api/snowball — server-filtered debt bills, pre-sorted by snowball_order -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const ramseyMode = isRamseyMode(req.user.id); res.json(getDebtBills(req.user.id, ramseyMode).map(serializeBill)); }); // GET /api/snowball/settings — extra monthly payment for this user -router.get('/settings', (req, res) => { +router.get('/settings', (req: Req, res: Res) => { const db = getDb(); const user = db.prepare('SELECT snowball_extra_payment FROM users WHERE id = ?').get(req.user.id); res.json({ @@ -102,7 +104,7 @@ router.get('/settings', (req, res) => { }); // PATCH /api/snowball/settings — save extra monthly payment -router.patch('/settings', (req, res) => { +router.patch('/settings', (req: Req, res: Res) => { const { extra_payment, ramsey_mode, ready_current_on_bills, ready_emergency_fund } = req.body; let val = 0; @@ -149,7 +151,7 @@ router.patch('/settings', (req, res) => { // GET /api/snowball/projection — snowball, avalanche, minimum-only projections // Each debt result is enriched with current APR metrics (monthly interest, etc.) -router.get('/projection', (req, res) => { +router.get('/projection', (req: Req, res: Res) => { const db = getDb(); const ramseyMode = isRamseyMode(req.user.id); @@ -227,7 +229,7 @@ function buildComparison(snowball, minimum_only) { } // PATCH /api/snowball/order — batch-save snowball_order positions -router.patch('/order', (req, res) => { +router.patch('/order', (req: Req, res: Res) => { const items = req.body; if (!Array.isArray(items)) { return res.status(400).json(standardizeError('Request body must be an array', 'VALIDATION_ERROR')); @@ -308,7 +310,7 @@ function enrichPlanWithProgress(db, plan) { } // POST /api/snowball/plans — start a new snowball plan -router.post('/plans', (req, res) => { +router.post('/plans', (req: Req, res: Res) => { try { const db = getDb(); const userId = req.user.id; @@ -385,7 +387,7 @@ router.post('/plans', (req, res) => { }); // GET /api/snowball/plans — list all plans for user -router.get('/plans', (req, res) => { +router.get('/plans', (req: Req, res: Res) => { try { const db = getDb(); const plans = db.prepare(` @@ -399,7 +401,7 @@ router.get('/plans', (req, res) => { }); // GET /api/snowball/plans/active — return the active or paused plan (or null) -router.get('/plans/active', (req, res) => { +router.get('/plans/active', (req: Req, res: Res) => { try { const db = getDb(); const plan = db.prepare(` @@ -415,7 +417,7 @@ router.get('/plans/active', (req, res) => { }); // PATCH /api/snowball/plans/:id — update name or notes -router.patch('/plans/:id', (req, res) => { +router.patch('/plans/:id', (req: Req, res: Res) => { try { const db = getDb(); const id = parseInt(req.params.id, 10); @@ -465,13 +467,13 @@ function transitionPlan(req, res, { allowedFrom, setSql, action, past }) { } // POST /api/snowball/plans/:id/{pause,resume,complete,abandon} -router.post('/plans/:id/pause', (req, res) => +router.post('/plans/:id/pause', (req: Req, res: Res) => transitionPlan(req, res, { allowedFrom: ['active'], setSql: "status = 'paused', paused_at = datetime('now')", action: 'pause', past: 'paused' })); -router.post('/plans/:id/resume', (req, res) => +router.post('/plans/:id/resume', (req: Req, res: Res) => transitionPlan(req, res, { allowedFrom: ['paused'], setSql: "status = 'active', paused_at = NULL", action: 'resume', past: 'resumed' })); -router.post('/plans/:id/complete', (req, res) => +router.post('/plans/:id/complete', (req: Req, res: Res) => transitionPlan(req, res, { allowedFrom: ['active', 'paused'], setSql: "status = 'completed', completed_at = datetime('now')", action: 'complete', past: 'completed' })); -router.post('/plans/:id/abandon', (req, res) => +router.post('/plans/:id/abandon', (req: Req, res: Res) => transitionPlan(req, res, { allowedFrom: ['active', 'paused'], setSql: "status = 'abandoned'", action: 'abandon', past: 'abandoned' })); module.exports = router; diff --git a/routes/spending.js b/routes/spending.cts similarity index 90% rename from routes/spending.js rename to routes/spending.cts index 2ccf1bc..281b891 100644 --- a/routes/spending.js +++ b/routes/spending.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; 'use strict'; const express = require('express'); @@ -20,7 +22,7 @@ function parseYM(source) { } // GET /api/spending/summary?year=&month= -router.get('/summary', (req, res) => { +router.get('/summary', (req: Req, res: Res) => { const ym = parseYM(req.query); if (ym.error) return res.status(400).json({ error: ym.error }); try { @@ -32,7 +34,7 @@ router.get('/summary', (req, res) => { }); // GET /api/spending/transactions?year=&month=&category_id=&page=&limit= -router.get('/transactions', (req, res) => { +router.get('/transactions', (req: Req, res: Res) => { const ym = parseYM(req.query); if (ym.error) return res.status(400).json({ error: ym.error }); @@ -55,7 +57,7 @@ router.get('/transactions', (req, res) => { }); // PATCH /api/spending/transactions/:id/category -router.patch('/transactions/:id/category', (req, res) => { +router.patch('/transactions/:id/category', (req: Req, res: Res) => { const txId = parseInt(req.params.id, 10); if (isNaN(txId)) return res.status(400).json({ error: 'Invalid transaction ID' }); @@ -71,7 +73,7 @@ router.patch('/transactions/:id/category', (req, res) => { }); // GET /api/spending/budgets?year=&month= -router.get('/budgets', (req, res) => { +router.get('/budgets', (req: Req, res: Res) => { const ym = parseYM(req.query); if (ym.error) return res.status(400).json({ error: ym.error }); try { @@ -83,7 +85,7 @@ router.get('/budgets', (req, res) => { }); // POST /api/spending/budgets/copy — copy all budgets from previous month into target month -router.post('/budgets/copy', (req, res) => { +router.post('/budgets/copy', (req: Req, res: Res) => { const ym = parseYM(req.body || {}); if (ym.error) return res.status(400).json({ error: ym.error }); @@ -122,7 +124,7 @@ router.post('/budgets/copy', (req, res) => { }); // PUT /api/spending/budgets — { category_id, year, month, amount } -router.put('/budgets', (req, res) => { +router.put('/budgets', (req: Req, res: Res) => { const { category_id, year, month, amount } = req.body || {}; if (!category_id) return res.status(400).json({ error: 'category_id required' }); const ym = parseYM({ year, month }); @@ -138,7 +140,7 @@ router.put('/budgets', (req, res) => { }); // GET /api/spending/category-rules -router.get('/category-rules', (req, res) => { +router.get('/category-rules', (req: Req, res: Res) => { try { res.json({ rules: getSpendingCategoryRules(getDb(), req.user.id) }); } catch (err) { @@ -148,7 +150,7 @@ router.get('/category-rules', (req, res) => { }); // POST /api/spending/category-rules — { category_id, merchant } -router.post('/category-rules', (req, res) => { +router.post('/category-rules', (req: Req, res: Res) => { const { category_id, merchant } = req.body || {}; if (!category_id || !merchant) return res.status(400).json({ error: 'category_id and merchant required' }); try { @@ -161,7 +163,7 @@ router.post('/category-rules', (req, res) => { }); // DELETE /api/spending/category-rules/:id -router.delete('/category-rules/:id', (req, res) => { +router.delete('/category-rules/:id', (req: Req, res: Res) => { try { deleteSpendingCategoryRule(getDb(), req.user.id, parseInt(req.params.id, 10)); res.json({ ok: true }); @@ -172,7 +174,7 @@ router.delete('/category-rules/:id', (req, res) => { }); // GET /api/spending/income?year=&month=&page= -router.get('/income', (req, res) => { +router.get('/income', (req: Req, res: Res) => { const ym = parseYM(req.query); if (ym.error) return res.status(400).json({ error: ym.error }); try { diff --git a/routes/status.js b/routes/status.cts similarity index 98% rename from routes/status.js rename to routes/status.cts index cc40cd0..65dcc12 100644 --- a/routes/status.js +++ b/routes/status.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const fs = require('fs'); @@ -48,7 +50,7 @@ function monthRange(now) { } // GET /api/status -router.get('/', async (req, res) => { +router.get('/', async (req: Req, res: Res) => { const runtimeState = getStatusRuntime(); const now = new Date(); const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000); diff --git a/routes/subscriptions.js b/routes/subscriptions.cts similarity index 94% rename from routes/subscriptions.js rename to routes/subscriptions.cts index 807a67b..7fc84b5 100644 --- a/routes/subscriptions.js +++ b/routes/subscriptions.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getDb, ensureUserDefaultCategories } = require('../db/database'); @@ -16,7 +18,7 @@ const { } = require('../services/subscriptionService.cts'); const { addMerchantRule, applyMerchantRules } = require('../services/billMerchantRuleService.cts'); -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const db = getDb(); ensureUserDefaultCategories(req.user.id); const subscriptions = getSubscriptions(db, req.user.id); @@ -26,14 +28,14 @@ router.get('/', (req, res) => { }); }); -router.get('/recommendations', (req, res) => { +router.get('/recommendations', (req: Req, res: Res) => { const db = getDb(); res.json({ recommendations: getSubscriptionRecommendations(db, req.user.id), }); }); -router.get('/transaction-matches', (req, res) => { +router.get('/transaction-matches', (req: Req, res: Res) => { try { res.json({ transactions: searchSubscriptionTransactions(getDb(), req.user.id, req.query), @@ -43,7 +45,7 @@ router.get('/transaction-matches', (req, res) => { } }); -router.post('/recommendations/decline', (req, res) => { +router.post('/recommendations/decline', (req: Req, res: Res) => { const { decline_key } = req.body || {}; if (!decline_key || typeof decline_key !== 'string' || decline_key.length > 200) { return res.status(400).json(standardizeError('decline_key is required', 'VALIDATION_ERROR', 'decline_key')); @@ -66,7 +68,7 @@ router.post('/recommendations/decline', (req, res) => { // POST /api/subscriptions/recommendations/match-bill // Link an existing bill to all transactions in a recommendation (no new bill created). -router.post('/recommendations/match-bill', (req, res) => { +router.post('/recommendations/match-bill', (req: Req, res: Res) => { const billId = parseInt(req.body?.bill_id, 10); const rawIds = Array.isArray(req.body?.transaction_ids) ? req.body.transaction_ids : []; const txIds = rawIds.map(id => parseInt(id, 10)).filter(n => Number.isInteger(n) && n > 0).slice(0, 50); @@ -136,7 +138,7 @@ router.post('/recommendations/match-bill', (req, res) => { res.json({ ok: true, matched_count: matchedCount + autoMatched, bill_name: bill.name }); }); -router.post('/recommendations/create', (req, res) => { +router.post('/recommendations/create', (req: Req, res: Res) => { const db = getDb(); ensureUserDefaultCategories(req.user.id); if (req.body?.category_id) { @@ -168,7 +170,7 @@ router.post('/recommendations/create', (req, res) => { // ── Catalog browser ─────────────────────────────────────────────────────────── -router.get('/catalog', (req, res) => { +router.get('/catalog', (req: Req, res: Res) => { const db = getDb(); try { const catalogEntries = db.prepare(` @@ -229,7 +231,7 @@ router.get('/catalog', (req, res) => { }); // Update which catalog entry a bill is linked to (or unlink with null) -router.put('/:id/catalog-link', (req, res) => { +router.put('/:id/catalog-link', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId) || billId < 1) { @@ -275,7 +277,7 @@ router.put('/:id/catalog-link', (req, res) => { }); // Add a custom bank descriptor for a catalog entry (per-user) -router.post('/catalog/:catalogId/descriptors', (req, res) => { +router.post('/catalog/:catalogId/descriptors', (req: Req, res: Res) => { const db = getDb(); const catalogId = parseInt(req.params.catalogId, 10); if (!Number.isInteger(catalogId) || catalogId < 1) { @@ -319,7 +321,7 @@ router.post('/catalog/:catalogId/descriptors', (req, res) => { }); // Delete a user-added catalog descriptor -router.delete('/catalog/descriptors/:id', (req, res) => { +router.delete('/catalog/descriptors/:id', (req: Req, res: Res) => { const db = getDb(); const descriptorId = parseInt(req.params.id, 10); if (!Number.isInteger(descriptorId) || descriptorId < 1) { @@ -350,7 +352,7 @@ router.delete('/catalog/descriptors/:id', (req, res) => { } }); -router.patch('/:id', (req, res) => { +router.patch('/:id', (req: Req, res: Res) => { const db = getDb(); const billId = parseInt(req.params.id, 10); if (!Number.isInteger(billId)) { diff --git a/routes/summary.js b/routes/summary.cts similarity index 98% rename from routes/summary.js rename to routes/summary.cts index 6040784..489d6da 100644 --- a/routes/summary.js +++ b/routes/summary.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getDb } = require('../db/database'); @@ -392,7 +394,7 @@ function buildSummary(db, userId, year, month) { }; } -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const parsed = parseYearMonth(req.query); if (parsed.error) return res.status(400).json({ error: parsed.error }); @@ -400,7 +402,7 @@ router.get('/', (req, res) => { res.json(buildSummary(db, req.user.id, parsed.year, parsed.month)); }); -router.put('/income', (req, res) => { +router.put('/income', (req: Req, res: Res) => { const parsed = parseYearMonth(req.body || {}); if (parsed.error) return res.status(400).json({ error: parsed.error }); diff --git a/routes/tracker.js b/routes/tracker.cts similarity index 79% rename from routes/tracker.js rename to routes/tracker.cts index 36231a7..b94f741 100644 --- a/routes/tracker.js +++ b/routes/tracker.cts @@ -1,10 +1,12 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const { getTracker, getUpcomingBills, getOverdueCount } = require('../services/trackerService.cts'); const { standardizeError } = require('../middleware/errorFormatter'); // GET /api/tracker/overdue-count — lightweight count for sidebar badge -router.get('/overdue-count', (req, res) => { +router.get('/overdue-count', (req: Req, res: Res) => { try { res.json(getOverdueCount(req.user.id)); } catch (err) { @@ -14,7 +16,7 @@ router.get('/overdue-count', (req, res) => { }); // GET /api/tracker?year=2026&month=5 -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { try { const result = getTracker(req.user.id, req.query); if (result.error) { @@ -28,7 +30,7 @@ router.get('/', (req, res) => { }); // GET /api/tracker/upcoming?days=30 -router.get('/upcoming', (req, res) => { +router.get('/upcoming', (req: Req, res: Res) => { try { res.json(getUpcomingBills(req.user.id, req.query)); } catch (err) { diff --git a/routes/transactions.js b/routes/transactions.cts similarity index 97% rename from routes/transactions.js rename to routes/transactions.cts index cc658b4..65ffbec 100644 --- a/routes/transactions.js +++ b/routes/transactions.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const router = require('express').Router(); const { getDb } = require('../db/database'); const { standardizeError } = require('../middleware/errorFormatter'); @@ -358,7 +360,7 @@ function bankLedgerWhere(query, userId) { } // GET /api/transactions -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const db = getDb(); ensureManualDataSource(db, req.user.id); @@ -486,7 +488,7 @@ router.get('/', (req, res) => { }); // GET /api/transactions/bank-ledger -router.get('/bank-ledger', (req, res) => { +router.get('/bank-ledger', (req: Req, res: Res) => { const config = getBankSyncConfig(); const page = parseLimitOffset(req.query); if (page.error) return res.status(400).json(page.error); @@ -609,7 +611,7 @@ router.get('/bank-ledger', (req, res) => { }); // POST /api/transactions/manual -router.post('/manual', (req, res) => { +router.post('/manual', (req: Req, res: Res) => { const db = getDb(); const directMatchState = rejectDirectMatchState(req.body); if (directMatchState) return res.status(400).json(directMatchState); @@ -650,7 +652,7 @@ router.post('/manual', (req, res) => { }); // PUT /api/transactions/:id -router.put('/:id', (req, res) => { +router.put('/:id', (req: Req, res: Res) => { const db = getDb(); const directMatchState = rejectDirectMatchState(req.body); if (directMatchState) return res.status(400).json(directMatchState); @@ -704,7 +706,7 @@ router.put('/:id', (req, res) => { }); // DELETE /api/transactions/:id -router.delete('/:id', (req, res) => { +router.delete('/:id', (req: Req, res: Res) => { const db = getDb(); const id = parseInteger(req.params.id, 'id'); if (id.error) return res.status(400).json(id.error); @@ -721,7 +723,7 @@ router.delete('/:id', (req, res) => { }); // POST /api/transactions/:id/match -router.post('/:id/match', (req, res) => { +router.post('/:id/match', (req: Req, res: Res) => { try { const result = matchTransactionToBill( req.user.id, @@ -736,7 +738,7 @@ router.post('/:id/match', (req, res) => { }); // POST /api/transactions/:id/unmatch -router.post('/:id/unmatch', (req, res) => { +router.post('/:id/unmatch', (req: Req, res: Res) => { try { const result = unmatchTransaction(req.user.id, req.params.id); res.json(result); @@ -749,7 +751,7 @@ router.post('/:id/unmatch', (req, res) => { // Body: { matches: [{ transaction_id, payment_id, payment_source }] } // Handles both provider_sync (full undo: balance restore + delete payment) and // transaction_match (standard service unmatch) in one call. -router.post('/unmatch-bulk', (req, res) => { +router.post('/unmatch-bulk', (req: Req, res: Res) => { const matches = req.body?.matches; if (!Array.isArray(matches) || matches.length === 0) { return res.status(400).json(standardizeError('matches array required', 'VALIDATION_ERROR')); @@ -816,7 +818,7 @@ router.post('/unmatch-bulk', (req, res) => { }); // POST /api/transactions/:id/ignore -router.post('/:id/ignore', (req, res) => { +router.post('/:id/ignore', (req: Req, res: Res) => { try { const result = ignoreTransaction(req.user.id, req.params.id); res.json(result.transaction); @@ -826,7 +828,7 @@ router.post('/:id/ignore', (req, res) => { }); // POST /api/transactions/:id/unignore -router.post('/:id/unignore', (req, res) => { +router.post('/:id/unignore', (req: Req, res: Res) => { try { const result = unignoreTransaction(req.user.id, req.params.id); res.json(result.transaction); @@ -836,7 +838,7 @@ router.post('/:id/unignore', (req, res) => { }); // GET /api/transactions/:id/merchant-match -router.get('/:id/merchant-match', (req, res) => { +router.get('/:id/merchant-match', (req: Req, res: Res) => { try { const db = getDb(); const id = parseInteger(req.params.id, 'id'); @@ -853,7 +855,7 @@ router.get('/:id/merchant-match', (req, res) => { }); // POST /api/transactions/:id/apply-merchant-match -router.post('/:id/apply-merchant-match', (req, res) => { +router.post('/:id/apply-merchant-match', (req: Req, res: Res) => { try { const db = getDb(); const id = parseInteger(req.params.id, 'id'); @@ -879,7 +881,7 @@ router.post('/:id/apply-merchant-match', (req, res) => { }); // POST /api/transactions/auto-categorize -router.post('/auto-categorize', (req, res) => { +router.post('/auto-categorize', (req: Req, res: Res) => { try { const db = getDb(); const result = applyMerchantStoreMatches(db, req.user.id, { dryRun: Boolean(req.body?.dry_run) }); diff --git a/routes/user.js b/routes/user.cts similarity index 90% rename from routes/user.js rename to routes/user.cts index b0e6305..0470ef5 100644 --- a/routes/user.js +++ b/routes/user.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; 'use strict'; const express = require('express'); @@ -9,7 +11,7 @@ const { eraseUserData } = require('../services/userDataService.cts'); const { standardizeError } = require('../middleware/errorFormatter'); // GET /api/user/seeded-status — returns whether the current user has any seeded data -router.get('/seeded-status', (req, res) => { +router.get('/seeded-status', (req: Req, res: Res) => { try { const db = getDb(); const userId = req.user.id; @@ -36,7 +38,7 @@ router.get('/seeded-status', (req, res) => { }); // POST /api/user/clear-demo-data — removes all seeded bills and categories for the requesting user -router.post('/clear-demo-data', demoDataLimiter, (req, res) => { +router.post('/clear-demo-data', demoDataLimiter, (req: Req, res: Res) => { try { const db = getDb(); const userId = req.user.id; @@ -67,7 +69,7 @@ router.post('/clear-demo-data', demoDataLimiter, (req, res) => { }); // POST /api/user/seed-demo-data — seeds demo bills for the requesting user -router.post('/seed-demo-data', (req, res) => { +router.post('/seed-demo-data', (req: Req, res: Res) => { try { const result = seedDemoData(req.user.id); res.json({ @@ -85,7 +87,7 @@ router.post('/seed-demo-data', (req, res) => { // POST /api/user/erase-data — permanently wipe the requesting user's financial + // imported data (bills, payments, transactions, connections, categories, imports). // Preserves the account, login, 2FA, and preferences. Requires a type-to-confirm token. -router.post('/erase-data', demoDataLimiter, (req, res) => { +router.post('/erase-data', demoDataLimiter, (req: Req, res: Res) => { const confirm = String(req.body?.confirm ?? '').trim().toUpperCase(); if (confirm !== 'ERASE') { return res.status(400).json(standardizeError('Type ERASE to confirm.', 'ERASE_CONFIRM_REQUIRED', 'confirm')); diff --git a/routes/version.js b/routes/version.cts similarity index 88% rename from routes/version.js rename to routes/version.cts index 53bb1c1..61163e2 100644 --- a/routes/version.js +++ b/routes/version.cts @@ -1,3 +1,5 @@ +// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). +import type { Req, Res, Next } from '../types/http'; const express = require('express'); const router = express.Router(); const fs = require('fs'); @@ -42,13 +44,13 @@ function parseHistory() { } // GET /api/version — version always comes from package.json; notes from HISTORY.md -router.get('/', (req, res) => { +router.get('/', (req: Req, res: Res) => { const { notes } = parseHistory(); res.json({ version: pkg.version, notes }); }); // GET /api/version/history -router.get('/history', (req, res) => { +router.get('/history', (req: Req, res: Res) => { try { if (!fs.existsSync(HISTORY_PATH)) { return res.status(404).json({ @@ -80,7 +82,7 @@ router.get('/history', (req, res) => { // GET /api/version/update-status — public, returns cached update check (no force-refresh) const { checkForUpdates } = require('../services/updateCheckService.cts'); -router.get('/update-status', async (req, res) => { +router.get('/update-status', async (req: Req, res: Res) => { try { res.json(await checkForUpdates(false)); } catch (err) { diff --git a/server.js b/server.js index afe8bcb..023d2dd 100644 --- a/server.js +++ b/server.js @@ -106,47 +106,47 @@ app.use('/api/auth/login', skipRateLimitIfNoUsers(loginUsernameLimiter)); // changes, require the SPA's x-csrf-token header like other mutating requests. // All other auth routes require CSRF (loginLimiter applied via /api/auth/login above) // Note: passwordLimiter is applied individually on routes that actually change passwords -app.use('/api/auth', csrfMiddleware, require('./routes/auth')); +app.use('/api/auth', csrfMiddleware, require('./routes/auth.cts')); // OIDC — rate-limited; returns 501 gracefully when OIDC is not configured -app.use('/api/auth/oidc', csrfMiddleware, oidcLimiter, require('./routes/authOidc')); +app.use('/api/auth/oidc', csrfMiddleware, oidcLimiter, require('./routes/authOidc.cts')); // Admin — all routes already require auth+admin; mutation-heavy routes get // an additional per-IP rate limit applied to the whole admin namespace // Backup operations have dedicated rate limiting (5 per hour) to prevent resource exhaustion // NOTE: backupOperationLimiter is applied per-route in routes/admin.js to avoid blocking non-backup admin actions -app.use('/api/admin', csrfMiddleware, requireAuth, requireAdmin, adminActionLimiter, require('./routes/admin')); +app.use('/api/admin', csrfMiddleware, requireAuth, requireAdmin, adminActionLimiter, require('./routes/admin.cts')); -app.use('/api/tracker', csrfMiddleware, requireAuth, requireUser, require('./routes/tracker')); -app.use('/api/bills', csrfMiddleware, requireAuth, requireUser, require('./routes/bills')); -app.use('/api/subscriptions', csrfMiddleware, requireAuth, requireUser, require('./routes/subscriptions')); -app.use('/api/payments', csrfMiddleware, requireAuth, requireUser, require('./routes/payments')); -app.use('/api/data-sources', csrfMiddleware, requireAuth, requireUser, require('./routes/dataSources')); -app.use('/api/transactions', csrfMiddleware, requireAuth, requireUser, require('./routes/transactions')); -app.use('/api/matches', csrfMiddleware, requireAuth, requireUser, require('./routes/matches')); -app.use('/api/categories', csrfMiddleware, requireAuth, requireUser, require('./routes/categories')); -app.use('/api/settings', csrfMiddleware, requireAuth, requireUser, require('./routes/settings')); -app.use('/api/user', csrfMiddleware, requireAuth, requireUser, require('./routes/user')); -app.use('/api/calendar', require('./routes/calendarFeed')); -app.use('/api/calendar', csrfMiddleware, requireAuth, requireUser, require('./routes/calendar')); -app.use('/api/summary', csrfMiddleware, requireAuth, requireUser, require('./routes/summary')); -app.use('/api/monthly-starting-amounts', csrfMiddleware, requireAuth, requireUser, require('./routes/monthly-starting-amounts')); -app.use('/api/analytics', csrfMiddleware, requireAuth, requireUser, require('./routes/analytics')); -app.use('/api/spending', csrfMiddleware, requireAuth, requireUser, require('./routes/spending')); -app.use('/api/snowball', csrfMiddleware, requireAuth, requireUser, require('./routes/snowball')); -app.use('/api/notifications', csrfMiddleware, requireAuth, require('./routes/notifications')); -app.use('/api/status', csrfMiddleware, requireAuth, requireAdmin, require('./routes/status')); -app.use('/api/about', require('./routes/about')); // public -app.use('/api/about-admin', adminActionLimiter, csrfMiddleware, requireAuth, requireAdmin, require('./routes/aboutAdmin')); // admin-only -app.use('/api/privacy', require('./routes/privacy')); // public -app.use('/api/version', require('./routes/version')); // public +app.use('/api/tracker', csrfMiddleware, requireAuth, requireUser, require('./routes/tracker.cts')); +app.use('/api/bills', csrfMiddleware, requireAuth, requireUser, require('./routes/bills.cts')); +app.use('/api/subscriptions', csrfMiddleware, requireAuth, requireUser, require('./routes/subscriptions.cts')); +app.use('/api/payments', csrfMiddleware, requireAuth, requireUser, require('./routes/payments.cts')); +app.use('/api/data-sources', csrfMiddleware, requireAuth, requireUser, require('./routes/dataSources.cts')); +app.use('/api/transactions', csrfMiddleware, requireAuth, requireUser, require('./routes/transactions.cts')); +app.use('/api/matches', csrfMiddleware, requireAuth, requireUser, require('./routes/matches.cts')); +app.use('/api/categories', csrfMiddleware, requireAuth, requireUser, require('./routes/categories.cts')); +app.use('/api/settings', csrfMiddleware, requireAuth, requireUser, require('./routes/settings.cts')); +app.use('/api/user', csrfMiddleware, requireAuth, requireUser, require('./routes/user.cts')); +app.use('/api/calendar', require('./routes/calendarFeed.cts')); +app.use('/api/calendar', csrfMiddleware, requireAuth, requireUser, require('./routes/calendar.cts')); +app.use('/api/summary', csrfMiddleware, requireAuth, requireUser, require('./routes/summary.cts')); +app.use('/api/monthly-starting-amounts', csrfMiddleware, requireAuth, requireUser, require('./routes/monthly-starting-amounts.cts')); +app.use('/api/analytics', csrfMiddleware, requireAuth, requireUser, require('./routes/analytics.cts')); +app.use('/api/spending', csrfMiddleware, requireAuth, requireUser, require('./routes/spending.cts')); +app.use('/api/snowball', csrfMiddleware, requireAuth, requireUser, require('./routes/snowball.cts')); +app.use('/api/notifications', csrfMiddleware, requireAuth, require('./routes/notifications.cts')); +app.use('/api/status', csrfMiddleware, requireAuth, requireAdmin, require('./routes/status.cts')); +app.use('/api/about', require('./routes/about.cts')); // public +app.use('/api/about-admin', adminActionLimiter, csrfMiddleware, requireAuth, requireAdmin, require('./routes/aboutAdmin.cts')); // admin-only +app.use('/api/privacy', require('./routes/privacy.cts')); // public +app.use('/api/version', require('./routes/version.cts')); // public // Profile — rate limit only on password-change, not all profile reads -app.use('/api/profile', csrfMiddleware, requireAuth, requireUser, require('./routes/profile')); +app.use('/api/profile', csrfMiddleware, requireAuth, requireUser, require('./routes/profile.cts')); // Export / Import — per-IP rate limited to deter abuse and resource exhaustion -const importRoutes = require('./routes/import'); -app.use('/api/export', csrfMiddleware, requireAuth, requireUser, exportLimiter, require('./routes/export')); +const importRoutes = require('./routes/import.cts'); +app.use('/api/export', csrfMiddleware, requireAuth, requireUser, exportLimiter, require('./routes/export.cts')); app.use('/api/import', csrfMiddleware, requireAuth, requireUser, importLimiter, importRoutes); app.use('/api/imports', csrfMiddleware, requireAuth, requireUser, importLimiter, importRoutes); diff --git a/tests/billReorder.test.js b/tests/billReorder.test.js index 7992162..9c2b92f 100644 --- a/tests/billReorder.test.js +++ b/tests/billReorder.test.js @@ -25,7 +25,7 @@ function createBill(db, userId, name, dueDay) { } function callBillsRoute(routePath, method, { userId, params = {}, query = {}, body = {} }) { - const billsRouter = require('../routes/bills'); + const billsRouter = require('../routes/bills.cts'); const layer = billsRouter.stack.find(item => item.route?.path === routePath && item.route.methods[method]); assert.ok(layer, `route ${method.toUpperCase()} ${routePath} should exist`); const handler = layer.route.stack[0].handle; diff --git a/tests/billsDeletedRoute.test.js b/tests/billsDeletedRoute.test.js index 81e1d13..0d6700a 100644 --- a/tests/billsDeletedRoute.test.js +++ b/tests/billsDeletedRoute.test.js @@ -33,7 +33,7 @@ function insertBill(db, userId, name, daysAgo) { } function callGetDeleted(userId) { - const router = require('../routes/bills'); + const router = require('../routes/bills.cts'); const layer = router.stack.find(item => item.route?.path === '/deleted' && item.route.methods.get); assert.ok(layer, 'GET /deleted route should exist'); const handler = layer.route.stack[0].handle; diff --git a/tests/calendarFeedService.test.js b/tests/calendarFeedService.test.js index e66a7ac..de643c6 100644 --- a/tests/calendarFeedService.test.js +++ b/tests/calendarFeedService.test.js @@ -45,7 +45,7 @@ function createBill(db, userId, overrides = {}) { } function callFeedRoute(query) { - const router = require('../routes/calendarFeed'); + const router = require('../routes/calendarFeed.cts'); const layer = router.stack.find(item => item.route?.path === '/feed.ics' && item.route.methods.get); assert.ok(layer, 'GET /feed.ics route should exist'); const handler = layer.route.stack[0].handle; diff --git a/tests/categoryGroups.test.js b/tests/categoryGroups.test.js index 4db3f24..27cfb1b 100644 --- a/tests/categoryGroups.test.js +++ b/tests/categoryGroups.test.js @@ -23,7 +23,7 @@ function createCategory(db, userId, name) { } function callCategoriesRoute(routePath, method, { userId, params = {}, body = {} }) { - const categoriesRouter = require('../routes/categories'); + const categoriesRouter = require('../routes/categories.cts'); const layer = categoriesRouter.stack.find(item => item.route?.path === routePath && item.route.methods[method]); assert.ok(layer, `route ${method.toUpperCase()} ${routePath} should exist`); const handler = layer.route.stack[0].handle; diff --git a/tests/categoryReorder.test.js b/tests/categoryReorder.test.js index 7871922..3b7a6e1 100644 --- a/tests/categoryReorder.test.js +++ b/tests/categoryReorder.test.js @@ -24,7 +24,7 @@ function createCategory(db, userId, name) { } function callCategoriesRoute(routePath, method, { userId, params = {}, body = {} }) { - const categoriesRouter = require('../routes/categories'); + const categoriesRouter = require('../routes/categories.cts'); const layer = categoriesRouter.stack.find(item => item.route?.path === routePath && item.route.methods[method]); assert.ok(layer, `route ${method.toUpperCase()} ${routePath} should exist`); const handler = layer.route.stack[0].handle; diff --git a/tests/exportImportRoundTrip.test.js b/tests/exportImportRoundTrip.test.js index ac9912f..dbffc33 100644 --- a/tests/exportImportRoundTrip.test.js +++ b/tests/exportImportRoundTrip.test.js @@ -14,7 +14,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-roundtrip-test-${process.pid process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const { buildUserDbExportFile } = require('../routes/export'); +const { buildUserDbExportFile } = require('../routes/export.cts'); const { previewUserDbImport, applyUserDbImport } = require('../services/userDbImportService.cts'); const mkUser = (db, name) => diff --git a/tests/exportRicher.test.js b/tests/exportRicher.test.js index 3e69292..0cc08f6 100644 --- a/tests/exportRicher.test.js +++ b/tests/exportRicher.test.js @@ -12,7 +12,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-export-richer-${process.pid} process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const exportRouter = require('../routes/export'); +const exportRouter = require('../routes/export.cts'); const { getUserExportData } = exportRouter; let userId, billId; diff --git a/tests/paymentsQuickRoute.test.js b/tests/paymentsQuickRoute.test.js index 4508648..1d5a984 100644 --- a/tests/paymentsQuickRoute.test.js +++ b/tests/paymentsQuickRoute.test.js @@ -14,7 +14,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-quickpay-${process.pid}.sqli process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const router = require('../routes/payments'); +const router = require('../routes/payments.cts'); function handler(method, routePath) { const layer = router.stack.find(l => l.route?.path === routePath && l.route.methods[method]); diff --git a/tests/paymentsRoute.test.js b/tests/paymentsRoute.test.js index cab1532..92d0b20 100644 --- a/tests/paymentsRoute.test.js +++ b/tests/paymentsRoute.test.js @@ -15,7 +15,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-payments-${process.pid}.sqli process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const router = require('../routes/payments'); +const router = require('../routes/payments.cts'); function handler(method, routePath) { const layer = router.stack.find(l => l.route?.path === routePath && l.route.methods[method]); diff --git a/tests/profileRoute.test.js b/tests/profileRoute.test.js index 2bef2e5..b0a6284 100644 --- a/tests/profileRoute.test.js +++ b/tests/profileRoute.test.js @@ -18,7 +18,7 @@ function createUser(db, suffix) { } function callProfileRoute(method, { userId, body = {} }) { - const profileRouter = require('../routes/profile'); + const profileRouter = require('../routes/profile.cts'); const layer = profileRouter.stack.find(item => item.route?.path === '/' && item.route.methods[method]); assert.ok(layer, `route ${method.toUpperCase()} / should exist`); const handler = layer.route.stack[0].handle; diff --git a/tests/reconciliation.test.js b/tests/reconciliation.test.js index 92cd635..da35dcc 100644 --- a/tests/reconciliation.test.js +++ b/tests/reconciliation.test.js @@ -21,7 +21,7 @@ const { getDb, closeDb } = require('../db/database'); const { getTracker } = require('../services/trackerService.cts'); const { getAnalyticsSummary } = require('../services/analyticsService.cts'); const { resolveDueDate } = require('../services/statusService.cts'); -const summaryRouter = require('../routes/summary'); +const summaryRouter = require('../routes/summary.cts'); // Fixed "now" so occurrence gating is deterministic: June 20, 2026. const NOW = new Date(2026, 5, 20, 12, 0, 0); diff --git a/tests/snowballPlanRoute.test.js b/tests/snowballPlanRoute.test.js index 7b25bcb..a3929af 100644 --- a/tests/snowballPlanRoute.test.js +++ b/tests/snowballPlanRoute.test.js @@ -13,7 +13,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-snowball-plan-${process.pid} process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const router = require('../routes/snowball'); +const router = require('../routes/snowball.cts'); function handler(method, routePath) { const layer = router.stack.find(l => l.route?.path === routePath && l.route.methods[method]); diff --git a/tests/summaryBankTracking.test.js b/tests/summaryBankTracking.test.js index 0978f06..8088b20 100644 --- a/tests/summaryBankTracking.test.js +++ b/tests/summaryBankTracking.test.js @@ -15,7 +15,7 @@ process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); function callSummary(userId, year, month) { - const router = require('../routes/summary'); + const router = require('../routes/summary.cts'); const layer = router.stack.find(item => item.route?.path === '/' && item.route.methods.get); assert.ok(layer, 'GET /api/summary route should exist'); const handler = layer.route.stack[0].handle; diff --git a/tests/summarySkipOverride.test.js b/tests/summarySkipOverride.test.js index 0a4529a..d60f9b3 100644 --- a/tests/summarySkipOverride.test.js +++ b/tests/summarySkipOverride.test.js @@ -15,7 +15,7 @@ process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); function callSummary(userId, year, month) { - const router = require('../routes/summary'); + const router = require('../routes/summary.cts'); const layer = router.stack.find(item => item.route?.path === '/' && item.route.methods.get); const handler = layer.route.stack[0].handle; return new Promise((resolve) => { diff --git a/tests/trackerService.test.js b/tests/trackerService.test.js index 8e7a4b8..fe68786 100644 --- a/tests/trackerService.test.js +++ b/tests/trackerService.test.js @@ -125,7 +125,7 @@ test('auto_mark_paid bill creates an autopay payment and drops the balance atomi }); test('GET /tracker returns a standardized error for an invalid month', async () => { - const router = require('../routes/tracker'); + const router = require('../routes/tracker.cts'); const layer = router.stack.find(l => l.route?.path === '/' && l.route.methods.get); const handler = layer.route.stack[layer.route.stack.length - 1].handle; const userId = mkUser('t1-route'); diff --git a/tests/transactionMatchService.test.js b/tests/transactionMatchService.test.js index 5834a7b..6b978e3 100644 --- a/tests/transactionMatchService.test.js +++ b/tests/transactionMatchService.test.js @@ -85,7 +85,7 @@ function trackerRow(userId, billId, today = '2026-05-20') { } function callBillsRoute(routePath, { userId, params = {}, query = {} }) { - const billsRouter = require('../routes/bills'); + const billsRouter = require('../routes/bills.cts'); const layer = billsRouter.stack.find(item => item.route?.path === routePath && item.route.methods.get); assert.ok(layer, `route ${routePath} should exist`); const handler = layer.route.stack[0].handle; @@ -115,7 +115,7 @@ function callBillsRoute(routePath, { userId, params = {}, query = {} }) { } function callPaymentsRoute(routePath, method, { userId, params = {}, query = {}, body = {} }) { - const paymentsRouter = require('../routes/payments'); + const paymentsRouter = require('../routes/payments.cts'); const layer = paymentsRouter.stack.find(item => item.route?.path === routePath && item.route.methods[method]); assert.ok(layer, `route ${method.toUpperCase()} ${routePath} should exist`); const handler = layer.route.stack[0].handle;