refactor(server): migrate middleware, workers, and db layer to TypeScript (.cts)
- middleware/ (5): requireAuth, securityHeaders, errorFormatter, csrf, rateLimiter — fully typed against the shared http Req/Res/Next types. - workers/dailyWorker — fully typed. - db/ (4): database, subscriptionCatalogSeed, migrations/versionedMigrations, migrations/legacyReconcileMigrations — large dynamic schema/migration infra, converted with @ts-nocheck (typing deferred). All requires updated to .cts. 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
866ba52890
commit
135bd98c6a
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra).
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
|
@ -74,11 +75,11 @@ function isValidSqlDefinition(def) {
|
|||
}
|
||||
|
||||
// Subscription catalog seed rows live in their own module (pure data, no logic).
|
||||
const { SUBSCRIPTION_CATALOG_ROWS, SUBSCRIPTION_CATALOG_V2_ROWS } = require('./subscriptionCatalogSeed');
|
||||
const { SUBSCRIPTION_CATALOG_ROWS, SUBSCRIPTION_CATALOG_V2_ROWS } = require('./subscriptionCatalogSeed.cts');
|
||||
// The versioned migration list lives in its own module (a factory injected with db + helpers).
|
||||
const buildVersionedMigrations = require('./migrations/versionedMigrations');
|
||||
const buildVersionedMigrations = require('./migrations/versionedMigrations.cts');
|
||||
// The legacy-reconcile migration list (same factory pattern, legacy DBs only).
|
||||
const buildLegacyReconcileMigrations = require('./migrations/legacyReconcileMigrations');
|
||||
const buildLegacyReconcileMigrations = require('./migrations/legacyReconcileMigrations.cts');
|
||||
|
||||
function runSubscriptionCatalogV2Migration(database) {
|
||||
// Category fixes for existing rows
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra).
|
||||
'use strict';
|
||||
|
||||
// The legacy-reconcile migration list, extracted from db/database.js. Same
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra).
|
||||
'use strict';
|
||||
|
||||
// The versioned migration list, extracted from db/database.js to keep that
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck — .cts via Node type-stripping; rich typing deferred (large dynamic DB/migration infra).
|
||||
'use strict';
|
||||
|
||||
// Seed rows for the subscription_catalog table, extracted from db/database.js
|
||||
|
|
@ -1,60 +1,37 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { logAudit } = require('../services/auditService.cts');
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// CSRF Middleware
|
||||
// Protects state-changing routes (POST, PUT, DELETE) from cross-site request
|
||||
// forgery by validating tokens stored in session cookie.
|
||||
// CSRF Middleware — double-submit token protection for state-changing routes.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const CSRF_HEADER_NAME = 'x-csrf-token';
|
||||
|
||||
// CSRF cookie httpOnly setting - configurable via environment variable
|
||||
// Default: true — the SPA fetches the token from GET /api/auth/csrf-token and stores
|
||||
// it in memory, so JavaScript does not need direct access to document.cookie.
|
||||
// httpOnly=true removes the token from the XSS-accessible cookie surface while
|
||||
// preserving the double-submit validation on the server.
|
||||
const CSRF_HTTP_ONLY = process.env.CSRF_HTTP_ONLY !== 'false'; // defaults to true
|
||||
|
||||
// CSRF cookie sameSite setting - configurable via environment variable
|
||||
// Options: 'lax', 'strict', 'none'
|
||||
// Default: 'strict' (most secure)
|
||||
// Set CSRF_SAME_SITE=lax for SPA cross-site scenarios
|
||||
const CSRF_SAME_SITE = process.env.CSRF_SAME_SITE || 'strict';
|
||||
|
||||
// CSRF cookie secure setting - configurable via environment variable
|
||||
// Default: true (only send over HTTPS)
|
||||
// Set CSRF_SECURE=false for HTTP development (NOT recommended for production)
|
||||
const CSRF_SECURE = process.env.CSRF_SECURE !== 'false'; // defaults to true
|
||||
|
||||
// CSRF cookie name - configurable via environment variable
|
||||
// Default: 'bt_csrf_token'
|
||||
// Use CSRF_COOKIE_NAME to customize for multi-app deployments
|
||||
const CSRF_COOKIE_NAME = process.env.CSRF_COOKIE_NAME || 'bt_csrf_token';
|
||||
|
||||
// Generate a cryptographically secure CSRF token
|
||||
function generateCsrfToken() {
|
||||
function generateCsrfToken(): string {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect HTTPS the same way services/authService.cookieOpts does:
|
||||
* req.secure (honors trust proxy) with an x-forwarded-proto fallback for
|
||||
* deployments where TRUST_PROXY is not configured.
|
||||
* Detect HTTPS the same way services/authService.cookieOpts does.
|
||||
*/
|
||||
function requestLooksHttps(req) {
|
||||
function requestLooksHttps(req?: Req): boolean {
|
||||
if (!req) return false;
|
||||
if (req.secure) return true;
|
||||
const proto = req.get?.('x-forwarded-proto') || req.headers?.['x-forwarded-proto'];
|
||||
return String(proto || '').split(',').map(s => s.trim()).includes('https');
|
||||
return String(proto || '').split(',').map((s: string) => s.trim()).includes('https');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create CSRF token for the current session.
|
||||
* In the SPA's double-submit flow, tokens are stored in a readable cookie so
|
||||
* client/api.js can copy the value into the x-csrf-token header.
|
||||
* Get or create CSRF token for the current session (readable double-submit cookie).
|
||||
*/
|
||||
function getCsrfToken(req, res) {
|
||||
function getCsrfToken(req: Req, res: Res): string {
|
||||
let token = req.cookies?.[CSRF_COOKIE_NAME];
|
||||
|
||||
if (!token) {
|
||||
|
|
@ -71,14 +48,9 @@ function getCsrfToken(req, res) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate CSRF token from request.
|
||||
* Tokens can be provided via:
|
||||
* - x-csrf-token header (API clients)
|
||||
* - csrf_token body field (form submissions)
|
||||
* Query-parameter tokens are deliberately NOT accepted — URLs leak into
|
||||
* access logs, browser history, and Referer headers.
|
||||
* Validate CSRF token from request (x-csrf-token header or csrf_token body field).
|
||||
*/
|
||||
function validateCsrfToken(req) {
|
||||
function validateCsrfToken(req: Req): boolean {
|
||||
const cookieToken = req.cookies?.[CSRF_COOKIE_NAME];
|
||||
if (!cookieToken) return false;
|
||||
|
||||
|
|
@ -92,36 +64,27 @@ function validateCsrfToken(req) {
|
|||
}
|
||||
|
||||
/**
|
||||
* CSRF middleware - validates tokens for state-changing methods.
|
||||
* Skips validation for: GET, HEAD, OPTIONS (safe methods)
|
||||
* Requires token for: POST, PUT, DELETE, PATCH (state-changing)
|
||||
* CSRF middleware — validates tokens for state-changing methods.
|
||||
*/
|
||||
function csrfMiddleware(req, res, next) {
|
||||
function csrfMiddleware(req: Req, res: Res, next: Next): any {
|
||||
// Exempt the login endpoint only — no session exists yet to hijack.
|
||||
// Compare against originalUrl (sans query string) so a "/login" subpath on
|
||||
// some other mounted router is NOT accidentally exempted.
|
||||
const fullPath = (req.originalUrl || '').split('?')[0];
|
||||
if (fullPath === '/api/auth/login') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Only validate state-changing methods
|
||||
if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Skip validation for OPTIONS (preflight)
|
||||
if (req.method === 'OPTIONS') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Allow API routes to opt-in explicitly via a flag
|
||||
// This allows flexibility for routes that use alternate auth (e.g., API keys)
|
||||
if (req.csrfSkip) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Validate the CSRF token
|
||||
if (!validateCsrfToken(req)) {
|
||||
logAudit({ user_id: req.user?.id || null, action: 'csrf.failure', ip_address: req.ip, user_agent: req.get('user-agent') });
|
||||
return res.status(403).json({
|
||||
|
|
@ -136,9 +99,8 @@ function csrfMiddleware(req, res, next) {
|
|||
|
||||
/**
|
||||
* Attach CSRF token to response locals for template rendering.
|
||||
* Frontend can access req.csrfToken() in templates.
|
||||
*/
|
||||
function csrfTokenProvider(req, res, next) {
|
||||
function csrfTokenProvider(req: Req, res: Res, next: Next): void {
|
||||
res.locals.csrfToken = getCsrfToken(req, res);
|
||||
next();
|
||||
}
|
||||
|
|
@ -2,65 +2,51 @@
|
|||
* Centralized Error Formatter Middleware
|
||||
*
|
||||
* Standard error response format:
|
||||
* {
|
||||
* "error": "ValidationError",
|
||||
* "message": "Human-readable description",
|
||||
* "field": "optional-field-name",
|
||||
* "code": "machine-readable-code"
|
||||
* }
|
||||
* { "error", "message", "field", "code" }
|
||||
*/
|
||||
|
||||
import type { Req, Res, Next } from '../types/http';
|
||||
|
||||
const { ValidationError, formatError } = require('../utils/apiError.cts');
|
||||
|
||||
/**
|
||||
* Extract field name from various validation error patterns
|
||||
*/
|
||||
function extractFieldFromError(message) {
|
||||
/** Extract field name from various validation error patterns */
|
||||
function extractFieldFromError(message: any): string | null {
|
||||
if (!message) return null;
|
||||
|
||||
// Patterns like "field must be ..." or "field is ..."
|
||||
const fieldMatch = message.match(/^(\w+)\s+(?:must be|is|are)\s+/i);
|
||||
if (fieldMatch) return fieldMatch[1];
|
||||
|
||||
// Patterns like "year must be..." or "month must be..."
|
||||
const yearMonthMatch = message.match(/^(year|month|due_day|interest_rate|actual_amount|start_year|start_month|end_year|end_month)\s+must\b/i);
|
||||
if (yearMonthMatch) return yearMonthMatch[1];
|
||||
|
||||
// Patterns like "name is required"
|
||||
const requiredMatch = message.match(/^(username|password|name|bill_id|category_id|due_day)\s+(?:is|are)\s+required/i);
|
||||
if (requiredMatch) return requiredMatch[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a plain error message/string into a standardized error object
|
||||
*/
|
||||
function standardizeError(message, code = 'VALIDATION_ERROR', fieldOverride = null) {
|
||||
/** Convert a plain error message/string into a standardized error object */
|
||||
function standardizeError(message: any, code = 'VALIDATION_ERROR', fieldOverride: any = null) {
|
||||
const field = fieldOverride || extractFieldFromError(message);
|
||||
|
||||
return {
|
||||
error: code,
|
||||
message: typeof message === 'string' ? message : String(message),
|
||||
field: field || null,
|
||||
code: code
|
||||
code: code,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Express middleware that ensures all error responses follow standard format
|
||||
*/
|
||||
function errorFormatter(req, res, next) {
|
||||
/** Express middleware that ensures all error responses follow standard format */
|
||||
function errorFormatter(req: Req, res: Res, next: Next): void {
|
||||
const originalJson = res.json;
|
||||
|
||||
res.json = function(data) {
|
||||
// If data is an error object (has error property), standardize it
|
||||
res.json = function(this: any, data: any) {
|
||||
if (data && typeof data === 'object' && !Array.isArray(data) && data.error && !data.success) {
|
||||
// Already standardized (machine-readable code + human message) — pass through
|
||||
if (typeof data.code === 'string' && data.code && typeof data.message === 'string') {
|
||||
return originalJson.call(this, data);
|
||||
}
|
||||
// Use the human text as the message, never as the machine code
|
||||
const message = (typeof data.message === 'string' && data.message) ? data.message : data.error;
|
||||
const code = (typeof data.code === 'string' && data.code) ? data.code : 'ERROR';
|
||||
const standardized = standardizeError(message, code, data.field);
|
||||
|
|
@ -72,31 +58,19 @@ function errorFormatter(req, res, next) {
|
|||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format validation errors with proper field extraction
|
||||
*/
|
||||
function formatValidationError(message, field) {
|
||||
function formatValidationError(message: any, field?: any) {
|
||||
return standardizeError(message, 'VALIDATION_ERROR', field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format not found errors
|
||||
*/
|
||||
function formatNotFoundError(message, field) {
|
||||
function formatNotFoundError(message: any, field?: any) {
|
||||
return standardizeError(message, 'NOT_FOUND', field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format authentication errors
|
||||
*/
|
||||
function formatAuthError(message) {
|
||||
function formatAuthError(message: any) {
|
||||
return standardizeError(message, 'AUTH_ERROR');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format forbidden/access errors
|
||||
*/
|
||||
function formatForbiddenError(message) {
|
||||
function formatForbiddenError(message: any) {
|
||||
return standardizeError(message, 'FORBIDDEN');
|
||||
}
|
||||
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
import type { Req, Res } from '../types/http';
|
||||
|
||||
const { rateLimit, ipKeyGenerator } = require('express-rate-limit');
|
||||
|
||||
function makeLimiter(max, windowMs, message) {
|
||||
function makeLimiter(max: number, windowMs: number, message: string): any {
|
||||
return rateLimit({
|
||||
windowMs,
|
||||
max,
|
||||
standardHeaders: 'draft-7',
|
||||
legacyHeaders: false,
|
||||
// Override default handler so the response is always JSON, not HTML
|
||||
handler(req, res) {
|
||||
handler(req: Req, res: Res) {
|
||||
res.status(429).json({ error: message });
|
||||
},
|
||||
});
|
||||
|
|
@ -21,21 +23,19 @@ const loginLimiter = makeLimiter(
|
|||
'Too many login attempts. Please try again in 15 minutes.',
|
||||
);
|
||||
|
||||
// 5 FAILED login attempts per 15 minutes per username — layered on top of the
|
||||
// per-IP limiter so a distributed attacker (or many clients behind one NAT/proxy
|
||||
// sharing an IP bucket) cannot brute-force a single account. Successful logins
|
||||
// don't count toward the limit, so legitimate users are unaffected.
|
||||
// 5 FAILED login attempts per 15 minutes per username — layered on the per-IP
|
||||
// limiter. Successful logins don't count, so legit users are unaffected.
|
||||
const loginUsernameLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
standardHeaders: 'draft-7',
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: true,
|
||||
keyGenerator: (req) => {
|
||||
keyGenerator: (req: Req) => {
|
||||
const username = String(req.body?.username || '').trim().toLowerCase();
|
||||
return username ? `user:${username}` : ipKeyGenerator(req);
|
||||
},
|
||||
handler(req, res) {
|
||||
handler(req: Req, res: Res) {
|
||||
res.status(429).json({ error: 'Too many failed login attempts for this account. Please try again in 15 minutes.' });
|
||||
},
|
||||
});
|
||||
|
|
@ -88,8 +88,8 @@ const syncLimiter = rateLimit({
|
|||
max: 10,
|
||||
standardHeaders: 'draft-7',
|
||||
legacyHeaders: false,
|
||||
keyGenerator: (req) => req.user?.id?.toString() || ipKeyGenerator(req),
|
||||
handler(req, res) {
|
||||
keyGenerator: (req: Req) => req.user?.id?.toString() || ipKeyGenerator(req),
|
||||
handler(req: Req, res: Res) {
|
||||
res.status(429).json({ error: 'Too many sync requests. Please try again in 15 minutes.' });
|
||||
},
|
||||
});
|
||||
|
|
@ -108,7 +108,7 @@ const allLimiters = [
|
|||
syncLimiter,
|
||||
];
|
||||
|
||||
function resetStores() {
|
||||
function resetStores(): void {
|
||||
for (const limiter of allLimiters) {
|
||||
if (limiter.store.reset) {
|
||||
limiter.store.reset();
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { getSessionUser, COOKIE_NAME, SINGLE_COOKIE_NAME, cookieOpts, publicUser, recordLogin } = require('../services/authService.cts');
|
||||
const { getDb, getSetting } = require('../db/database');
|
||||
const { standardizeError } = require('./errorFormatter');
|
||||
const { getDb, getSetting } = require('../db/database.cts');
|
||||
const { standardizeError } = require('./errorFormatter.cts');
|
||||
|
||||
function getSingleModeUser() {
|
||||
function getSingleModeUser(): any {
|
||||
if (getSetting('auth_mode') !== 'single') return null;
|
||||
const userId = getSetting('default_user_id');
|
||||
if (!userId) return null;
|
||||
// Single-user mode: validate only that the configured user exists,
|
||||
// is active, and has role 'user'. Sessions are not relevant here —
|
||||
// single-user mode bypasses session auth entirely.
|
||||
// Single-user mode: validate only that the configured user exists, is active,
|
||||
// and has role 'user'. Sessions are not relevant here.
|
||||
const row = getDb().prepare(`
|
||||
SELECT id, username, display_name, role, must_change_password, first_login,
|
||||
active, is_default_admin, last_seen_version
|
||||
|
|
@ -19,7 +20,7 @@ function getSingleModeUser() {
|
|||
return row ? publicUser(row) : null;
|
||||
}
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
function requireAuth(req: Req, res: Res, next: Next): any {
|
||||
// Single-user mode: bypass session entirely, auto-attach the default user
|
||||
const singleUser = getSingleModeUser();
|
||||
if (singleUser) {
|
||||
|
|
@ -27,7 +28,6 @@ function requireAuth(req, res, next) {
|
|||
req.singleUserMode = true;
|
||||
|
||||
// Track logins via a presence cookie so login history works without a real session.
|
||||
// A new cookie = new browser/device visit → record a login entry.
|
||||
const existing = req.cookies?.[SINGLE_COOKIE_NAME];
|
||||
if (existing) {
|
||||
req.singleSessionId = existing;
|
||||
|
|
@ -56,7 +56,7 @@ function requireAuth(req, res, next) {
|
|||
next();
|
||||
}
|
||||
|
||||
function requireUser(req, res, next) {
|
||||
function requireUser(req: Req, res: Res, next: Next): any {
|
||||
if (req.user?.is_default_admin) {
|
||||
return res.status(403).json(standardizeError('Default admin account does not have tracker access', 'FORBIDDEN'));
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ function requireUser(req, res, next) {
|
|||
next();
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
function requireAdmin(req: Req, res: Res, next: Next): any {
|
||||
// In single-user mode the auto-attached user is never admin,
|
||||
// so admin routes naturally stay protected by session.
|
||||
if (req.user?.role !== 'admin') {
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
import type { Req, Res, Next } from '../types/http';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Generates a secure nonce for CSP policy.
|
||||
* Call once per request to get a unique nonce.
|
||||
*/
|
||||
function getCspNonce(req) {
|
||||
/** Generates a secure nonce for CSP policy. Call once per request. */
|
||||
function getCspNonce(req: Req): string {
|
||||
if (!req.cspNonce) {
|
||||
req.cspNonce = crypto.randomBytes(16).toString('base64');
|
||||
}
|
||||
|
|
@ -15,15 +14,9 @@ function getCspNonce(req) {
|
|||
|
||||
/**
|
||||
* Applies baseline security response headers on every request.
|
||||
*
|
||||
* CSP notes:
|
||||
* - No nonces. index.html is served via sendFile/static, so a per-request nonce
|
||||
* is never injected into the markup and would accomplish nothing for scripts.
|
||||
* Worse, per CSP3 the mere presence of a nonce makes 'unsafe-inline' ignored,
|
||||
* which would silently block the inline styles Tailwind/Radix/framer-motion
|
||||
* rely on. All scripts are external and covered by 'self'.
|
||||
* (See original notes on why CSP uses no nonce — index.html is static.)
|
||||
*/
|
||||
function securityHeaders(req, res, next) {
|
||||
function securityHeaders(req: Req, res: Res, next: Next): void {
|
||||
const cspPolicy =
|
||||
`default-src 'self'; ` +
|
||||
`script-src 'self'; ` +
|
||||
|
|
@ -37,24 +30,13 @@ function securityHeaders(req, res, next) {
|
|||
`object-src 'none';`;
|
||||
|
||||
res.setHeader('Content-Security-Policy', cspPolicy);
|
||||
|
||||
// Prevent MIME-type sniffing (browsers must respect Content-Type)
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// Disallow embedding in iframes from other origins — prevents clickjacking
|
||||
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
|
||||
// Don't send full URL in Referer header to third-party origins
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// Disallow cross-domain policy files (Flash/PDF legacy attack surface)
|
||||
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
|
||||
|
||||
// Don't advertise the server tech stack
|
||||
res.removeHeader('X-Powered-By');
|
||||
|
||||
// HSTS — only set when the app is explicitly configured to run over HTTPS.
|
||||
// Setting this on HTTP breaks the site. Enable with HTTPS=true env var.
|
||||
// HSTS — only when explicitly configured to run over HTTPS.
|
||||
if (process.env.HTTPS === 'true') {
|
||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import type { Req, Res, Next } from '../types/http';
|
|||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/requireAuth');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/requireAuth.cts');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -490,7 +490,7 @@ router.post('/check-updates', requireAuth, requireAdmin, async (req: Req, res: R
|
|||
});
|
||||
|
||||
// QA-B16-01: opt-out control for the external version check.
|
||||
const { getSetting, setSetting } = require('../db/database');
|
||||
const { getSetting, setSetting } = require('../db/database.cts');
|
||||
|
||||
// GET /api/about-admin/update-check-setting — is the external version check enabled?
|
||||
router.get('/update-check-setting', requireAuth, requireAdmin, (req: Req, res: Res) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb, rollbackMigration } = require('../db/database');
|
||||
const { getDb, rollbackMigration } = require('../db/database.cts');
|
||||
const { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays, setDebugLogging } = require('../services/bankSyncConfigService.cts');
|
||||
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts');
|
||||
const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts');
|
||||
|
|
@ -26,7 +26,7 @@ const {
|
|||
runAllCleanup,
|
||||
validateAndApplySettings: applyCleanupSettings,
|
||||
} = require('../services/cleanupService.cts');
|
||||
const { backupOperationLimiter } = require('../middleware/rateLimiter');
|
||||
const { backupOperationLimiter } = require('../middleware/rateLimiter.cts');
|
||||
|
||||
// All routes mounted at /api/admin (requireAuth + requireAdmin applied at server level)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const { getAnalyticsSummary } = require('../services/analyticsService.cts');
|
||||
|
||||
router.get('/summary', (req: Req, res: Res) => {
|
||||
|
|
|
|||
|
|
@ -11,15 +11,15 @@ function getAppVersion() {
|
|||
return _appVersion;
|
||||
}
|
||||
|
||||
const { getDb, getSetting, setSetting } = require('../db/database');
|
||||
const { getDb, getSetting, setSetting } = require('../db/database.cts');
|
||||
const { login, logout, hashPassword, cookieOpts, COOKIE_NAME, SINGLE_COOKIE_NAME, rotateSessionId, invalidateOtherSessions, recordLogin, recordFailedLogin } = require('../services/authService.cts');
|
||||
const { decryptSecret } = require('../services/encryptionService.cts');
|
||||
const { getCsrfToken } = require('../middleware/csrf');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/requireAuth');
|
||||
const { getCsrfToken } = require('../middleware/csrf.cts');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/requireAuth.cts');
|
||||
const { getPublicOidcInfo } = require('../services/oidcService.cts');
|
||||
const { ValidationError, formatError } = require('../utils/apiError.cts');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { passwordLimiter } = require('../middleware/rateLimiter');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const { passwordLimiter } = require('../middleware/rateLimiter.cts');
|
||||
const { logAudit } = require('../services/auditService.cts');
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb, ensureUserDefaultCategories } = require('../db/database');
|
||||
const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
|
||||
const {
|
||||
auditBillsForUser,
|
||||
categoryBelongsToUser,
|
||||
|
|
@ -15,7 +15,7 @@ const {
|
|||
applyBalanceDelta,
|
||||
} = require('../services/billsService.cts');
|
||||
const { amortizationSchedule, debtAprSnapshot } = require('../services/aprService.cts');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts');
|
||||
const { addMerchantRule, syncBillPaymentsFromSimplefin, merchantMatches } = require('../services/billMerchantRuleService.cts');
|
||||
const { normalizeMerchant } = require('../services/subscriptionService.cts');
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// @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 { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts');
|
||||
const { getUserSettings } = require('../services/userSettings.cts');
|
||||
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// @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 { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const router = express.Router();
|
||||
const { getDb, ensureUserDefaultCategories } = require('../db/database');
|
||||
const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
|
||||
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
|
||||
|
||||
// GET /api/categories
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import type { Req, Res, Next } from '../types/http';
|
|||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService.cts');
|
||||
const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService.cts');
|
||||
const { sanitizeErrorMessage } = require('../services/simplefinService.cts');
|
||||
const { getBankSyncConfig } = require('../services/bankSyncConfigService.cts');
|
||||
const { syncLimiter } = require('../middleware/rateLimiter');
|
||||
const { syncLimiter } = require('../middleware/rateLimiter.cts');
|
||||
|
||||
const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']);
|
||||
const VALID_STATUSES = new Set(['active', 'inactive', 'error']);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
// @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 { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const router = express.Router();
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const Database = require('better-sqlite3');
|
||||
const xlsx = require('xlsx');
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Req, Res, Next } from '../types/http';
|
|||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const router = express.Router();
|
||||
const {
|
||||
previewSpreadsheet,
|
||||
|
|
|
|||
|
|
@ -1,8 +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 router = require('express').Router();
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { getDb } = require('../db/database');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { applyBankPaymentAsSourceOfTruth, reactivatePaymentsOverriddenBy } = require('../services/paymentAccountingService.cts');
|
||||
const {
|
||||
listMatchSuggestions,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { getCycleRange } = require('../services/statusService.cts');
|
||||
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
|
||||
const { toCents, fromCents } = require('../utils/money.mts');
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb, getSetting, setSetting } = require('../db/database');
|
||||
const { requireAuth, requireUser, requireAdmin } = require('../middleware/requireAuth');
|
||||
const { getDb, getSetting, setSetting } = require('../db/database.cts');
|
||||
const { requireAuth, requireUser, requireAdmin } = require('../middleware/requireAuth.cts');
|
||||
const { sendTestEmail } = require('../services/notificationService.cts');
|
||||
const { sendTestPush } = require('../services/notificationService.cts')._push || {};
|
||||
const { decryptSecret } = require('../services/encryptionService.cts');
|
||||
|
|
@ -48,7 +48,7 @@ router.put('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => {
|
|||
const hour = Number.isInteger(h) && h >= 0 && h <= 23 ? h : 6;
|
||||
setSetting('reminder_hour', String(hour));
|
||||
try {
|
||||
require('../workers/dailyWorker').rescheduleDailyWorker();
|
||||
require('../workers/dailyWorker.cts').rescheduleDailyWorker();
|
||||
} catch (err) {
|
||||
console.error('[notifications] Failed to reschedule daily worker:', err.message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// @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 { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const router = require('express').Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { computeBalanceDelta, applyBalanceDelta } = require('../services/billsService.cts');
|
||||
const { validatePaymentInput, serializePayment } = require('../services/paymentValidation.cts');
|
||||
const { getCycleRange, resolveDueDate } = require('../services/statusService.cts');
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import type { Req, Res, Next } from '../types/http';
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { passwordLimiter } = require('../middleware/rateLimiter');
|
||||
const { passwordLimiter } = require('../middleware/rateLimiter.cts');
|
||||
|
||||
const { getDb, getSetting } = require('../db/database');
|
||||
const { getDb, getSetting } = require('../db/database.cts');
|
||||
const { hashPassword, invalidateOtherSessions, rotateSessionId, COOKIE_NAME, cookieOpts } = require('../services/authService.cts');
|
||||
const { getImportHistory } = require('../services/spreadsheetImportService.cts');
|
||||
const { logAudit } = require('../services/auditService.cts');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const { encryptSecret, decryptSecret } = require('../services/encryptionService.cts');
|
||||
|
||||
// All profile routes require authentication — enforced in server.js.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const { calculateSnowball, calculateAvalanche } = require('../services/snowballService.cts');
|
||||
const { calculateMinimumOnly, debtAprSnapshot } = require('../services/aprService.cts');
|
||||
const { serializeBill } = require('../services/billsService.cts');
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { Req, Res, Next } from '../types/http';
|
|||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const {
|
||||
getSpendingSummary, getSpendingTransactions, categorizeTransaction,
|
||||
getSpendingBudgets, setSpendingBudget,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { getDb, getSetting } = require('../db/database');
|
||||
const { getDb, getSetting } = require('../db/database.cts');
|
||||
const { getStatusRuntime, recordError } = require('../services/statusRuntime.cts');
|
||||
const { listBackups } = require('../services/backupService.cts');
|
||||
const { getScheduleStatus } = require('../services/backupScheduler.cts');
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb, ensureUserDefaultCategories } = require('../db/database');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const { fromCents } = require('../utils/money.mts');
|
||||
const {
|
||||
createSubscriptionFromRecommendation,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Req, Res, Next } from '../types/http';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { getCycleRange, resolveDueDate } = require('../services/statusService.cts');
|
||||
const { getUserSettings } = require('../services/userSettings.cts');
|
||||
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
|
||||
// GET /api/tracker/overdue-count — lightweight count for sidebar badge
|
||||
router.get('/overdue-count', (req: Req, res: Res) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +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 router = require('express').Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
const {
|
||||
decorateTransaction,
|
||||
ensureManualDataSource,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import type { Req, Res, Next } from '../types/http';
|
|||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { seedDemoData } = require('../scripts/seedDemoData');
|
||||
const { demoDataLimiter } = require('../middleware/rateLimiter');
|
||||
const { demoDataLimiter } = require('../middleware/rateLimiter.cts');
|
||||
const { eraseUserData } = require('../services/userDataService.cts');
|
||||
const { standardizeError } = require('../middleware/errorFormatter');
|
||||
const { standardizeError } = require('../middleware/errorFormatter.cts');
|
||||
|
||||
// GET /api/user/seeded-status — returns whether the current user has any seeded data
|
||||
router.get('/seeded-status', (req: Req, res: Res) => {
|
||||
|
|
|
|||
20
server.js
20
server.js
|
|
@ -2,15 +2,15 @@ const express = require('express');
|
|||
const cookieParser = require('cookie-parser');
|
||||
const path = require('path');
|
||||
|
||||
const { getDb } = require('./db/database');
|
||||
const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth');
|
||||
const { getDb } = require('./db/database.cts');
|
||||
const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth.cts');
|
||||
const { recordError } = require('./services/statusRuntime.cts');
|
||||
const { securityHeaders } = require('./middleware/securityHeaders');
|
||||
const { securityHeaders } = require('./middleware/securityHeaders.cts');
|
||||
const { logAudit } = require('./services/auditService.cts');
|
||||
const { errorFormatter } = require('./middleware/errorFormatter');
|
||||
const { errorFormatter } = require('./middleware/errorFormatter.cts');
|
||||
const { importLimiter, exportLimiter, adminActionLimiter, oidcLimiter, loginLimiter, loginUsernameLimiter, passwordLimiter, backupOperationLimiter } =
|
||||
require('./middleware/rateLimiter');
|
||||
const { csrfMiddleware, csrfTokenProvider } = require('./middleware/csrf');
|
||||
require('./middleware/rateLimiter.cts');
|
||||
const { csrfMiddleware, csrfTokenProvider } = require('./middleware/csrf.cts');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
|
@ -170,7 +170,7 @@ app.all(['/legacy', '/legacy/*splat'], (req, res) => {
|
|||
app.get('/login.html', (req, res) => res.redirect(302, '/login'));
|
||||
app.use(express.static(DIST));
|
||||
// Ensure CSRF cookie is set for SPA by calling getCsrfToken before sending index.html
|
||||
const { getCsrfToken } = require('./middleware/csrf');
|
||||
const { getCsrfToken } = require('./middleware/csrf.cts');
|
||||
app.get('/{*splat}', (req, res) => {
|
||||
// Set CSRF cookie if not present (needed for SPA to read token)
|
||||
getCsrfToken(req, res);
|
||||
|
|
@ -221,7 +221,7 @@ async function main() {
|
|||
}
|
||||
|
||||
// Run session cleanup on startup
|
||||
const { cleanupExpiredSessions } = require('./db/database');
|
||||
const { cleanupExpiredSessions } = require('./db/database.cts');
|
||||
try {
|
||||
console.log('[cleanup] Running session cleanup on startup');
|
||||
cleanupExpiredSessions();
|
||||
|
|
@ -273,7 +273,7 @@ async function main() {
|
|||
if (userCount > 0) console.log(`Users found: ${userCount}`);
|
||||
|
||||
// Set up periodic session cleanup
|
||||
const { cleanupExpiredSessions } = require('./db/database');
|
||||
const { cleanupExpiredSessions } = require('./db/database.cts');
|
||||
const rawInterval = process.env.SESSION_CLEANUP_INTERVAL_MS;
|
||||
let CLEANUP_INTERVAL_MS = 86400000; // 24 hours default
|
||||
if (rawInterval !== undefined) {
|
||||
|
|
@ -316,7 +316,7 @@ async function main() {
|
|||
|
||||
// Start daily worker (autopay marking, notifications, session pruning, cleanup)
|
||||
try {
|
||||
require('./workers/dailyWorker').start();
|
||||
require('./workers/dailyWorker.cts').start();
|
||||
} catch (err) {
|
||||
console.error('[dailyWorker] Failed to start daily worker:', err.message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
|
||||
// Lazy-loaded in-memory cache — loaded once on first use
|
||||
let _patterns: any[] | null = null;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { accountingActiveSql } = require('./paymentAccountingService.cts');
|
||||
// Aggregation output re-typed by the client; branding adds friction (Dollars|null
|
||||
// through many .filter/.sort/arith) without much safety here → plain require (any).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
|
||||
interface AuditParams {
|
||||
user_id?: number | null;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Req } from '../types/http';
|
|||
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { buildDeviceFingerprint } = require('./loginFingerprint.cts');
|
||||
const { encryptSecret, decryptSecret } = require('./encryptionService.cts');
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Db } from '../types/db';
|
||||
|
||||
const cron = require('node-cron');
|
||||
const { getSetting, setSetting } = require('../db/database');
|
||||
const { getSetting, setSetting } = require('../db/database.cts');
|
||||
const { applyScheduledRetention, createBackup } = require('./backupService.cts');
|
||||
|
||||
interface ScheduleSettings {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const crypto = require('crypto');
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
const { closeDb, getDb, getDbPath } = require('../db/database');
|
||||
const { closeDb, getDb, getDbPath } = require('../db/database.cts');
|
||||
|
||||
const BACKUP_DIR = path.resolve(
|
||||
process.env.BACKUP_PATH || path.join(path.dirname(getDbPath()), '..', 'backups')
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// 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.cts');
|
||||
|
||||
const SYNC_DAYS_MAX = 45; // SimpleFIN Bridge hard limit — requests beyond this return an error
|
||||
const SYNC_DAYS_EFFECTIVE = 44; // 1-day buffer so request latency never tips over the hard limit
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { getBankSyncConfig } = require('./bankSyncConfigService.cts');
|
||||
const { syncDataSource } = require('./bankSyncService.cts');
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { Db } from '../types/db';
|
|||
import type { Req } from '../types/http';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { normalizeCycleType, resolveDueDate } = require('./statusService.cts');
|
||||
const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts');
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const os = require('os');
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { getDb, getSetting, setSetting } = require('../db/database');
|
||||
const { getDb, getSetting, setSetting } = require('../db/database.cts');
|
||||
const { BACKUP_DIR } = require('./backupService.cts');
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type { Db } from '../types/db';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts');
|
||||
|
||||
type Row = Record<string, any>;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Dollars } from '../utils/money.mts';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { getCycleRange } = require('./statusService.cts');
|
||||
const { accountingActiveSql } = require('./paymentAccountingService.cts');
|
||||
const { getUserSettings } = require('./userSettings.cts');
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function getEnvIkm(): Buffer | null {
|
|||
|
||||
// Returns the auto-generated DB IKM, creating it on first call.
|
||||
function getDbIkm(): Buffer {
|
||||
const { getSetting, setSetting } = require('../db/database');
|
||||
const { getSetting, setSetting } = require('../db/database.cts');
|
||||
let stored = getSetting('_auto_encryption_key');
|
||||
if (!stored) {
|
||||
stored = crypto.randomBytes(48).toString('hex');
|
||||
|
|
@ -57,7 +57,7 @@ function isEnvKeyActive(): boolean {
|
|||
function keyFingerprint(): string | null {
|
||||
let ikm = getEnvIkm();
|
||||
if (!ikm) {
|
||||
const { getSetting } = require('../db/database');
|
||||
const { getSetting } = require('../db/database.cts');
|
||||
const stored = getSetting('_auto_encryption_key');
|
||||
if (!stored) return null;
|
||||
ikm = Buffer.from(stored, 'utf8');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { getCycleRange, resolveDueDate } = require('./statusService.cts');
|
||||
const { decorateTransaction } = require('./transactionService.cts');
|
||||
const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Db } from '../types/db';
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
const { getDb, getSetting } = require('../db/database');
|
||||
const { getDb, getSetting } = require('../db/database.cts');
|
||||
const { decryptSecret, encryptSecret } = require('./encryptionService.cts');
|
||||
const { accountingActiveSql } = require('./paymentAccountingService.cts');
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts');
|
||||
const {
|
||||
saveImportSession,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
const crypto = require('crypto');
|
||||
const { Issuer } = require('openid-client');
|
||||
|
||||
const { getDb, getSetting, setSetting } = require('../db/database');
|
||||
const { getDb, getSetting, setSetting } = require('../db/database.cts');
|
||||
const { encryptSecret, decryptSecret } = require('./encryptionService.cts');
|
||||
|
||||
// Decrypt the stored OIDC client secret, falling back to plaintext for
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
const xlsx = require('xlsx');
|
||||
const crypto = require('crypto');
|
||||
const { getDb, ensureUserDefaultCategories } = require('../db/database');
|
||||
const { getDb, ensureUserDefaultCategories } = require('../db/database.cts');
|
||||
const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts');
|
||||
const { toCents, fromCents } = require('../utils/money.mts');
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const state: { worker: WorkerState; notifications: NotificationsState; recentErr
|
|||
// Wrapped in try/catch — DB may not be ready at require-time in tests.
|
||||
function seedFromDb(): void {
|
||||
try {
|
||||
const { getSetting } = require('../db/database');
|
||||
const { getSetting } = require('../db/database.cts');
|
||||
state.worker.last_run_at = getSetting(DB_KEY.workerLastRun) || null;
|
||||
state.worker.next_run_at = getSetting(DB_KEY.workerNextRun) || null;
|
||||
state.worker.last_error = getSetting(DB_KEY.workerError) || null;
|
||||
|
|
@ -56,7 +56,7 @@ seedFromDb();
|
|||
|
||||
function dbSet(key: string, value: unknown): void {
|
||||
try {
|
||||
const { setSetting } = require('../db/database');
|
||||
const { setSetting } = require('../db/database.cts');
|
||||
setSetting(key, value == null ? '' : String(value));
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getSetting } = require('../db/database');
|
||||
const { getSetting } = require('../db/database.cts');
|
||||
|
||||
// Dynamic better-sqlite3 bill/payment rows — columns read individually.
|
||||
type Row = Record<string, any>;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { buildTrackerRow, getCycleRange, resolveDueDate, isPaidStatus } = require('./statusService.cts');
|
||||
const { getUserSettings } = require('./userSettings.cts');
|
||||
const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts');
|
||||
const {
|
||||
applyBankPaymentAsSourceOfTruth,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
// 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.cts');
|
||||
|
||||
const REPO_API_BASE = process.env.REPO_API_URL
|
||||
|| 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { ensureUserDefaultCategories } = require('../db/database');
|
||||
const { ensureUserDefaultCategories } = require('../db/database.cts');
|
||||
|
||||
// Independent user-owned tables (each has a user_id) wiped wholesale.
|
||||
const USER_TABLES = [
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const fs = require('fs');
|
|||
const os = require('os');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
const { getDb } = require('../db/database');
|
||||
const { getDb } = require('../db/database.cts');
|
||||
const { billingCycleForCycleType, cycleTypeFromBillingCycle } = require('./billsService.cts');
|
||||
const { toCents } = require('../utils/money.mts');
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const { getDb, getSetting } = require('../db/database');
|
||||
const { getDb, getSetting } = require('../db/database.cts');
|
||||
|
||||
const USER_SETTING_KEYS = [
|
||||
'currency',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-amountsug-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { computeAmountSuggestion, computeAmountSuggestionsBatch } = require('../services/amountSuggestionService.cts');
|
||||
|
||||
let db, userId, bills;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const crypto = require('node:crypto');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-authsvc-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const auth = require('../services/authService.cts');
|
||||
|
||||
const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const backupPath = path.join(os.tmpdir(), `${testId}-backups`);
|
|||
process.env.DB_PATH = dbPath;
|
||||
process.env.BACKUP_PATH = backupPath;
|
||||
|
||||
const { closeDb, setSetting, getDb } = require('../db/database');
|
||||
const { closeDb, setSetting, getDb } = require('../db/database.cts');
|
||||
const {
|
||||
BACKUP_DIR,
|
||||
createBackup,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-bank-sync-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { encryptSecret } = require('../services/encryptionService.cts');
|
||||
const { syncDataSource } = require('../services/bankSyncService.cts');
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-merchantrule-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { addMerchantRule, syncBillPaymentsFromSimplefin } = require('../services/billMerchantRuleService.cts');
|
||||
|
||||
let db, userId, billId;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-reorder-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { getTracker } = require('../services/trackerService.cts');
|
||||
|
||||
function createUser(db, suffix) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-bills-deleted-route-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
|
||||
function createUser(db, suffix) {
|
||||
return db.prepare(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const {
|
|||
foldLine,
|
||||
rruleForCycle,
|
||||
} = require('../services/calendarFeedService.cts');
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
|
||||
function createUser(db, username = 'calendar-user') {
|
||||
return db.prepare(`
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-category-groups-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
|
||||
function createUser(db, suffix) {
|
||||
return db.prepare(`
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-category-reorder-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
|
||||
function createUser(db, suffix) {
|
||||
return db.prepare(`
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-csv-import-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const {
|
||||
commitCsvTransactions,
|
||||
previewCsvTransactions,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-encsvc-${process.pid}.sqlite
|
|||
process.env.DB_PATH = dbPath;
|
||||
delete process.env.TOKEN_ENCRYPTION_KEY; // start in DB-key mode
|
||||
|
||||
const { getDb, closeDb, getSetting, setSetting } = require('../db/database');
|
||||
const { getDb, closeDb, getSetting, setSetting } = require('../db/database.cts');
|
||||
const enc = require('../services/encryptionService.cts');
|
||||
|
||||
test.before(() => { getDb(); }); // init schema + migrations (also creates the initial admin)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-erase-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { eraseUserData } = require('../services/userDataService.cts');
|
||||
|
||||
function makeUser(db, name) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-roundtrip-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { buildUserDbExportFile } = require('../routes/export.cts');
|
||||
const { previewUserDbImport, applyUserDbImport } = require('../services/userDbImportService.cts');
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-export-richer-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const exportRouter = require('../routes/export.cts');
|
||||
const { getUserExportData } = exportRouter;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const buildVersioned = require('../db/migrations/versionedMigrations');
|
||||
const buildReconcile = require('../db/migrations/legacyReconcileMigrations');
|
||||
const buildVersioned = require('../db/migrations/versionedMigrations.cts');
|
||||
const buildReconcile = require('../db/migrations/legacyReconcileMigrations.cts');
|
||||
|
||||
test('both migration modules build their arrays with no deps needed at construction', () => {
|
||||
const versioned = buildVersioned({});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-ofx-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { parseOfx, previewOfxTransactions, commitOfxTransactions } = require('../services/ofxImportService.cts');
|
||||
|
||||
// OFX 1.x SGML — container tags closed, leaf tags unclosed.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-payacct-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const {
|
||||
isBankBackedPayment,
|
||||
accountingActiveSql,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-quickpay-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const router = require('../routes/payments.cts');
|
||||
|
||||
function handler(method, routePath) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-payments-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const router = require('../routes/payments.cts');
|
||||
|
||||
function handler(method, routePath) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-profile-route-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { publicUser } = require('../services/authService.cts');
|
||||
|
||||
function createUser(db, suffix) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-recon-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { getTracker } = require('../services/trackerService.cts');
|
||||
const { getAnalyticsSummary } = require('../services/analyticsService.cts');
|
||||
const { resolveDueDate } = require('../services/statusService.cts');
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-snowball-plan-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const router = require('../routes/snowball.cts');
|
||||
|
||||
function handler(method, routePath) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-spendsvc-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { setSpendingBudget, getSpendingBudgets } = require('../services/spendingService.cts');
|
||||
|
||||
let db, userId, catId;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-spending-summary-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { getSpendingSummary, setSpendingBudget } = require('../services/spendingService.cts');
|
||||
|
||||
function createUser(db, suffix) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-subscription-service-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const {
|
||||
getSubscriptionRecommendations,
|
||||
recordSubscriptionFeedback,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-summary-bt-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
|
||||
function callSummary(userId, year, month) {
|
||||
const router = require('../routes/summary.cts');
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-summary-skip-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
|
||||
function callSummary(userId, year, month) {
|
||||
const router = require('../routes/summary.cts');
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const { generateSync } = require('otplib');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-totp-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { encryptSecret } = require('../services/encryptionService.cts');
|
||||
const totp = require('../services/totpService.cts');
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-trackersvc-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { getTracker, getOverdueCount } = require('../services/trackerService.cts');
|
||||
|
||||
// Fixed "now" so occurrence gating + statuses are deterministic (mid-June 2026).
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('node:path');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-transaction-match-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { ensureManualDataSource } = require('../services/transactionService.cts');
|
||||
const { getTracker } = require('../services/trackerService.cts');
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-match-state-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, closeDb } = require('../db/database');
|
||||
const { getDb, closeDb } = require('../db/database.cts');
|
||||
const { markMatched, markUnmatched, markIgnored } = require('../services/transactionMatchState.cts');
|
||||
|
||||
let db, userId, otherId, billId;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const fs = require('node:fs');
|
|||
const dbPath = path.join(os.tmpdir(), `bill-tracker-updatecheck-test-${process.pid}.sqlite`);
|
||||
process.env.DB_PATH = dbPath;
|
||||
|
||||
const { getDb, setSetting, closeDb } = require('../db/database');
|
||||
const { getDb, setSetting, closeDb } = require('../db/database.cts');
|
||||
const svc = require('../services/updateCheckService.cts');
|
||||
|
||||
test('disabled: makes no external request and returns disabled', async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
import type { Db } from '../types/db';
|
||||
|
||||
const cron = require('node-cron');
|
||||
const { getDb, getSetting } = require('../db/database');
|
||||
const { getDb, getSetting } = require('../db/database.cts');
|
||||
const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts');
|
||||
const { pruneExpiredSessions } = require('../services/authService.cts');
|
||||
const { pruneExpiredChallenges: pruneWebAuthnChallenges } = require('../services/webauthnService.cts');
|
||||
|
|
@ -14,42 +16,35 @@ const {
|
|||
} = require('../services/statusRuntime.cts');
|
||||
const { localDateString, localDateStringDaysAgo } = require('../utils/dates.mts');
|
||||
|
||||
// The hour (0–23) the single daily job runs — configurable via the global
|
||||
// 'reminder_hour' setting, defaulting to 6 AM. One source of truth for both the
|
||||
// cron expression and the next-run display.
|
||||
function resolveReminderHour() {
|
||||
// The hour (0–23) the single daily job runs — configurable via 'reminder_hour'
|
||||
// (default 6 AM). One source of truth for both the cron expression and next-run.
|
||||
function resolveReminderHour(): number {
|
||||
const parsed = parseInt(getSetting('reminder_hour') ?? '6', 10);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 23 ? parsed : 6;
|
||||
}
|
||||
|
||||
function nextDailyRunIso(from = new Date()) {
|
||||
function nextDailyRunIso(from: Date = new Date()): string {
|
||||
const next = new Date(from);
|
||||
next.setHours(resolveReminderHour(), 0, 0, 0);
|
||||
if (next <= from) next.setDate(next.getDate() + 1);
|
||||
return next.toISOString();
|
||||
}
|
||||
|
||||
async function runDailyTasks() {
|
||||
const db = getDb();
|
||||
async function runDailyTasks(): Promise<void> {
|
||||
const db: Db = getDb();
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1;
|
||||
// Local date — keep consistent with year/month above and with the client's
|
||||
// notion of "today". toISOString() would give the UTC date, which can be a
|
||||
// different calendar day and caused autopay marking a day early/late.
|
||||
const todayStr = localDateString(now);
|
||||
|
||||
const bills = db.prepare('SELECT * FROM bills WHERE active = 1 AND deleted_at IS NULL').all();
|
||||
|
||||
if (bills.length > 0) {
|
||||
// Batch-fetch payments for all active bills from the last 90 days to avoid
|
||||
// one query per bill. Different billing cycles (monthly/quarterly/annual) all
|
||||
// fit inside a 90-day window for the current month's due-date checks.
|
||||
const billIds = bills.map(b => b.id);
|
||||
const billIds = bills.map((b: any) => b.id);
|
||||
const placeholders = billIds.map(() => '?').join(',');
|
||||
const windowStart = localDateStringDaysAgo(90, now);
|
||||
|
||||
let allPayments = [];
|
||||
let allPayments: any[] = [];
|
||||
try {
|
||||
allPayments = db.prepare(`
|
||||
SELECT * FROM payments
|
||||
|
|
@ -57,18 +52,17 @@ async function runDailyTasks() {
|
|||
AND paid_date >= ?
|
||||
AND deleted_at IS NULL
|
||||
`).all(...billIds, windowStart);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('[worker] Failed to batch-fetch payments:', err.message);
|
||||
}
|
||||
|
||||
// Group payments by bill_id in memory
|
||||
const paymentsByBill = new Map();
|
||||
const paymentsByBill = new Map<any, any>();
|
||||
for (const p of allPayments) {
|
||||
if (!paymentsByBill.has(p.bill_id)) paymentsByBill.set(p.bill_id, []);
|
||||
paymentsByBill.get(p.bill_id).push(p);
|
||||
}
|
||||
|
||||
// Prepare autopay update statement once outside the loop
|
||||
const markAutopay = db.prepare(
|
||||
"UPDATE bills SET autodraft_status = 'assumed_paid', updated_at = datetime('now') WHERE id = ?"
|
||||
);
|
||||
|
|
@ -77,9 +71,8 @@ async function runDailyTasks() {
|
|||
const range = getCycleRange(year, month, bill);
|
||||
if (!range) continue; // bill does not apply this cycle
|
||||
|
||||
// Filter pre-fetched payments to the bill's cycle range
|
||||
const payments = (paymentsByBill.get(bill.id) || []).filter(
|
||||
p => p.paid_date >= range.start && p.paid_date <= range.end
|
||||
(p: any) => p.paid_date >= range.start && p.paid_date <= range.end
|
||||
);
|
||||
|
||||
const row = buildTrackerRow(bill, payments, year, month, todayStr);
|
||||
|
|
@ -93,7 +86,7 @@ async function runDailyTasks() {
|
|||
) {
|
||||
try {
|
||||
markAutopay.run(bill.id);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(`[worker] Failed to mark autopay for bill ${bill.id}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
|
@ -103,15 +96,15 @@ async function runDailyTasks() {
|
|||
pruneExpiredSessions();
|
||||
pruneWebAuthnChallenges(db);
|
||||
|
||||
await runNotifications().catch(err => {
|
||||
await runNotifications().catch((err: any) => {
|
||||
console.error('[worker] Notification error (non-fatal):', err.message);
|
||||
});
|
||||
|
||||
await runDriftNotifications().catch(err => {
|
||||
await runDriftNotifications().catch((err: any) => {
|
||||
console.error('[worker] Drift notification error (non-fatal):', err.message);
|
||||
});
|
||||
|
||||
await runAllCleanup().catch(err => {
|
||||
await runAllCleanup().catch((err: any) => {
|
||||
console.error('[worker] Cleanup error (non-fatal):', err.message);
|
||||
});
|
||||
|
||||
|
|
@ -119,40 +112,39 @@ async function runDailyTasks() {
|
|||
console.log(`[worker] Daily tasks ran at ${todayStr}`);
|
||||
}
|
||||
|
||||
let scheduledTask = null;
|
||||
let scheduledTask: any = null;
|
||||
|
||||
// (Re)create the daily cron at the configured hour. Stops any prior task first.
|
||||
function scheduleDaily() {
|
||||
function scheduleDaily(): void {
|
||||
if (scheduledTask) {
|
||||
scheduledTask.stop();
|
||||
scheduledTask = null;
|
||||
}
|
||||
const hour = resolveReminderHour();
|
||||
scheduledTask = cron.schedule(`0 ${hour} * * *`, () => {
|
||||
runDailyTasks().catch(err => {
|
||||
runDailyTasks().catch((err: any) => {
|
||||
markWorkerError(err, nextDailyRunIso());
|
||||
console.error('[worker] Daily task error:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Called when the 'reminder_hour' setting changes so it applies without a
|
||||
// restart. Defensive: a failure here must never take the worker down.
|
||||
function rescheduleDailyWorker() {
|
||||
// Called when 'reminder_hour' changes so it applies without a restart.
|
||||
function rescheduleDailyWorker(): void {
|
||||
try {
|
||||
scheduleDaily();
|
||||
markWorkerStarted(nextDailyRunIso());
|
||||
console.log(`[worker] Rescheduled daily tasks to ${resolveReminderHour()}:00`);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('[worker] Failed to reschedule daily worker:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
function start(): void {
|
||||
markWorkerStarted(nextDailyRunIso());
|
||||
|
||||
// Run once at startup
|
||||
runDailyTasks().catch(err => {
|
||||
runDailyTasks().catch((err: any) => {
|
||||
markWorkerError(err, nextDailyRunIso());
|
||||
console.error('[worker] Startup task error:', err);
|
||||
});
|
||||
Loading…
Reference in New Issue