diff --git a/routes/auth.cts b/routes/auth.cts index 924eeab..81a7304 100644 --- a/routes/auth.cts +++ b/routes/auth.cts @@ -1,9 +1,8 @@ -// @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 _appVersion; +let _appVersion: string | undefined; function getAppVersion() { if (!_appVersion) { try { @@ -196,7 +195,7 @@ router.get('/login-history', requireAuth, (req: Req, res: Res) => { ) .all(req.user.id); - const safeDecrypt = (v) => { + const safeDecrypt = (v: any) => { if (!v) return null; try { return decryptSecret(v); @@ -214,7 +213,7 @@ router.get('/login-history', requireAuth, (req: Req, res: Res) => { ? require('crypto').createHash('sha256').update(currentCookie).digest('hex').slice(0, 32) : null; - const history = rows.map((r) => ({ + const history = rows.map((r: any) => ({ id: r.id, logged_in_at: r.logged_in_at, ip_address: safeDecrypt(r.ip_address), diff --git a/routes/categories.cts b/routes/categories.cts index 7613ab9..9464494 100644 --- a/routes/categories.cts +++ b/routes/categories.cts @@ -1,4 +1,3 @@ -// @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 { ValidationError, NotFoundError, ConflictError } = require('../utils/apiError.cts'); @@ -10,7 +9,7 @@ const { sql } = require('kysely'); // Maps a SQLite UNIQUE-constraint violation to a friendly 409; re-throws anything // else so the terminal handler returns a safe 500. -function asConflict(e, message, field) { +function asConflict(e: any, message: string, field?: string) { if (e.message?.includes('UNIQUE')) throw ConflictError(message, field); throw e; } @@ -57,8 +56,8 @@ router.get('/', (req: Req, res: Res) => { ORDER BY b.active DESC, b.due_day ASC, b.name COLLATE NOCASE ASC `); - const shaped = categories.map((category) => { - const bills = billsByCategory.all(req.user.id, category.id).map((bill) => ({ + const shaped = categories.map((category: any) => { + const bills = billsByCategory.all(req.user.id, category.id).map((bill: any) => ({ ...bill, active: !!bill.active, payment_count: Number(bill.payment_count || 0), @@ -66,9 +65,9 @@ router.get('/', (req: Req, res: Res) => { last_paid_date: bill.last_paid_date || null, })); - const activeBillCount = bills.filter((bill) => bill.active).length; + const activeBillCount = bills.filter((bill: any) => bill.active).length; const inactiveBillCount = bills.length - activeBillCount; - const paymentCount = bills.reduce((sum, bill) => sum + bill.payment_count, 0); + const paymentCount = bills.reduce((sum: number, bill: any) => sum + bill.payment_count, 0); return { ...category, @@ -77,7 +76,7 @@ router.get('/', (req: Req, res: Res) => { active_bill_count: activeBillCount, inactive_bill_count: inactiveBillCount, payment_count: paymentCount, - bill_names: bills.map((bill) => bill.name), + bill_names: bills.map((bill: any) => bill.name), bills, }; }); @@ -129,7 +128,7 @@ router.put('/reorder', (req: Req, res: Res) => { const update = db.prepare( "UPDATE categories SET sort_order = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?", ); - db.transaction((items) => { + db.transaction((items: any) => { for (const item of items) update.run(item.sortOrder, item.categoryId, req.user.id); })(entries); diff --git a/routes/dataSources.cts b/routes/dataSources.cts index b2a46b6..d668ccb 100644 --- a/routes/dataSources.cts +++ b/routes/dataSources.cts @@ -1,4 +1,3 @@ -// @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'); @@ -22,14 +21,14 @@ const { syncLimiter } = require('../middleware/rateLimiter.cts'); const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']); const VALID_STATUSES = new Set(['active', 'inactive', 'error']); -function cleanFilter(value) { +function cleanFilter(value: any) { return typeof value === 'string' ? value.trim() : ''; } // SimpleFIN/service errors are deliberately surfaced to the user — but only // after sanitizeErrorMessage strips tokens/URLs. Wrapping in ApiError keeps // the sanitized message through the terminal handler (raw 5xx would be masked). -function toSourceError(err, fallback, code = 'SIMPLEFIN_ERROR') { +function toSourceError(err: any, fallback: string, code = 'SIMPLEFIN_ERROR') { const msg = sanitizeErrorMessage(err?.message || fallback); const status = typeof err?.status === 'number' ? err.status : 500; return new ApiError(err?.code || code, msg, status); @@ -63,7 +62,7 @@ router.get('/accounts/all', (req: Req, res: Res) => { .all(req.user.id); res.json( - accounts.map((a) => ({ + accounts.map((a: any) => ({ ...a, monitored: a.monitored === 1, balance_dollars: a.balance !== null ? a.balance / 100 : null, @@ -211,7 +210,7 @@ router.get('/:sourceId/accounts', (req: Req, res: Res) => { LIMIT 50 `); - const result = accounts.map((acc) => ({ + const result = accounts.map((acc: any) => ({ ...acc, monitored: acc.monitored === 1, transactions: acc.monitored === 1 ? txStmt.all(acc.id, req.user.id) : [], @@ -310,7 +309,7 @@ router.post('/sync-all', syncLimiter, async (req: Req, res: Res) => { autoMatched += result.autoMatched ?? 0; for (const name of result.matched_bills ?? []) matchedBillSet.add(name); for (const attr of result.late_attributions ?? []) lateAttrAll.push(attr); - } catch (err) { + } catch (err: any) { errors.push(sanitizeErrorMessage(err?.message || 'Sync failed')); } } diff --git a/routes/import.cts b/routes/import.cts index 4ac11cf..4d03ace 100644 --- a/routes/import.cts +++ b/routes/import.cts @@ -1,4 +1,3 @@ -// @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'); @@ -25,7 +24,7 @@ function dataImportEnabled() { return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false'; } -function requireDataImportEnabled(req, res, next) { +function requireDataImportEnabled(req: Req, res: Res, next: Next) { if (!dataImportEnabled()) { return res .status(403) @@ -38,7 +37,7 @@ function makeErrorId() { return `imp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; } -function sendImportError(res, err, fallback, defaultCode) { +function sendImportError(res: Res, err: any, fallback: string, defaultCode: string) { // Only 4xx service errors carry a user-facing message; a thrown error with // an explicit 5xx status must fall through to the masked error-id branch. if (err.status && err.status < 500) { @@ -423,7 +422,7 @@ router.get('/history', requireDataImportEnabled, (req: Req, res: Res) => { try { const history = getImportHistory(req.user.id); res.json({ history }); - } catch (err) { + } catch (err: any) { log.error('[import] history error:', err.message); res.status(500).json({ error: 'Failed to load import history' }); } diff --git a/routes/notifications.cts b/routes/notifications.cts index 1bed038..50512c8 100644 --- a/routes/notifications.cts +++ b/routes/notifications.cts @@ -1,4 +1,3 @@ -// @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 { log } = require('../utils/logger.cts'); @@ -29,7 +28,7 @@ router.get('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => { 'notify_global_recipient', 'reminder_hour', ]; - const settings = {}; + const settings: Record = {}; for (const k of keys) settings[k] = getSetting(k) || ''; if (!settings.reminder_hour) settings.reminder_hour = '6'; // Mask password in response @@ -65,7 +64,7 @@ router.put('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => { setSetting('reminder_hour', String(hour)); try { require('../workers/dailyWorker.cts').rescheduleDailyWorker(); - } catch (err) { + } catch (err: any) { log.error('[notifications] Failed to reschedule daily worker:', err.message); } } @@ -78,7 +77,7 @@ router.post('/test', requireAuth, requireAdmin, async (req: Req, res: Res) => { if (!to) throw ValidationError('Recipient address required', 'to'); try { await sendTestEmail(to); - } catch (err) { + } catch (err: any) { // Deliberate pass-through: the SMTP error text is the admin's own config // feedback (bad host/auth/TLS). ApiError messages survive formatError; // anything not wrapped would be masked as a generic 5xx. @@ -172,7 +171,7 @@ router.post('/test-push', requireAuth, requireUser, async (req: Req, res: Res) = } // Decrypt for sending - const safeDecrypt = (v) => { + const safeDecrypt = (v: any) => { try { return v ? decryptSecret(v) : ''; } catch { @@ -189,7 +188,7 @@ router.post('/test-push', requireAuth, requireUser, async (req: Req, res: Res) = try { if (!sendTestPush) throw new Error('Push service not initialised'); await sendTestPush(userForPush); - } catch (err) { + } catch (err: any) { // Deliberate pass-through: the push-provider error is the user's own // channel-config feedback (bad URL/token) — same rationale as /test above. throw new ApiError('NOTIFICATION_TEST_FAILED', err.message || 'Test push failed', 502); diff --git a/routes/profile.cts b/routes/profile.cts index eb68242..8aa95b6 100644 --- a/routes/profile.cts +++ b/routes/profile.cts @@ -1,4 +1,3 @@ -// @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'); @@ -35,14 +34,14 @@ function dataImportEnabled() { return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false'; } -function requireDataImportEnabled(req, res, next) { +function requireDataImportEnabled(req: Req, res: Res, next: Next) { if (!dataImportEnabled()) { throw ForbiddenError('Data import is disabled by DATA_IMPORT_ENABLED=false'); } next(); } -function profileResponse(user) { +function profileResponse(user: any) { const displayName = user.display_name || null; return { id: user.id, @@ -287,10 +286,11 @@ router.patch('/settings', (req: Req, res: Res) => { : null : current.notification_email; - const boolVal = (incoming, fallback) => (incoming !== undefined ? (incoming ? 1 : 0) : fallback); + const boolVal = (incoming: any, fallback: any) => + incoming !== undefined ? (incoming ? 1 : 0) : fallback; // Encrypt push_url and push_token before storing - const encryptOrNull = (v, fallback) => { + const encryptOrNull = (v: any, fallback: any) => { if (v === undefined) return fallback; if (!v) return null; try { diff --git a/routes/spending.cts b/routes/spending.cts index 0cf8444..b911d31 100644 --- a/routes/spending.cts +++ b/routes/spending.cts @@ -1,4 +1,3 @@ -// @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'); @@ -21,7 +20,7 @@ const { // Throws ValidationError on bad input; service errors propagate to the // terminal handler (4xx with .status pass their message through, 5xx are // masked + logged there — no per-route try/catch, per CODE_STANDARDS.md). -function parseYM(source) { +function parseYM(source: any) { const now = new Date(); const year = parseInt(source.year || now.getFullYear(), 10); const month = parseInt(source.month || now.getMonth() + 1, 10); diff --git a/routes/status.cts b/routes/status.cts index b5f804c..48be218 100644 --- a/routes/status.cts +++ b/routes/status.cts @@ -1,4 +1,3 @@ -// @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(); @@ -22,7 +21,7 @@ try { pkg = { name: 'bill-tracker', version: '1.0.0' }; } -function errorMessage(err) { +function errorMessage(err: any) { return err?.message || String(err); } @@ -30,7 +29,7 @@ function errorMessage(err) { // Without the Z suffix, JS Date parses it as LOCAL time, creating timezone // inconsistencies when mixed with ISO strings that do have Z. // This ensures all timestamps sent to clients are unambiguous UTC ISO strings. -function toIso(sqliteOrIso) { +function toIso(sqliteOrIso: any) { if (!sqliteOrIso) return null; const s = String(sqliteOrIso).trim(); // Already ISO with Z or offset — return as-is @@ -40,7 +39,7 @@ function toIso(sqliteOrIso) { return s; } -function monthRange(now) { +function monthRange(now: any) { const year = now.getFullYear(); const month = now.getMonth() + 1; const daysInMonth = new Date(year, month, 0).getDate(); @@ -84,7 +83,7 @@ router.get('/', async (req: Req, res: Res) => { last_sent_at: null, last_error: runtimeState.notifications.last_error, }; - let backups = { + let backups: any = { ok: true, enabled: false, configured: false, @@ -271,7 +270,7 @@ router.get('/', async (req: Req, res: Res) => { } // Bank sync (SimpleFIN) status - let bankSync = { + let bankSync: any = { enabled: false, running: false, source_count: 0, @@ -351,7 +350,7 @@ router.get('/', async (req: Req, res: Res) => { } // Cleanup status — safe read-only summary from settings, no paths or secrets - let cleanup = { ok: true, last_run_at: null, last_result: null }; + let cleanup: any = { ok: true, last_run_at: null, last_result: null }; try { const raw = getSetting('cleanup_last_result'); cleanup = {