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