refactor(server): migrate 8 leaf services + apiError to TypeScript (.cts)
Batch of low-blast-radius modules: utils/apiError, and services statusRuntime, loginFingerprint, auditService, transactionMatchState, updateCheckService, advisoryFilterService, bankSyncConfigService. - types/http.d.ts: shared permissive Express-ish Req/Res/Next/Router types (@types/express isn't installed; handlers are dynamic) — for apiError's errorHandler now and the routes/middleware batches next. - Every require of a migrated module updated to the explicit .cts extension. - Confirmed + documented the Node rule: a .cts needs at least one `import` statement or type-stripping never activates (node --check fails on the first type token); import-less modules lead with an `import type`. Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
005b0b7b64
commit
a6c9b6cb1c
|
|
@ -7,7 +7,7 @@ const fs = require('fs');
|
||||||
let _logAudit = null;
|
let _logAudit = null;
|
||||||
function getLogAudit() {
|
function getLogAudit() {
|
||||||
if (!_logAudit) {
|
if (!_logAudit) {
|
||||||
try { _logAudit = require('../services/auditService').logAudit; } catch { _logAudit = () => {}; }
|
try { _logAudit = require('../services/auditService.cts').logAudit; } catch { _logAudit = () => {}; }
|
||||||
}
|
}
|
||||||
return _logAudit;
|
return _logAudit;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { logAudit } = require('../services/auditService');
|
const { logAudit } = require('../services/auditService.cts');
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// CSRF Middleware
|
// CSRF Middleware
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { ValidationError, formatError } = require('../utils/apiError');
|
const { ValidationError, formatError } = require('../utils/apiError.cts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract field name from various validation error patterns
|
* Extract field name from various validation error patterns
|
||||||
|
|
|
||||||
|
|
@ -475,7 +475,7 @@ router.get('/dev-log', requireAuth, requireAdmin, (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { checkForUpdates } = require('../services/updateCheckService');
|
const { checkForUpdates } = require('../services/updateCheckService.cts');
|
||||||
|
|
||||||
// POST /api/about-admin/check-updates — force a fresh update check, bypassing cache
|
// 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, res) => {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getDb, rollbackMigration } = require('../db/database');
|
const { getDb, rollbackMigration } = require('../db/database');
|
||||||
const { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays, setDebugLogging } = require('../services/bankSyncConfigService');
|
const { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays, setDebugLogging } = require('../services/bankSyncConfigService.cts');
|
||||||
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker');
|
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker');
|
||||||
const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts');
|
const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts');
|
||||||
const { hashPassword } = require('../services/authService');
|
const { hashPassword } = require('../services/authService');
|
||||||
const { logAudit } = require('../services/auditService');
|
const { logAudit } = require('../services/auditService.cts');
|
||||||
const {
|
const {
|
||||||
createBackup,
|
createBackup,
|
||||||
deleteBackup,
|
deleteBackup,
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@ const { decryptSecret } = require('../services/encryptionService.cts');
|
||||||
const { getCsrfToken } = require('../middleware/csrf');
|
const { getCsrfToken } = require('../middleware/csrf');
|
||||||
const { requireAuth, requireAdmin } = require('../middleware/requireAuth');
|
const { requireAuth, requireAdmin } = require('../middleware/requireAuth');
|
||||||
const { getPublicOidcInfo } = require('../services/oidcService');
|
const { getPublicOidcInfo } = require('../services/oidcService');
|
||||||
const { ValidationError, formatError } = require('../utils/apiError');
|
const { ValidationError, formatError } = require('../utils/apiError.cts');
|
||||||
const { standardizeError } = require('../middleware/errorFormatter');
|
const { standardizeError } = require('../middleware/errorFormatter');
|
||||||
const { passwordLimiter } = require('../middleware/rateLimiter');
|
const { passwordLimiter } = require('../middleware/rateLimiter');
|
||||||
const { logAudit } = require('../services/auditService');
|
const { logAudit } = require('../services/auditService.cts');
|
||||||
|
|
||||||
// ─────────────────────────────────────────
|
// ─────────────────────────────────────────
|
||||||
// PUBLIC AUTH ROUTES
|
// PUBLIC AUTH ROUTES
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const { standardizeError } = require('../middleware/erro
|
||||||
const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService');
|
const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService');
|
||||||
const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService');
|
const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService');
|
||||||
const { sanitizeErrorMessage } = require('../services/simplefinService');
|
const { sanitizeErrorMessage } = require('../services/simplefinService');
|
||||||
const { getBankSyncConfig } = require('../services/bankSyncConfigService');
|
const { getBankSyncConfig } = require('../services/bankSyncConfigService.cts');
|
||||||
const { syncLimiter } = require('../middleware/rateLimiter');
|
const { syncLimiter } = require('../middleware/rateLimiter');
|
||||||
|
|
||||||
const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']);
|
const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const {
|
||||||
rejectMatchSuggestion,
|
rejectMatchSuggestion,
|
||||||
} = require('../services/matchSuggestionService');
|
} = require('../services/matchSuggestionService');
|
||||||
const { learnMerchantRuleFromMatch } = require('../services/billMerchantRuleService.cts');
|
const { learnMerchantRuleFromMatch } = require('../services/billMerchantRuleService.cts');
|
||||||
const { markMatched, markUnmatched } = require('../services/transactionMatchState');
|
const { markMatched, markUnmatched } = require('../services/transactionMatchState.cts');
|
||||||
const { serializePayment } = require('../services/paymentValidation.cts');
|
const { serializePayment } = require('../services/paymentValidation.cts');
|
||||||
const { todayLocal } = require('../utils/dates.mts');
|
const { todayLocal } = require('../utils/dates.mts');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const { getDb } = require('../db/database');
|
||||||
const { computeBalanceDelta, applyBalanceDelta } = require('../services/billsService');
|
const { computeBalanceDelta, applyBalanceDelta } = require('../services/billsService');
|
||||||
const { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts');
|
const { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts');
|
||||||
const { getCycleRange, resolveDueDate } = require('../services/statusService.cts');
|
const { getCycleRange, resolveDueDate } = require('../services/statusService.cts');
|
||||||
const { markUnmatched } = require('../services/transactionMatchState');
|
const { markUnmatched } = require('../services/transactionMatchState.cts');
|
||||||
const {
|
const {
|
||||||
markProvisionalManualPaymentsOverridden,
|
markProvisionalManualPaymentsOverridden,
|
||||||
reactivatePaymentsOverriddenBy,
|
reactivatePaymentsOverriddenBy,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const { passwordLimiter } = require('../middleware/rateLimiter');
|
||||||
const { getDb, getSetting } = require('../db/database');
|
const { getDb, getSetting } = require('../db/database');
|
||||||
const { hashPassword, invalidateOtherSessions, rotateSessionId, COOKIE_NAME, cookieOpts } = require('../services/authService');
|
const { hashPassword, invalidateOtherSessions, rotateSessionId, COOKIE_NAME, cookieOpts } = require('../services/authService');
|
||||||
const { getImportHistory } = require('../services/spreadsheetImportService');
|
const { getImportHistory } = require('../services/spreadsheetImportService');
|
||||||
const { logAudit } = require('../services/auditService');
|
const { logAudit } = require('../services/auditService.cts');
|
||||||
const { standardizeError } = require('../middleware/errorFormatter');
|
const { standardizeError } = require('../middleware/errorFormatter');
|
||||||
const { encryptSecret, decryptSecret } = require('../services/encryptionService.cts');
|
const { encryptSecret, decryptSecret } = require('../services/encryptionService.cts');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ const router = express.Router();
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { getDb, getSetting } = require('../db/database');
|
const { getDb, getSetting } = require('../db/database');
|
||||||
const { getStatusRuntime, recordError } = require('../services/statusRuntime');
|
const { getStatusRuntime, recordError } = require('../services/statusRuntime.cts');
|
||||||
const { listBackups } = require('../services/backupService');
|
const { listBackups } = require('../services/backupService');
|
||||||
const { getScheduleStatus } = require('../services/backupScheduler');
|
const { getScheduleStatus } = require('../services/backupScheduler');
|
||||||
const { checkForUpdates } = require('../services/updateCheckService');
|
const { checkForUpdates } = require('../services/updateCheckService.cts');
|
||||||
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker');
|
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker');
|
||||||
const { getBankSyncConfig } = require('../services/bankSyncConfigService');
|
const { getBankSyncConfig } = require('../services/bankSyncConfigService.cts');
|
||||||
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
|
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
|
||||||
const { localDateString } = require('../utils/dates.mts');
|
const { localDateString } = require('../utils/dates.mts');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ const {
|
||||||
ensureManualDataSource,
|
ensureManualDataSource,
|
||||||
getTransactionForUser,
|
getTransactionForUser,
|
||||||
} = require('../services/transactionService');
|
} = require('../services/transactionService');
|
||||||
const { checkTransaction: advisoryCheck } = require('../services/advisoryFilterService');
|
const { checkTransaction: advisoryCheck } = require('../services/advisoryFilterService.cts');
|
||||||
const { getBankSyncConfig } = require('../services/bankSyncConfigService');
|
const { getBankSyncConfig } = require('../services/bankSyncConfigService.cts');
|
||||||
const { markUnmatched } = require('../services/transactionMatchState');
|
const { markUnmatched } = require('../services/transactionMatchState.cts');
|
||||||
const {
|
const {
|
||||||
ignoreTransaction,
|
ignoreTransaction,
|
||||||
matchTransactionToBill,
|
matchTransactionToBill,
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ router.get('/history', (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/version/update-status — public, returns cached update check (no force-refresh)
|
// GET /api/version/update-status — public, returns cached update check (no force-refresh)
|
||||||
const { checkForUpdates } = require('../services/updateCheckService');
|
const { checkForUpdates } = require('../services/updateCheckService.cts');
|
||||||
|
|
||||||
router.get('/update-status', async (req, res) => {
|
router.get('/update-status', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ const path = require('path');
|
||||||
|
|
||||||
const { getDb } = require('./db/database');
|
const { getDb } = require('./db/database');
|
||||||
const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth');
|
const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth');
|
||||||
const { recordError } = require('./services/statusRuntime');
|
const { recordError } = require('./services/statusRuntime.cts');
|
||||||
const { securityHeaders } = require('./middleware/securityHeaders');
|
const { securityHeaders } = require('./middleware/securityHeaders');
|
||||||
const { logAudit } = require('./services/auditService');
|
const { logAudit } = require('./services/auditService.cts');
|
||||||
const { errorFormatter } = require('./middleware/errorFormatter');
|
const { errorFormatter } = require('./middleware/errorFormatter');
|
||||||
const { importLimiter, exportLimiter, adminActionLimiter, oidcLimiter, loginLimiter, loginUsernameLimiter, passwordLimiter, backupOperationLimiter } =
|
const { importLimiter, exportLimiter, adminActionLimiter, oidcLimiter, loginLimiter, loginUsernameLimiter, passwordLimiter, backupOperationLimiter } =
|
||||||
require('./middleware/rateLimiter');
|
require('./middleware/rateLimiter');
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import type { Db } from '../types/db';
|
||||||
|
|
||||||
const { getDb } = require('../db/database');
|
const { getDb } = require('../db/database');
|
||||||
|
|
||||||
// Lazy-loaded in-memory cache — loaded once on first use
|
// Lazy-loaded in-memory cache — loaded once on first use
|
||||||
let _patterns = null;
|
let _patterns: any[] | null = null;
|
||||||
let _overrideTerms = null;
|
let _overrideTerms: string[] | null = null;
|
||||||
|
|
||||||
function normalize(text) {
|
function normalize(text: unknown): string {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
return String(text)
|
return String(text)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
@ -15,15 +17,15 @@ function normalize(text) {
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCache() {
|
function loadCache(): void {
|
||||||
if (_patterns !== null) return;
|
if (_patterns !== null) return;
|
||||||
const db = getDb();
|
const db: Db = getDb();
|
||||||
_patterns = db.prepare(
|
_patterns = db.prepare(
|
||||||
'SELECT pattern, confidence, category, rationale FROM advisory_non_bill_filters'
|
'SELECT pattern, confidence, category, rationale FROM advisory_non_bill_filters'
|
||||||
).all();
|
).all();
|
||||||
_overrideTerms = db.prepare(
|
_overrideTerms = db.prepare(
|
||||||
'SELECT term FROM advisory_bill_like_overrides'
|
'SELECT term FROM advisory_bill_like_overrides'
|
||||||
).all().map(r => r.term);
|
).all().map((r: any) => r.term);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,7 +33,7 @@ function loadCache() {
|
||||||
* Returns null if Create Bill should be shown normally, or
|
* Returns null if Create Bill should be shown normally, or
|
||||||
* { confidence: 'high'|'medium', category, rationale } if it should be suppressed.
|
* { confidence: 'high'|'medium', category, rationale } if it should be suppressed.
|
||||||
*/
|
*/
|
||||||
function checkTransaction(title) {
|
function checkTransaction(title: unknown): { confidence: string; category: any; rationale: any } | null {
|
||||||
if (!title) return null;
|
if (!title) return null;
|
||||||
try {
|
try {
|
||||||
loadCache();
|
loadCache();
|
||||||
|
|
@ -43,15 +45,15 @@ function checkTransaction(title) {
|
||||||
if (!normalized) return null;
|
if (!normalized) return null;
|
||||||
|
|
||||||
// Bill-like override terms take priority — always show Create Bill
|
// Bill-like override terms take priority — always show Create Bill
|
||||||
for (const term of _overrideTerms) {
|
for (const term of _overrideTerms!) {
|
||||||
if (normalized.includes(term)) return null;
|
if (normalized.includes(term)) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the highest-confidence matching pattern
|
// Find the highest-confidence matching pattern
|
||||||
let highMatch = null;
|
let highMatch: any = null;
|
||||||
let mediumMatch = null;
|
let mediumMatch: any = null;
|
||||||
|
|
||||||
for (const row of _patterns) {
|
for (const row of _patterns!) {
|
||||||
if (normalized.includes(row.pattern)) {
|
if (normalized.includes(row.pattern)) {
|
||||||
if (row.confidence === 'high') {
|
if (row.confidence === 'high') {
|
||||||
highMatch = row;
|
highMatch = row;
|
||||||
|
|
@ -73,7 +75,7 @@ function checkTransaction(title) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear the in-memory cache (used after re-seeding in tests). */
|
/** Clear the in-memory cache (used after re-seeding in tests). */
|
||||||
function clearCache() {
|
function clearCache(): void {
|
||||||
_patterns = null;
|
_patterns = null;
|
||||||
_overrideTerms = null;
|
_overrideTerms = null;
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
|
import type { Db } from '../types/db';
|
||||||
|
|
||||||
const { getDb } = require('../db/database');
|
const { getDb } = require('../db/database');
|
||||||
|
|
||||||
|
interface AuditParams {
|
||||||
|
user_id?: number | null;
|
||||||
|
action: string;
|
||||||
|
entity_type?: string | null;
|
||||||
|
entity_id?: number | null;
|
||||||
|
details?: any;
|
||||||
|
ip_address?: string | null;
|
||||||
|
user_agent?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a security-sensitive action to the audit_log table.
|
* Log a security-sensitive action to the audit_log table.
|
||||||
* @param {Object} params
|
|
||||||
* @param {number|null} params.user_id - User ID (null for anonymous/failed attempts)
|
|
||||||
* @param {string} params.action - Action type (e.g., 'login.success', 'login.failure', 'password.change', 'role.change', 'session.invalidate')
|
|
||||||
* @param {string} [params.entity_type] - Entity type (e.g., 'user', 'session', 'bill')
|
|
||||||
* @param {number} [params.entity_id] - Entity ID
|
|
||||||
* @param {Object} [params.details] - Additional details (stored as JSON)
|
|
||||||
* @param {string} [params.ip_address] - Request IP
|
|
||||||
* @param {string} [params.user_agent] - Request user-agent
|
|
||||||
*/
|
*/
|
||||||
function logAudit({ user_id, action, entity_type, entity_id, details, ip_address, user_agent }) {
|
function logAudit({ user_id, action, entity_type, entity_id, details, ip_address, user_agent }: AuditParams): void {
|
||||||
const db = getDb();
|
const db: Db = getDb();
|
||||||
try {
|
try {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO audit_log (user_id, action, entity_type, entity_id, details_json, ip_address, user_agent)
|
`INSERT INTO audit_log (user_id, action, entity_type, entity_id, details_json, ip_address, user_agent)
|
||||||
|
|
@ -26,7 +30,7 @@ function logAudit({ user_id, action, entity_type, entity_id, details, ip_address
|
||||||
ip_address || null,
|
ip_address || null,
|
||||||
user_agent || null
|
user_agent || null
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
// Audit logging should never crash the app
|
// Audit logging should never crash the app
|
||||||
console.error('[audit-error] Failed to log audit event:', err.message);
|
console.error('[audit-error] Failed to log audit event:', err.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const { getDb } = require('../db/database');
|
const { getDb } = require('../db/database');
|
||||||
const { buildDeviceFingerprint } = require('./loginFingerprint');
|
const { buildDeviceFingerprint } = require('./loginFingerprint.cts');
|
||||||
const { encryptSecret, decryptSecret } = require('./encryptionService.cts');
|
const { encryptSecret, decryptSecret } = require('./encryptionService.cts');
|
||||||
|
|
||||||
// Store SHA-256(token) in the DB; the raw token stays only in the cookie.
|
// Store SHA-256(token) in the DB; the raw token stays only in the cookie.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// import required so Node type-strips this .cts (it has no other import).
|
||||||
|
import type { Db } from '../types/db';
|
||||||
|
|
||||||
const { getSetting, setSetting } = require('../db/database');
|
const { getSetting, setSetting } = require('../db/database');
|
||||||
|
|
||||||
const SYNC_DAYS_MAX = 45; // SimpleFIN Bridge hard limit — requests beyond this return an error
|
const SYNC_DAYS_MAX = 45; // SimpleFIN Bridge hard limit — requests beyond this return an error
|
||||||
|
|
@ -7,11 +10,20 @@ const SYNC_DAYS_EFFECTIVE = 44; // 1-day buffer so request latency never tips
|
||||||
const SYNC_DAYS_DEFAULT = 30; // routine sync window (initial seed always uses SYNC_DAYS_EFFECTIVE)
|
const SYNC_DAYS_DEFAULT = 30; // routine sync window (initial seed always uses SYNC_DAYS_EFFECTIVE)
|
||||||
const SYNC_INTERVAL_DEFAULT = 4; // hours
|
const SYNC_INTERVAL_DEFAULT = 4; // hours
|
||||||
|
|
||||||
function getBankSyncConfig() {
|
interface BankSyncConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
sync_days: number;
|
||||||
|
seed_days: number;
|
||||||
|
sync_days_max: number;
|
||||||
|
sync_interval_hours: number;
|
||||||
|
debug_logging: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBankSyncConfig(): BankSyncConfig {
|
||||||
const dbValue = getSetting('bank_sync_enabled');
|
const dbValue = getSetting('bank_sync_enabled');
|
||||||
const envValue = process.env.BANK_SYNC_ENABLED;
|
const envValue = process.env.BANK_SYNC_ENABLED;
|
||||||
|
|
||||||
let enabled;
|
let enabled: boolean;
|
||||||
if (dbValue !== null && dbValue !== undefined && dbValue !== '') {
|
if (dbValue !== null && dbValue !== undefined && dbValue !== '') {
|
||||||
enabled = dbValue === 'true';
|
enabled = dbValue === 'true';
|
||||||
} else if (envValue !== undefined && envValue !== '') {
|
} else if (envValue !== undefined && envValue !== '') {
|
||||||
|
|
@ -51,13 +63,13 @@ function getBankSyncConfig() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBankSyncEnabled(enabled) {
|
function setBankSyncEnabled(enabled: boolean): BankSyncConfig {
|
||||||
setSetting('bank_sync_enabled', enabled ? 'true' : 'false');
|
setSetting('bank_sync_enabled', enabled ? 'true' : 'false');
|
||||||
return getBankSyncConfig();
|
return getBankSyncConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSyncIntervalHours(hours) {
|
function setSyncIntervalHours(hours: number | string): BankSyncConfig {
|
||||||
const n = parseFloat(hours);
|
const n = parseFloat(hours as string);
|
||||||
if (!Number.isFinite(n) || n < 0.5 || n > 168) {
|
if (!Number.isFinite(n) || n < 0.5 || n > 168) {
|
||||||
throw Object.assign(new Error('sync_interval_hours must be between 0.5 and 168'), { status: 400 });
|
throw Object.assign(new Error('sync_interval_hours must be between 0.5 and 168'), { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
@ -65,8 +77,8 @@ function setSyncIntervalHours(hours) {
|
||||||
return getBankSyncConfig();
|
return getBankSyncConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSyncDays(days) {
|
function setSyncDays(days: number | string): BankSyncConfig {
|
||||||
const n = parseInt(days, 10);
|
const n = parseInt(days as string, 10);
|
||||||
if (!Number.isFinite(n) || n < 1 || n > SYNC_DAYS_MAX) {
|
if (!Number.isFinite(n) || n < 1 || n > SYNC_DAYS_MAX) {
|
||||||
throw Object.assign(new Error(`sync_days must be between 1 and ${SYNC_DAYS_MAX} (SimpleFIN Bridge hard limit)`), { status: 400 });
|
throw Object.assign(new Error(`sync_days must be between 1 and ${SYNC_DAYS_MAX} (SimpleFIN Bridge hard limit)`), { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +86,7 @@ function setSyncDays(days) {
|
||||||
return getBankSyncConfig();
|
return getBankSyncConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDebugLogging(enabled) {
|
function setDebugLogging(enabled: boolean): BankSyncConfig {
|
||||||
setSetting('simplefin_debug_logging', enabled ? 'true' : 'false');
|
setSetting('simplefin_debug_logging', enabled ? 'true' : 'false');
|
||||||
return getBankSyncConfig();
|
return getBankSyncConfig();
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ const {
|
||||||
normalizeTransaction,
|
normalizeTransaction,
|
||||||
sanitizeErrorMessage,
|
sanitizeErrorMessage,
|
||||||
} = require('./simplefinService');
|
} = require('./simplefinService');
|
||||||
const { getBankSyncConfig, SYNC_DAYS_EFFECTIVE, SYNC_DAYS_DEFAULT } = require('./bankSyncConfigService');
|
const { getBankSyncConfig, SYNC_DAYS_EFFECTIVE, SYNC_DAYS_DEFAULT } = require('./bankSyncConfigService.cts');
|
||||||
const { decorateDataSource } = require('./transactionService');
|
const { decorateDataSource } = require('./transactionService');
|
||||||
const { applyMerchantRules } = require('./billMerchantRuleService.cts');
|
const { applyMerchantRules } = require('./billMerchantRuleService.cts');
|
||||||
const { applySpendingCategoryRules } = require('./spendingService.cts');
|
const { applySpendingCategoryRules } = require('./spendingService.cts');
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { getDb } = require('../db/database');
|
const { getDb } = require('../db/database');
|
||||||
const { getBankSyncConfig } = require('./bankSyncConfigService');
|
const { getBankSyncConfig } = require('./bankSyncConfigService.cts');
|
||||||
const { syncDataSource } = require('./bankSyncService');
|
const { syncDataSource } = require('./bankSyncService');
|
||||||
|
|
||||||
// Skip a source if it was synced less than this long ago (catches recent manual syncs)
|
// Skip a source if it was synced less than this long ago (catches recent manual syncs)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// import required so Node type-strips this .cts (it has no other import).
|
||||||
|
import type { Db } from '../types/db';
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
function parseBrowser(userAgent) {
|
function parseBrowser(userAgent: unknown): string {
|
||||||
const ua = String(userAgent || '');
|
const ua = String(userAgent || '');
|
||||||
if (/Edg\//i.test(ua)) return 'Edge';
|
if (/Edg\//i.test(ua)) return 'Edge';
|
||||||
if (/OPR\//i.test(ua) || /Opera/i.test(ua)) return 'Opera';
|
if (/OPR\//i.test(ua) || /Opera/i.test(ua)) return 'Opera';
|
||||||
|
|
@ -14,7 +17,7 @@ function parseBrowser(userAgent) {
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseOs(userAgent) {
|
function parseOs(userAgent: unknown): string {
|
||||||
const ua = String(userAgent || '');
|
const ua = String(userAgent || '');
|
||||||
if (/iPhone|iPad|iPod/i.test(ua)) return 'iOS';
|
if (/iPhone|iPad|iPod/i.test(ua)) return 'iOS';
|
||||||
if (/Android/i.test(ua)) return 'Android';
|
if (/Android/i.test(ua)) return 'Android';
|
||||||
|
|
@ -25,7 +28,7 @@ function parseOs(userAgent) {
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDeviceType(userAgent) {
|
function parseDeviceType(userAgent: unknown): string {
|
||||||
const ua = String(userAgent || '');
|
const ua = String(userAgent || '');
|
||||||
if (/iPad|Tablet/i.test(ua)) return 'tablet';
|
if (/iPad|Tablet/i.test(ua)) return 'tablet';
|
||||||
if (/Mobi|iPhone|Android/i.test(ua)) return 'mobile';
|
if (/Mobi|iPhone|Android/i.test(ua)) return 'mobile';
|
||||||
|
|
@ -33,7 +36,7 @@ function parseDeviceType(userAgent) {
|
||||||
return 'desktop';
|
return 'desktop';
|
||||||
}
|
}
|
||||||
|
|
||||||
function coarseIpPrefix(ipAddress) {
|
function coarseIpPrefix(ipAddress: unknown): string {
|
||||||
const ip = String(ipAddress || '').trim();
|
const ip = String(ipAddress || '').trim();
|
||||||
if (!ip) return '';
|
if (!ip) return '';
|
||||||
|
|
||||||
|
|
@ -47,7 +50,7 @@ function coarseIpPrefix(ipAddress) {
|
||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDeviceFingerprint({ userAgent, ipAddress }) {
|
function buildDeviceFingerprint({ userAgent, ipAddress }: { userAgent?: unknown; ipAddress?: unknown }) {
|
||||||
const browser = parseBrowser(userAgent);
|
const browser = parseBrowser(userAgent);
|
||||||
const os = parseOs(userAgent);
|
const os = parseOs(userAgent);
|
||||||
const deviceType = parseDeviceType(userAgent);
|
const deviceType = parseDeviceType(userAgent);
|
||||||
|
|
@ -6,7 +6,7 @@ const {
|
||||||
markNotificationError,
|
markNotificationError,
|
||||||
markNotificationSuccess,
|
markNotificationSuccess,
|
||||||
markNotificationTestSuccess,
|
markNotificationTestSuccess,
|
||||||
} = require('./statusRuntime');
|
} = require('./statusRuntime.cts');
|
||||||
const { localDateString } = require('../utils/dates.mts');
|
const { localDateString } = require('../utils/dates.mts');
|
||||||
const { fromCents } = require('../utils/money.mts');
|
const { fromCents } = require('../utils/money.mts');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// import required so Node type-strips this .cts (it has no other import).
|
||||||
|
import type { Db } from '../types/db';
|
||||||
|
|
||||||
const MAX_RECENT_ERRORS = 10;
|
const MAX_RECENT_ERRORS = 10;
|
||||||
|
|
||||||
// Keys written to the settings table for cross-restart persistence
|
// Keys written to the settings table for cross-restart persistence
|
||||||
|
|
@ -10,7 +13,18 @@ const DB_KEY = {
|
||||||
workerStarted: '_worker_started_at',
|
workerStarted: '_worker_started_at',
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
interface WorkerState {
|
||||||
|
enabled: boolean;
|
||||||
|
running: boolean;
|
||||||
|
started_at: string | null;
|
||||||
|
last_run_at: string | null;
|
||||||
|
next_run_at: string | null;
|
||||||
|
last_error: string | null;
|
||||||
|
}
|
||||||
|
interface NotificationsState { last_test_at: string | null; last_error: string | null }
|
||||||
|
interface RuntimeError { timestamp: string; source: string; message: string }
|
||||||
|
|
||||||
|
const state: { worker: WorkerState; notifications: NotificationsState; recentErrors: RuntimeError[] } = {
|
||||||
worker: {
|
worker: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
running: false,
|
running: false,
|
||||||
|
|
@ -28,7 +42,7 @@ const state = {
|
||||||
|
|
||||||
// Seed from DB on first load so status survives container restarts.
|
// Seed from DB on first load so status survives container restarts.
|
||||||
// Wrapped in try/catch — DB may not be ready at require-time in tests.
|
// Wrapped in try/catch — DB may not be ready at require-time in tests.
|
||||||
function seedFromDb() {
|
function seedFromDb(): void {
|
||||||
try {
|
try {
|
||||||
const { getSetting } = require('../db/database');
|
const { getSetting } = require('../db/database');
|
||||||
state.worker.last_run_at = getSetting(DB_KEY.workerLastRun) || null;
|
state.worker.last_run_at = getSetting(DB_KEY.workerLastRun) || null;
|
||||||
|
|
@ -40,27 +54,27 @@ function seedFromDb() {
|
||||||
}
|
}
|
||||||
seedFromDb();
|
seedFromDb();
|
||||||
|
|
||||||
function dbSet(key, value) {
|
function dbSet(key: string, value: unknown): void {
|
||||||
try {
|
try {
|
||||||
const { setSetting } = require('../db/database');
|
const { setSetting } = require('../db/database');
|
||||||
setSetting(key, value == null ? '' : String(value));
|
setSetting(key, value == null ? '' : String(value));
|
||||||
} catch { /* non-fatal */ }
|
} catch { /* non-fatal */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMessage(error) {
|
function toMessage(error: unknown): string | null {
|
||||||
if (!error) return null;
|
if (!error) return null;
|
||||||
if (typeof error === 'string') return error;
|
if (typeof error === 'string') return error;
|
||||||
return error.message || String(error);
|
return (error as any).message || String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordError(source, error) {
|
function recordError(source: string, error: unknown): void {
|
||||||
const message = toMessage(error);
|
const message = toMessage(error);
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
state.recentErrors.unshift({ timestamp: new Date().toISOString(), source, message });
|
state.recentErrors.unshift({ timestamp: new Date().toISOString(), source, message });
|
||||||
state.recentErrors = state.recentErrors.slice(0, MAX_RECENT_ERRORS);
|
state.recentErrors = state.recentErrors.slice(0, MAX_RECENT_ERRORS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markWorkerStarted(nextRunAt = null) {
|
function markWorkerStarted(nextRunAt: string | null = null): void {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
state.worker.enabled = true;
|
state.worker.enabled = true;
|
||||||
state.worker.running = true;
|
state.worker.running = true;
|
||||||
|
|
@ -70,7 +84,7 @@ function markWorkerStarted(nextRunAt = null) {
|
||||||
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
|
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markWorkerSuccess(nextRunAt = null) {
|
function markWorkerSuccess(nextRunAt: string | null = null): void {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
state.worker.running = false;
|
state.worker.running = false;
|
||||||
state.worker.last_run_at = now;
|
state.worker.last_run_at = now;
|
||||||
|
|
@ -81,7 +95,7 @@ function markWorkerSuccess(nextRunAt = null) {
|
||||||
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
|
if (nextRunAt) dbSet(DB_KEY.workerNextRun, nextRunAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markWorkerError(error, nextRunAt = null) {
|
function markWorkerError(error: unknown, nextRunAt: string | null = null): void {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
state.worker.running = false;
|
state.worker.running = false;
|
||||||
state.worker.last_run_at = now;
|
state.worker.last_run_at = now;
|
||||||
|
|
@ -93,17 +107,17 @@ function markWorkerError(error, nextRunAt = null) {
|
||||||
recordError('Daily Worker', error);
|
recordError('Daily Worker', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markNotificationTestSuccess() {
|
function markNotificationTestSuccess(): void {
|
||||||
state.notifications.last_test_at = new Date().toISOString();
|
state.notifications.last_test_at = new Date().toISOString();
|
||||||
state.notifications.last_error = null;
|
state.notifications.last_error = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markNotificationError(error) {
|
function markNotificationError(error: unknown): void {
|
||||||
state.notifications.last_error = toMessage(error);
|
state.notifications.last_error = toMessage(error);
|
||||||
recordError('Notifications', error);
|
recordError('Notifications', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markNotificationSuccess() {
|
function markNotificationSuccess(): void {
|
||||||
state.notifications.last_error = null;
|
state.notifications.last_error = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ const {
|
||||||
getTransactionForUser,
|
getTransactionForUser,
|
||||||
} = require('./transactionService');
|
} = require('./transactionService');
|
||||||
const { serializePayment } = require('./paymentValidation.cts');
|
const { serializePayment } = require('./paymentValidation.cts');
|
||||||
const { markMatched, markUnmatched, markIgnored } = require('./transactionMatchState');
|
const { markMatched, markUnmatched, markIgnored } = require('./transactionMatchState.cts');
|
||||||
|
|
||||||
const MATCH_PAYMENT_SOURCE = 'transaction_match';
|
const MATCH_PAYMENT_SOURCE = 'transaction_match';
|
||||||
const MATCH_PAYMENT_METHOD = 'transaction_match';
|
const MATCH_PAYMENT_METHOD = 'transaction_match';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import type { Db } from '../types/db';
|
||||||
|
|
||||||
// Canonical writers for a transaction's match state.
|
// Canonical writers for a transaction's match state.
|
||||||
//
|
//
|
||||||
// A transaction's match state lives in three columns that must move together:
|
// A transaction's match state lives in three columns that must move together:
|
||||||
|
|
@ -17,10 +19,10 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a transaction matched to a bill.
|
* Mark a transaction matched to a bill.
|
||||||
* @param {boolean} [opts.resetIgnored] also clear the ignored flag (used when
|
* @param resetIgnored also clear the ignored flag (used when matching a
|
||||||
* matching a transaction directly, which implicitly un-ignores it).
|
* transaction directly, which implicitly un-ignores it).
|
||||||
*/
|
*/
|
||||||
function markMatched(db, userId, transactionId, billId, { resetIgnored = false } = {}) {
|
function markMatched(db: Db, userId: number, transactionId: number, billId: number, { resetIgnored = false }: { resetIgnored?: boolean } = {}): number {
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
UPDATE transactions
|
UPDATE transactions
|
||||||
SET matched_bill_id = ?, match_status = 'matched'${resetIgnored ? ', ignored = 0' : ''}, updated_at = datetime('now')
|
SET matched_bill_id = ?, match_status = 'matched'${resetIgnored ? ', ignored = 0' : ''}, updated_at = datetime('now')
|
||||||
|
|
@ -30,9 +32,9 @@ function markMatched(db, userId, transactionId, billId, { resetIgnored = false }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear a transaction's match — back to 'unmatched' with no bill.
|
* Clear a transaction's match — back to 'unmatched' with no bill.
|
||||||
* @param {boolean} [opts.resetIgnored] also clear the ignored flag (un-ignore).
|
* @param resetIgnored also clear the ignored flag (un-ignore).
|
||||||
*/
|
*/
|
||||||
function markUnmatched(db, userId, transactionId, { resetIgnored = false } = {}) {
|
function markUnmatched(db: Db, userId: number, transactionId: number, { resetIgnored = false }: { resetIgnored?: boolean } = {}): number {
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
UPDATE transactions
|
UPDATE transactions
|
||||||
SET matched_bill_id = NULL, match_status = 'unmatched'${resetIgnored ? ', ignored = 0' : ''}, updated_at = datetime('now')
|
SET matched_bill_id = NULL, match_status = 'unmatched'${resetIgnored ? ', ignored = 0' : ''}, updated_at = datetime('now')
|
||||||
|
|
@ -41,7 +43,7 @@ function markUnmatched(db, userId, transactionId, { resetIgnored = false } = {})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark a transaction ignored — dropped from matching, no bill. */
|
/** Mark a transaction ignored — dropped from matching, no bill. */
|
||||||
function markIgnored(db, userId, transactionId) {
|
function markIgnored(db: Db, userId: number, transactionId: number): number {
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
UPDATE transactions
|
UPDATE transactions
|
||||||
SET ignored = 1, match_status = 'ignored', matched_bill_id = NULL, updated_at = datetime('now')
|
SET ignored = 1, match_status = 'ignored', matched_bill_id = NULL, updated_at = datetime('now')
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
* 5 minutes for errors) so the status page stays fast under load.
|
* 5 minutes for errors) so the status page stays fast under load.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// import required so Node type-strips this .cts (it has no other import).
|
||||||
|
import type { Db } from '../types/db';
|
||||||
|
|
||||||
const { getSetting } = require('../db/database');
|
const { getSetting } = require('../db/database');
|
||||||
|
|
||||||
const REPO_API_BASE = process.env.REPO_API_URL
|
const REPO_API_BASE = process.env.REPO_API_URL
|
||||||
|
|
@ -12,7 +15,7 @@ const REPO_API_BASE = process.env.REPO_API_URL
|
||||||
// QA-B16-01: the version check is opt-out-able. Admins can disable the external
|
// QA-B16-01: the version check is opt-out-able. Admins can disable the external
|
||||||
// request via the `update_check_enabled` setting (default on). When off, no
|
// request via the `update_check_enabled` setting (default on). When off, no
|
||||||
// network call is made and a `disabled` status is returned.
|
// network call is made and a `disabled` status is returned.
|
||||||
function updateCheckEnabled() {
|
function updateCheckEnabled(): boolean {
|
||||||
return getSetting('update_check_enabled') !== 'false';
|
return getSetting('update_check_enabled') !== 'false';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,10 +23,10 @@ const TTL_OK_MS = 60 * 60 * 1000; // 1 hour on success
|
||||||
const TTL_ERROR_MS = 5 * 60 * 1000; // 5 min on error (avoid hammering)
|
const TTL_ERROR_MS = 5 * 60 * 1000; // 5 min on error (avoid hammering)
|
||||||
const FETCH_TIMEOUT_MS = 8_000;
|
const FETCH_TIMEOUT_MS = 8_000;
|
||||||
|
|
||||||
let _cache = { result: null, expiresAt: 0 };
|
let _cache: { result: any; expiresAt: number } = { result: null, expiresAt: 0 };
|
||||||
let _pkg = null;
|
let _pkg: any = null;
|
||||||
|
|
||||||
function getCurrentVersion() {
|
function getCurrentVersion(): string {
|
||||||
if (!_pkg) {
|
if (!_pkg) {
|
||||||
try { _pkg = require('../package.json'); } catch { _pkg = { version: '0.0.0' }; }
|
try { _pkg = require('../package.json'); } catch { _pkg = { version: '0.0.0' }; }
|
||||||
}
|
}
|
||||||
|
|
@ -31,8 +34,8 @@ function getCurrentVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns positive if a > b, negative if a < b, 0 if equal.
|
// Returns positive if a > b, negative if a < b, 0 if equal.
|
||||||
function compareVersions(a, b) {
|
function compareVersions(a: string, b: string): number {
|
||||||
const parse = v => String(v).replace(/^v/, '').split('.').map(Number);
|
const parse = (v: string) => String(v).replace(/^v/, '').split('.').map(Number);
|
||||||
const pa = parse(a), pb = parse(b);
|
const pa = parse(a), pb = parse(b);
|
||||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||||
const diff = (pa[i] || 0) - (pb[i] || 0);
|
const diff = (pa[i] || 0) - (pb[i] || 0);
|
||||||
|
|
@ -42,10 +45,10 @@ function compareVersions(a, b) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} force Skip the cache and always hit the API.
|
* @param force Skip the cache and always hit the API.
|
||||||
* @returns {Promise<Object>} Update status object.
|
* @returns Update status object.
|
||||||
*/
|
*/
|
||||||
async function checkForUpdates(force = false) {
|
async function checkForUpdates(force = false): Promise<any> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Opt-out: no external request when disabled by the admin.
|
// Opt-out: no external request when disabled by the admin.
|
||||||
|
|
@ -99,7 +102,7 @@ async function checkForUpdates(force = false) {
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`Forgejo API returned HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`Forgejo API returned HTTP ${res.status}`);
|
||||||
|
|
||||||
const release = await res.json();
|
const release: any = await res.json();
|
||||||
const rawTag = release.tag_name || '';
|
const rawTag = release.tag_name || '';
|
||||||
const latestVersion = rawTag.replace(/^v/, '') || null;
|
const latestVersion = rawTag.replace(/^v/, '') || null;
|
||||||
const hasUpdate = latestVersion
|
const hasUpdate = latestVersion
|
||||||
|
|
@ -121,7 +124,7 @@ async function checkForUpdates(force = false) {
|
||||||
_cache = { result, expiresAt: now + TTL_OK_MS };
|
_cache = { result, expiresAt: now + TTL_OK_MS };
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
const result = {
|
const result = {
|
||||||
current_version: currentVersion,
|
current_version: currentVersion,
|
||||||
latest_version: null,
|
latest_version: null,
|
||||||
|
|
@ -13,7 +13,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-match-state-${process.pid}.s
|
||||||
process.env.DB_PATH = dbPath;
|
process.env.DB_PATH = dbPath;
|
||||||
|
|
||||||
const { getDb, closeDb } = require('../db/database');
|
const { getDb, closeDb } = require('../db/database');
|
||||||
const { markMatched, markUnmatched, markIgnored } = require('../services/transactionMatchState');
|
const { markMatched, markUnmatched, markIgnored } = require('../services/transactionMatchState.cts');
|
||||||
|
|
||||||
let db, userId, otherId, billId;
|
let db, userId, otherId, billId;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-updatecheck-test-${process.p
|
||||||
process.env.DB_PATH = dbPath;
|
process.env.DB_PATH = dbPath;
|
||||||
|
|
||||||
const { getDb, setSetting, closeDb } = require('../db/database');
|
const { getDb, setSetting, closeDb } = require('../db/database');
|
||||||
const svc = require('../services/updateCheckService');
|
const svc = require('../services/updateCheckService.cts');
|
||||||
|
|
||||||
test('disabled: makes no external request and returns disabled', async () => {
|
test('disabled: makes no external request and returns disabled', async () => {
|
||||||
getDb();
|
getDb();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Minimal permissive Express-ish request/response types for the migrated routes
|
||||||
|
// and middleware. @types/express is not installed and the handlers are dynamic
|
||||||
|
// (req.user is added by requireAuth, req.body/query are user input), so these
|
||||||
|
// carry an index signature — typed enough for `.json()/.status()` chaining and
|
||||||
|
// the common fields, permissive everywhere else. Swap for @types/express +
|
||||||
|
// declaration-merged `user` if the routes ever warrant full typing.
|
||||||
|
|
||||||
|
export interface Req {
|
||||||
|
body: any;
|
||||||
|
params: any;
|
||||||
|
query: any;
|
||||||
|
user?: any;
|
||||||
|
ip?: string;
|
||||||
|
method: string;
|
||||||
|
path?: string;
|
||||||
|
originalUrl?: string;
|
||||||
|
headers: any;
|
||||||
|
cookies?: any;
|
||||||
|
signedCookies?: any;
|
||||||
|
secure?: boolean;
|
||||||
|
csrfSkip?: boolean;
|
||||||
|
file?: any;
|
||||||
|
files?: any;
|
||||||
|
get(name: string): string | undefined;
|
||||||
|
header(name: string): string | undefined;
|
||||||
|
[k: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Res {
|
||||||
|
json(body?: any): Res;
|
||||||
|
status(code: number): Res;
|
||||||
|
send(body?: any): Res;
|
||||||
|
end(body?: any): Res;
|
||||||
|
cookie(name: string, value: any, options?: any): Res;
|
||||||
|
clearCookie(name: string, options?: any): Res;
|
||||||
|
setHeader(name: string, value: any): void;
|
||||||
|
getHeader(name: string): any;
|
||||||
|
set(field: string, value?: any): Res;
|
||||||
|
type(t: string): Res;
|
||||||
|
redirect(url: string): void;
|
||||||
|
download(path: string, filename?: string, cb?: (err?: any) => void): void;
|
||||||
|
sendFile(path: string, options?: any, cb?: (err?: any) => void): void;
|
||||||
|
attachment(filename?: string): Res;
|
||||||
|
locals: any;
|
||||||
|
headersSent: boolean;
|
||||||
|
[k: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Next = (err?: any) => void;
|
||||||
|
|
||||||
|
// An Express router (the shape `express.Router()` returns) — permissive.
|
||||||
|
export interface Router {
|
||||||
|
get(path: string, ...handlers: any[]): void;
|
||||||
|
post(path: string, ...handlers: any[]): void;
|
||||||
|
put(path: string, ...handlers: any[]): void;
|
||||||
|
patch(path: string, ...handlers: any[]): void;
|
||||||
|
delete(path: string, ...handlers: any[]): void;
|
||||||
|
use(...handlers: any[]): void;
|
||||||
|
[k: string]: any;
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,15 @@
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Req, Res, Next } from '../types/http';
|
||||||
|
|
||||||
class ApiError extends Error {
|
class ApiError extends Error {
|
||||||
constructor(code, message, status = 500, details = {}) {
|
code: string;
|
||||||
|
status: number;
|
||||||
|
details: any;
|
||||||
|
field?: string;
|
||||||
|
|
||||||
|
constructor(code: string, message: string, status = 500, details: any = {}) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'ApiError';
|
this.name = 'ApiError';
|
||||||
this.code = code;
|
this.code = code;
|
||||||
|
|
@ -28,42 +35,42 @@ class ApiError extends Error {
|
||||||
/**
|
/**
|
||||||
* Create a standardized validation error
|
* Create a standardized validation error
|
||||||
*/
|
*/
|
||||||
function ValidationError(message, field, code = 'VALIDATION_ERROR') {
|
function ValidationError(message: string, field?: string, code = 'VALIDATION_ERROR'): ApiError {
|
||||||
return new ApiError(code, message, 400, { field });
|
return new ApiError(code, message, 400, { field });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a standardized authentication error
|
* Create a standardized authentication error
|
||||||
*/
|
*/
|
||||||
function AuthError(message = 'Authentication required', code = 'AUTH_ERROR') {
|
function AuthError(message = 'Authentication required', code = 'AUTH_ERROR'): ApiError {
|
||||||
return new ApiError(code, message, 401);
|
return new ApiError(code, message, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a standardized authorization error
|
* Create a standardized authorization error
|
||||||
*/
|
*/
|
||||||
function ForbiddenError(message = 'Access denied', code = 'FORBIDDEN') {
|
function ForbiddenError(message = 'Access denied', code = 'FORBIDDEN'): ApiError {
|
||||||
return new ApiError(code, message, 403);
|
return new ApiError(code, message, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a standardized not found error
|
* Create a standardized not found error
|
||||||
*/
|
*/
|
||||||
function NotFoundError(message = 'Resource not found', code = 'NOT_FOUND') {
|
function NotFoundError(message = 'Resource not found', code = 'NOT_FOUND'): ApiError {
|
||||||
return new ApiError(code, message, 404);
|
return new ApiError(code, message, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a standardized conflict error
|
* Create a standardized conflict error
|
||||||
*/
|
*/
|
||||||
function ConflictError(message = 'Resource conflict', code = 'CONFLICT') {
|
function ConflictError(message = 'Resource conflict', code = 'CONFLICT'): ApiError {
|
||||||
return new ApiError(code, message, 409);
|
return new ApiError(code, message, 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a standardized rate limit error
|
* Create a standardized rate limit error
|
||||||
*/
|
*/
|
||||||
function RateLimitError(message = 'Too many requests', code = 'RATE_LIMITED') {
|
function RateLimitError(message = 'Too many requests', code = 'RATE_LIMITED'): ApiError {
|
||||||
return new ApiError(code, message, 429);
|
return new ApiError(code, message, 429);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,9 +78,9 @@ function RateLimitError(message = 'Too many requests', code = 'RATE_LIMITED') {
|
||||||
* Format an error for JSON response
|
* Format an error for JSON response
|
||||||
* This ensures consistent error format across all routes
|
* This ensures consistent error format across all routes
|
||||||
*/
|
*/
|
||||||
function formatError(err) {
|
function formatError(err: any): { error: string; message: string; field?: string } {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
const output = {
|
const output: { error: string; message: string; field?: string } = {
|
||||||
error: err.code,
|
error: err.code,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
};
|
};
|
||||||
|
|
@ -95,7 +102,7 @@ function formatError(err) {
|
||||||
/**
|
/**
|
||||||
* Express middleware to handle errors centrally
|
* Express middleware to handle errors centrally
|
||||||
*/
|
*/
|
||||||
function errorHandler(err, req, res, next) {
|
function errorHandler(err: any, req: Req, res: Res, next: Next): void {
|
||||||
const formatted = formatError(err);
|
const formatted = formatError(err);
|
||||||
const status = err.status || (err instanceof ApiError ? 500 : 500);
|
const status = err.status || (err instanceof ApiError ? 500 : 500);
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ const {
|
||||||
markWorkerError,
|
markWorkerError,
|
||||||
markWorkerStarted,
|
markWorkerStarted,
|
||||||
markWorkerSuccess,
|
markWorkerSuccess,
|
||||||
} = require('../services/statusRuntime');
|
} = require('../services/statusRuntime.cts');
|
||||||
const { localDateString, localDateStringDaysAgo } = require('../utils/dates.mts');
|
const { localDateString, localDateStringDaysAgo } = require('../utils/dates.mts');
|
||||||
|
|
||||||
// The hour (0–23) the single daily job runs — configurable via the global
|
// The hour (0–23) the single daily job runs — configurable via the global
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue