2026-07-06 11:26:50 -05:00
|
|
|
import type { Req, Res, Next } from '../types/http';
|
2026-07-06 14:23:53 -05:00
|
|
|
('use strict');
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
const express = require('express');
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const router = express.Router();
|
|
|
|
|
const bcrypt = require('bcryptjs');
|
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>
2026-07-06 11:34:08 -05:00
|
|
|
const { passwordLimiter } = require('../middleware/rateLimiter.cts');
|
2026-05-03 19:51:57 -05:00
|
|
|
|
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>
2026-07-06 11:34:08 -05:00
|
|
|
const { getDb, getSetting } = require('../db/database.cts');
|
2026-07-06 14:23:53 -05:00
|
|
|
const {
|
|
|
|
|
hashPassword,
|
|
|
|
|
invalidateOtherSessions,
|
|
|
|
|
rotateSessionId,
|
|
|
|
|
COOKIE_NAME,
|
|
|
|
|
cookieOpts,
|
|
|
|
|
} = require('../services/authService.cts');
|
|
|
|
|
const { getImportHistory } = require('../services/spreadsheetImportService.cts');
|
|
|
|
|
const { logAudit } = require('../services/auditService.cts');
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
const {
|
|
|
|
|
ApiError,
|
|
|
|
|
ValidationError,
|
|
|
|
|
AuthError,
|
|
|
|
|
ForbiddenError,
|
|
|
|
|
NotFoundError,
|
|
|
|
|
ConflictError,
|
|
|
|
|
} = require('../utils/apiError.cts');
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const { encryptSecret, decryptSecret } = require('../services/encryptionService.cts');
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
// All profile routes require authentication — enforced in server.js.
|
|
|
|
|
// req.user is always the signed-in user; user_id is never accepted from the body.
|
|
|
|
|
|
2026-05-16 21:36:04 -05:00
|
|
|
function dataImportEnabled() {
|
|
|
|
|
return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 06:53:27 -05:00
|
|
|
function requireDataImportEnabled(req: Req, res: Res, next: Next) {
|
2026-05-16 21:36:04 -05:00
|
|
|
if (!dataImportEnabled()) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ForbiddenError('Data import is disabled by DATA_IMPORT_ENABLED=false');
|
2026-05-16 21:36:04 -05:00
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 06:53:27 -05:00
|
|
|
function profileResponse(user: any) {
|
2026-06-07 01:17:49 -05:00
|
|
|
const displayName = user.display_name || null;
|
|
|
|
|
return {
|
2026-07-06 14:23:53 -05:00
|
|
|
id: user.id,
|
|
|
|
|
username: user.username,
|
|
|
|
|
display_name: displayName,
|
2026-06-07 01:17:49 -05:00
|
|
|
displayName,
|
2026-07-06 14:23:53 -05:00
|
|
|
name: displayName || user.username,
|
|
|
|
|
role: user.role,
|
|
|
|
|
active: !!user.active,
|
|
|
|
|
is_default_admin: !!user.is_default_admin,
|
|
|
|
|
created_at: user.created_at,
|
|
|
|
|
updated_at: user.updated_at,
|
2026-06-07 01:17:49 -05:00
|
|
|
last_password_change_at: user.last_password_change_at || null,
|
2026-07-06 14:23:53 -05:00
|
|
|
first_login: !!user.first_login,
|
2026-06-07 01:17:49 -05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
// ── GET /api/profile ──────────────────────────────────────────────────────────
|
|
|
|
|
// Returns safe profile data for the signed-in user.
|
|
|
|
|
// Never returns password_hash, session tokens, or secrets.
|
2026-07-06 11:26:50 -05:00
|
|
|
router.get('/', (req: Req, res: Res) => {
|
2026-07-06 14:23:53 -05:00
|
|
|
const db = getDb();
|
|
|
|
|
const user = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-04 23:34:24 -05:00
|
|
|
SELECT id, username, display_name, role, active, is_default_admin,
|
2026-05-03 19:51:57 -05:00
|
|
|
first_login, created_at, updated_at,
|
|
|
|
|
last_password_change_at,
|
|
|
|
|
notification_email, notifications_enabled,
|
|
|
|
|
notify_3d, notify_1d, notify_due, notify_overdue
|
|
|
|
|
FROM users WHERE id = ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(req.user.id);
|
2026-05-03 19:51:57 -05:00
|
|
|
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
if (!user) throw NotFoundError('User not found');
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
res.json({
|
2026-06-07 01:17:49 -05:00
|
|
|
...profileResponse(user),
|
2026-05-03 19:51:57 -05:00
|
|
|
notifications: {
|
2026-07-06 14:23:53 -05:00
|
|
|
email: user.notification_email || null,
|
|
|
|
|
enabled: !!user.notifications_enabled,
|
|
|
|
|
notify_3d: !!user.notify_3d,
|
|
|
|
|
notify_1d: !!user.notify_1d,
|
|
|
|
|
notify_due: !!user.notify_due,
|
2026-05-03 19:51:57 -05:00
|
|
|
notify_overdue: !!user.notify_overdue,
|
|
|
|
|
},
|
|
|
|
|
exports: {
|
2026-07-06 14:23:53 -05:00
|
|
|
user_db: '/api/export/user-db',
|
2026-05-03 19:51:57 -05:00
|
|
|
user_excel: '/api/export/user-excel',
|
|
|
|
|
},
|
|
|
|
|
import_history_url: '/api/profile/import-history',
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── PATCH /api/profile ────────────────────────────────────────────────────────
|
2026-05-14 01:17:05 -05:00
|
|
|
// Updates safe profile fields: username and display_name.
|
2026-05-03 19:51:57 -05:00
|
|
|
// Ignores any unknown or restricted fields.
|
2026-07-06 11:26:50 -05:00
|
|
|
router.patch('/', (req: Req, res: Res) => {
|
2026-06-07 01:17:49 -05:00
|
|
|
const { username } = req.body;
|
2026-07-06 14:23:53 -05:00
|
|
|
const displayNameInput =
|
|
|
|
|
req.body.display_name !== undefined
|
|
|
|
|
? req.body.display_name
|
|
|
|
|
: req.body.displayName !== undefined
|
|
|
|
|
? req.body.displayName
|
|
|
|
|
: req.body.name;
|
2026-05-14 01:17:05 -05:00
|
|
|
const db = getDb();
|
|
|
|
|
|
|
|
|
|
if (username !== undefined) {
|
|
|
|
|
if (typeof username !== 'string') {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('username must be a string', 'username');
|
2026-05-14 01:17:05 -05:00
|
|
|
}
|
|
|
|
|
const trimmedUsername = username.trim();
|
|
|
|
|
if (trimmedUsername.length < 3) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('username must be at least 3 characters', 'username');
|
2026-05-14 01:17:05 -05:00
|
|
|
}
|
|
|
|
|
if (trimmedUsername.length > 50) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('username must be 50 characters or fewer', 'username');
|
2026-05-14 01:17:05 -05:00
|
|
|
}
|
2026-07-06 14:23:53 -05:00
|
|
|
const taken = db
|
|
|
|
|
.prepare('SELECT id FROM users WHERE username = ? COLLATE NOCASE AND id != ?')
|
|
|
|
|
.get(trimmedUsername, req.user.id);
|
2026-05-14 01:17:05 -05:00
|
|
|
if (taken) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ConflictError('Username already taken', 'username');
|
2026-05-14 01:17:05 -05:00
|
|
|
}
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare("UPDATE users SET username = ?, updated_at = datetime('now') WHERE id = ?").run(
|
|
|
|
|
trimmedUsername,
|
|
|
|
|
req.user.id,
|
|
|
|
|
);
|
|
|
|
|
logAudit({
|
|
|
|
|
user_id: req.user.id,
|
|
|
|
|
action: 'profile.username.change',
|
|
|
|
|
ip_address: req.ip,
|
|
|
|
|
user_agent: req.get('user-agent'),
|
|
|
|
|
});
|
2026-05-14 01:17:05 -05:00
|
|
|
}
|
2026-05-03 19:51:57 -05:00
|
|
|
|
2026-06-07 01:17:49 -05:00
|
|
|
if (displayNameInput !== undefined) {
|
|
|
|
|
if (typeof displayNameInput !== 'string') {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('display_name must be a string', 'display_name');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
2026-06-07 01:17:49 -05:00
|
|
|
const trimmed = displayNameInput.trim();
|
2026-05-03 19:51:57 -05:00
|
|
|
if (trimmed.length > 100) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('display_name must be 100 characters or fewer', 'display_name');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare("UPDATE users SET display_name = ?, updated_at = datetime('now') WHERE id = ?").run(
|
|
|
|
|
trimmed || null,
|
|
|
|
|
req.user.id,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
user_id: req.user.id,
|
|
|
|
|
action: 'profile.update',
|
|
|
|
|
ip_address: req.ip,
|
|
|
|
|
user_agent: req.get('user-agent'),
|
|
|
|
|
});
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const updated = getDb()
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-04 23:34:24 -05:00
|
|
|
SELECT id, username, display_name, role, active, is_default_admin,
|
|
|
|
|
first_login, created_at, updated_at,
|
|
|
|
|
last_password_change_at
|
|
|
|
|
FROM users WHERE id = ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(req.user.id);
|
2026-05-04 23:34:24 -05:00
|
|
|
|
2026-06-07 01:17:49 -05:00
|
|
|
res.json({ success: true, profile: profileResponse(updated) });
|
2026-05-03 19:51:57 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── GET /api/profile/settings ─────────────────────────────────────────────────
|
|
|
|
|
// Returns user-owned notification preferences from the users table.
|
|
|
|
|
// Does not return admin/global/SMTP settings.
|
2026-07-06 11:26:50 -05:00
|
|
|
router.get('/settings', (req: Req, res: Res) => {
|
2026-07-06 14:23:53 -05:00
|
|
|
const db = getDb();
|
|
|
|
|
const user = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-03 19:51:57 -05:00
|
|
|
SELECT notification_email, notifications_enabled,
|
2026-06-03 21:43:54 -05:00
|
|
|
notify_3d, notify_1d, notify_due, notify_overdue, notify_amount_change,
|
2026-06-07 21:18:02 -05:00
|
|
|
notify_push_enabled, push_channel, push_url, push_token, push_chat_id,
|
|
|
|
|
geolocation_enabled
|
2026-05-03 19:51:57 -05:00
|
|
|
FROM users WHERE id = ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(req.user.id);
|
2026-05-03 19:51:57 -05:00
|
|
|
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
if (!user) throw NotFoundError('User not found');
|
2026-05-03 19:51:57 -05:00
|
|
|
|
2026-06-03 21:43:54 -05:00
|
|
|
// Decrypt push secrets — return masked indicator for token (never the raw value)
|
2026-07-06 14:23:53 -05:00
|
|
|
const pushUrlDecrypted = user.push_url
|
|
|
|
|
? (() => {
|
|
|
|
|
try {
|
|
|
|
|
return decryptSecret(user.push_url);
|
|
|
|
|
} catch {
|
|
|
|
|
return user.push_url;
|
|
|
|
|
}
|
|
|
|
|
})()
|
|
|
|
|
: null;
|
2026-06-03 21:43:54 -05:00
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
res.json({
|
2026-07-06 14:23:53 -05:00
|
|
|
notification_email: user.notification_email || null,
|
2026-05-03 19:51:57 -05:00
|
|
|
notifications_enabled: !!user.notifications_enabled,
|
2026-07-06 14:23:53 -05:00
|
|
|
notify_3d: !!user.notify_3d,
|
|
|
|
|
notify_1d: !!user.notify_1d,
|
|
|
|
|
notify_due: !!user.notify_due,
|
|
|
|
|
notify_overdue: !!user.notify_overdue,
|
|
|
|
|
notify_amount_change: user.notify_amount_change !== 0,
|
|
|
|
|
notify_push_enabled: !!user.notify_push_enabled,
|
|
|
|
|
push_channel: user.push_channel || null,
|
|
|
|
|
push_url: pushUrlDecrypted || null,
|
|
|
|
|
push_token_set: !!user.push_token,
|
|
|
|
|
push_chat_id: user.push_chat_id || null,
|
|
|
|
|
geolocation_enabled: !!user.geolocation_enabled,
|
2026-05-03 19:51:57 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── PATCH /api/profile/settings ───────────────────────────────────────────────
|
|
|
|
|
// Updates user-owned notification preferences only.
|
|
|
|
|
// Cannot modify global/admin/SMTP settings through this endpoint.
|
2026-06-03 21:43:54 -05:00
|
|
|
const VALID_PUSH_CHANNELS = new Set(['ntfy', 'gotify', 'discord', 'telegram']);
|
|
|
|
|
|
2026-07-06 11:26:50 -05:00
|
|
|
router.patch('/settings', (req: Req, res: Res) => {
|
2026-05-03 19:51:57 -05:00
|
|
|
const db = getDb();
|
|
|
|
|
const {
|
2026-07-06 14:23:53 -05:00
|
|
|
notification_email,
|
|
|
|
|
email,
|
|
|
|
|
notifications_enabled,
|
|
|
|
|
notify_3d,
|
|
|
|
|
notify_1d,
|
|
|
|
|
notify_due,
|
|
|
|
|
notify_overdue,
|
|
|
|
|
notify_amount_change,
|
|
|
|
|
notify_push_enabled,
|
|
|
|
|
push_channel,
|
|
|
|
|
push_url,
|
|
|
|
|
push_token,
|
|
|
|
|
push_chat_id,
|
2026-06-07 21:18:02 -05:00
|
|
|
geolocation_enabled,
|
2026-05-03 19:51:57 -05:00
|
|
|
} = req.body;
|
|
|
|
|
|
2026-05-04 23:34:24 -05:00
|
|
|
const nextEmail = notification_email !== undefined ? notification_email : email;
|
|
|
|
|
|
|
|
|
|
if (nextEmail !== undefined && nextEmail !== null) {
|
|
|
|
|
if (typeof nextEmail !== 'string') {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('notification_email must be a string', 'notification_email');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
2026-05-04 23:34:24 -05:00
|
|
|
if (nextEmail.trim().length > 255) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('notification_email is too long', 'notification_email');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
if (
|
|
|
|
|
push_channel !== undefined &&
|
|
|
|
|
push_channel !== null &&
|
|
|
|
|
!VALID_PUSH_CHANNELS.has(push_channel)
|
|
|
|
|
) {
|
|
|
|
|
return res
|
|
|
|
|
.status(400)
|
|
|
|
|
.json({ error: `push_channel must be one of: ${[...VALID_PUSH_CHANNELS].join(', ')}` });
|
2026-06-03 21:43:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const current = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-03 19:51:57 -05:00
|
|
|
SELECT notification_email, notifications_enabled,
|
2026-06-03 21:43:54 -05:00
|
|
|
notify_3d, notify_1d, notify_due, notify_overdue, notify_amount_change,
|
2026-06-07 21:18:02 -05:00
|
|
|
notify_push_enabled, push_channel, push_url, push_token, push_chat_id,
|
|
|
|
|
geolocation_enabled
|
2026-05-03 19:51:57 -05:00
|
|
|
FROM users WHERE id = ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(req.user.id);
|
2026-05-03 19:51:57 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const emailVal =
|
|
|
|
|
nextEmail !== undefined
|
|
|
|
|
? nextEmail
|
|
|
|
|
? nextEmail.trim() || null
|
|
|
|
|
: null
|
|
|
|
|
: current.notification_email;
|
2026-05-03 19:51:57 -05:00
|
|
|
|
2026-07-11 06:53:27 -05:00
|
|
|
const boolVal = (incoming: any, fallback: any) =>
|
|
|
|
|
incoming !== undefined ? (incoming ? 1 : 0) : fallback;
|
2026-05-03 19:51:57 -05:00
|
|
|
|
2026-06-03 21:43:54 -05:00
|
|
|
// Encrypt push_url and push_token before storing
|
2026-07-11 06:53:27 -05:00
|
|
|
const encryptOrNull = (v: any, fallback: any) => {
|
2026-06-03 21:43:54 -05:00
|
|
|
if (v === undefined) return fallback;
|
|
|
|
|
if (!v) return null;
|
2026-07-06 14:23:53 -05:00
|
|
|
try {
|
|
|
|
|
return encryptSecret(String(v).trim());
|
|
|
|
|
} catch {
|
|
|
|
|
return String(v).trim();
|
|
|
|
|
}
|
2026-06-03 21:43:54 -05:00
|
|
|
};
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare(
|
|
|
|
|
`
|
2026-05-03 19:51:57 -05:00
|
|
|
UPDATE users SET
|
|
|
|
|
notification_email = ?,
|
|
|
|
|
notifications_enabled = ?,
|
|
|
|
|
notify_3d = ?,
|
|
|
|
|
notify_1d = ?,
|
|
|
|
|
notify_due = ?,
|
|
|
|
|
notify_overdue = ?,
|
2026-05-30 14:33:55 -05:00
|
|
|
notify_amount_change = ?,
|
2026-06-03 21:43:54 -05:00
|
|
|
notify_push_enabled = ?,
|
|
|
|
|
push_channel = ?,
|
|
|
|
|
push_url = ?,
|
|
|
|
|
push_token = ?,
|
|
|
|
|
push_chat_id = ?,
|
2026-06-07 21:18:02 -05:00
|
|
|
geolocation_enabled = ?,
|
2026-05-03 19:51:57 -05:00
|
|
|
updated_at = datetime('now')
|
|
|
|
|
WHERE id = ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
).run(
|
2026-05-03 19:51:57 -05:00
|
|
|
emailVal,
|
2026-07-06 14:23:53 -05:00
|
|
|
boolVal(notifications_enabled, current.notifications_enabled),
|
|
|
|
|
boolVal(notify_3d, current.notify_3d),
|
|
|
|
|
boolVal(notify_1d, current.notify_1d),
|
|
|
|
|
boolVal(notify_due, current.notify_due),
|
|
|
|
|
boolVal(notify_overdue, current.notify_overdue),
|
|
|
|
|
boolVal(notify_amount_change, current.notify_amount_change),
|
|
|
|
|
boolVal(notify_push_enabled, current.notify_push_enabled),
|
|
|
|
|
push_channel !== undefined ? push_channel || null : current.push_channel,
|
|
|
|
|
encryptOrNull(push_url, current.push_url),
|
2026-06-03 21:43:54 -05:00
|
|
|
encryptOrNull(push_token, current.push_token),
|
2026-07-06 14:23:53 -05:00
|
|
|
push_chat_id !== undefined ? push_chat_id?.trim() || null : current.push_chat_id,
|
2026-06-07 21:18:02 -05:00
|
|
|
boolVal(geolocation_enabled, current.geolocation_enabled),
|
2026-05-03 19:51:57 -05:00
|
|
|
req.user.id,
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
logAudit({
|
|
|
|
|
user_id: req.user.id,
|
|
|
|
|
action: 'profile.settings.update',
|
|
|
|
|
ip_address: req.ip,
|
|
|
|
|
user_agent: req.get('user-agent'),
|
|
|
|
|
});
|
2026-05-10 00:03:12 -05:00
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
res.json({ success: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── POST /api/profile/change-password ─────────────────────────────────────────
|
|
|
|
|
// Changes the signed-in user's password.
|
|
|
|
|
// Always requires: current_password, new_password, confirm_new_password.
|
|
|
|
|
// Never bypasses current_password verification regardless of must_change_password.
|
|
|
|
|
// Never accepts user_id from the request body.
|
2026-07-06 11:26:50 -05:00
|
|
|
router.post('/change-password', passwordLimiter, async (req: Req, res: Res) => {
|
2026-05-03 19:51:57 -05:00
|
|
|
const { current_password, new_password, confirm_new_password } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!current_password) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('current_password is required', 'current_password');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
|
|
|
|
if (!new_password) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('new_password is required', 'new_password');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
|
|
|
|
if (!confirm_new_password) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('confirm_new_password is required', 'confirm_new_password');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
|
|
|
|
if (new_password !== confirm_new_password) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('new passwords do not match', 'confirm_new_password');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
|
|
|
|
if (new_password.length < 8) {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
throw ValidationError('new password must be at least 8 characters', 'new_password');
|
2026-05-03 19:51:57 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const db = getDb();
|
2026-05-03 19:51:57 -05:00
|
|
|
const user = db.prepare('SELECT password_hash FROM users WHERE id = ?').get(req.user.id);
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
if (!user) throw NotFoundError('User not found');
|
2026-05-03 19:51:57 -05:00
|
|
|
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
const valid = await bcrypt.compare(current_password, user.password_hash);
|
|
|
|
|
if (!valid) {
|
|
|
|
|
throw new ApiError('AUTH_ERROR', 'current password is incorrect', 401, {
|
|
|
|
|
field: 'current_password',
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-05-03 19:51:57 -05:00
|
|
|
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
const hash = await hashPassword(new_password);
|
2026-05-31 15:06:10 -05:00
|
|
|
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
db.prepare(
|
|
|
|
|
`
|
2026-05-31 15:06:10 -05:00
|
|
|
UPDATE users
|
|
|
|
|
SET password_hash = ?, must_change_password = 0,
|
|
|
|
|
last_password_change_at = datetime('now'),
|
|
|
|
|
updated_at = datetime('now')
|
|
|
|
|
WHERE id = ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
).run(hash, req.user.id);
|
2026-05-10 03:55:14 -05:00
|
|
|
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
// Invalidate all other sessions for this user
|
|
|
|
|
const currentSessionId = req.cookies?.[COOKIE_NAME];
|
|
|
|
|
if (currentSessionId) {
|
|
|
|
|
invalidateOtherSessions(req.user.id, currentSessionId);
|
2026-05-10 03:55:14 -05:00
|
|
|
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
// Rotate the current session ID for security
|
|
|
|
|
const newSessionId = rotateSessionId(currentSessionId, req.user.id);
|
|
|
|
|
if (newSessionId) {
|
|
|
|
|
res.cookie(COOKIE_NAME, newSessionId, cookieOpts(req));
|
|
|
|
|
}
|
2026-05-31 15:06:10 -05:00
|
|
|
}
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
|
|
|
|
|
logAudit({
|
|
|
|
|
user_id: req.user.id,
|
|
|
|
|
action: 'password.change',
|
|
|
|
|
ip_address: req.ip,
|
|
|
|
|
user_agent: req.get('user-agent'),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({ success: true });
|
2026-05-03 19:51:57 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── GET /api/profile/exports ──────────────────────────────────────────────────
|
|
|
|
|
// Returns metadata about available user export capabilities.
|
|
|
|
|
// Does not trigger any download; links point to the actual export endpoints.
|
2026-07-06 11:26:50 -05:00
|
|
|
router.get('/exports', (req: Req, res: Res) => {
|
2026-05-03 19:51:57 -05:00
|
|
|
res.json({
|
|
|
|
|
exports: [
|
|
|
|
|
{
|
2026-07-06 14:23:53 -05:00
|
|
|
id: 'user_db',
|
|
|
|
|
label: 'SQLite database export',
|
|
|
|
|
description: 'Your bills, categories, payments, and notes as a portable SQLite file',
|
|
|
|
|
url: '/api/export/user-db',
|
|
|
|
|
method: 'GET',
|
2026-05-03 19:51:57 -05:00
|
|
|
},
|
|
|
|
|
{
|
2026-07-06 14:23:53 -05:00
|
|
|
id: 'user_excel',
|
|
|
|
|
label: 'Excel databook export',
|
|
|
|
|
description: 'Your data as an Excel workbook with multiple sheets',
|
|
|
|
|
url: '/api/export/user-excel',
|
|
|
|
|
method: 'GET',
|
2026-05-03 19:51:57 -05:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── GET /api/profile/import-history ──────────────────────────────────────────
|
|
|
|
|
// Returns the signed-in user's import history.
|
|
|
|
|
// Delegates to the same service as GET /api/import/history.
|
2026-07-06 11:26:50 -05:00
|
|
|
router.get('/import-history', requireDataImportEnabled, (req: Req, res: Res) => {
|
refactor(routes): throw-pattern for profile, transactions, admin, aboutAdmin
Standards-unification batch 4b (completes the route conversion program —
all 29 route files now emit errors through the canonical path or a
documented exception):
- profile.cts: full conversion; try/catch-500 wrappers removed
- transactions.cts: boundary adapter throwStandardized() rethrows the
internal {error} parse/normalize results as ApiError (internal result
convention unchanged); sendTransactionServiceError -> throwing
toTransactionServiceError (4xx keep message/code, 5xx masked)
- admin.cts: user-management + bank-sync-config + rollback converted;
coded rollback errors (NOT_APPLIED 404 / ROLLBACK_NOT_SUPPORTED 422)
preserved as intentional messages
- aboutAdmin.cts: loose 400 converted; static no-path-disclosure 500s kept
Documented exceptions (intentional, commented inline): admin sendError
(backup download can fail mid-stream after headers sent), import.cts
sendImportError pipeline (error-id envelope), transactions bulk-unmatch
500 (carries per-item results), payments DUPLICATE_SUSPECTED 409 (carries
existing payment), calendarFeed text/plain 404 (ICS clients), authOidc
static 501/502 (rewritten wholesale in the openid-client v6 batch).
Server suite 252/252.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:28 -05:00
|
|
|
const history = getImportHistory(req.user.id);
|
|
|
|
|
res.json({ history });
|
2026-05-03 19:51:57 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
module.exports = router;
|