fix: encryptionService removes env-var key path, always uses auto-generated DB key
This commit is contained in:
parent
a88d5c4647
commit
0011ade58d
|
|
@ -1043,11 +1043,44 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
|||
{/* Bank Matching Rules */}
|
||||
{!isNew && (
|
||||
<div className="mt-4 rounded-lg border border-border/60 bg-background/35">
|
||||
<div className="border-b border-border/50 px-3 py-2.5">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Bank matching rules</p>
|
||||
<p className="mt-0.5 text-[11px] text-muted-foreground/70">
|
||||
Transactions whose description contains these patterns are automatically imported as payments.
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-border/50 px-3 py-2.5">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Bank matching rules</p>
|
||||
<p className="mt-0.5 text-[11px] text-muted-foreground/70">
|
||||
Transactions whose description contains these patterns are automatically imported as payments.
|
||||
</p>
|
||||
</div>
|
||||
{sourceBill?.has_merchant_rule && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="shrink-0 gap-1.5 text-xs"
|
||||
disabled={syncingPayments}
|
||||
title="Scan unmatched bank transactions and import any matching payments for this bill"
|
||||
onClick={async () => {
|
||||
setSyncingPayments(true);
|
||||
try {
|
||||
const result = await api.syncBillSimplefinPayments(sourceBill.id);
|
||||
if (result.added > 0) {
|
||||
toast.success(`${result.added} payment${result.added !== 1 ? 's' : ''} imported from bank history.`);
|
||||
loadLinkedTransactions?.();
|
||||
refetch?.();
|
||||
} else {
|
||||
toast.info('No new matching transactions found.');
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Sync failed.');
|
||||
} finally {
|
||||
setSyncingPayments(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{syncingPayments
|
||||
? <><Loader2 className="h-3.5 w-3.5 animate-spin" />Syncing…</>
|
||||
: <><RefreshCw className="h-3.5 w-3.5" />Sync</>}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-3 py-3">
|
||||
<BillMerchantRules
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const { normalizeMerchant } = require('./subscriptionService');
|
||||
const { computeBalanceDelta } = require('./billsService');
|
||||
|
||||
// Persist a merchant→bill rule so future synced transactions auto-match.
|
||||
function addMerchantRule(db, userId, billId, merchant) {
|
||||
|
|
@ -48,10 +49,12 @@ function applyMerchantRules(db, userId) {
|
|||
|
||||
if (txRows.length === 0) return { matched: 0 };
|
||||
|
||||
const getBill = db.prepare('SELECT current_balance, interest_rate FROM bills WHERE id = ? AND deleted_at IS NULL');
|
||||
const insertPayment = db.prepare(`
|
||||
INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id)
|
||||
VALUES (?, ?, ?, 'auto_match', ?)
|
||||
INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id, balance_delta)
|
||||
VALUES (?, ?, ?, 'provider_sync', ?, ?)
|
||||
`);
|
||||
const updateBalance = db.prepare("UPDATE bills SET current_balance = ?, updated_at = datetime('now') WHERE id = ?");
|
||||
const updateTx = db.prepare(`
|
||||
UPDATE transactions
|
||||
SET matched_bill_id = ?, match_status = 'matched', updated_at = datetime('now')
|
||||
|
|
@ -73,10 +76,18 @@ function applyMerchantRules(db, userId) {
|
|||
const paidDate = tx.posted_date || (tx.transacted_at ? String(tx.transacted_at).slice(0, 10) : null);
|
||||
if (!paidDate) continue;
|
||||
|
||||
const amount = Math.round(Math.abs(tx.amount)) / 100;
|
||||
insertPayment.run(rule.bill_id, amount, paidDate, tx.id);
|
||||
updateTx.run(rule.bill_id, tx.id, userId);
|
||||
matched++;
|
||||
const amount = Math.round(Math.abs(tx.amount)) / 100;
|
||||
// Read bill fresh so sequential matches for the same bill chain correctly
|
||||
const bill = getBill.get(rule.bill_id);
|
||||
const balCalc = bill ? computeBalanceDelta(bill, amount) : null;
|
||||
|
||||
const result = insertPayment.run(rule.bill_id, amount, paidDate, tx.id, balCalc?.balance_delta ?? null);
|
||||
if (result.changes > 0) {
|
||||
// Only update balance and mark matched if the payment was actually inserted
|
||||
if (balCalc) updateBalance.run(balCalc.new_balance, rule.bill_id);
|
||||
updateTx.run(rule.bill_id, tx.id, userId);
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
@ -126,10 +137,12 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) {
|
|||
|
||||
if (txRows.length === 0) return { added: 0 };
|
||||
|
||||
const getBill = db.prepare('SELECT current_balance, interest_rate FROM bills WHERE id = ? AND deleted_at IS NULL');
|
||||
const insertPayment = db.prepare(`
|
||||
INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id)
|
||||
VALUES (?, ?, ?, 'auto_match', ?)
|
||||
INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id, balance_delta)
|
||||
VALUES (?, ?, ?, 'provider_sync', ?, ?)
|
||||
`);
|
||||
const updateBalance = db.prepare("UPDATE bills SET current_balance = ?, updated_at = datetime('now') WHERE id = ?");
|
||||
const updateTx = db.prepare(`
|
||||
UPDATE transactions
|
||||
SET matched_bill_id = ?, match_status = 'matched', updated_at = datetime('now')
|
||||
|
|
@ -145,10 +158,16 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) {
|
|||
if (!matches) continue;
|
||||
const paidDate = tx.posted_date || (tx.transacted_at ? String(tx.transacted_at).slice(0, 10) : null);
|
||||
if (!paidDate) continue;
|
||||
const amount = Math.round(Math.abs(tx.amount)) / 100;
|
||||
insertPayment.run(billId, amount, paidDate, tx.id);
|
||||
updateTx.run(billId, tx.id, userId);
|
||||
added++;
|
||||
const amount = Math.round(Math.abs(tx.amount)) / 100;
|
||||
const bill = getBill.get(billId);
|
||||
const balCalc = bill ? computeBalanceDelta(bill, amount) : null;
|
||||
|
||||
const result = insertPayment.run(billId, amount, paidDate, tx.id, balCalc?.balance_delta ?? null);
|
||||
if (result.changes > 0) {
|
||||
if (balCalc) updateBalance.run(balCalc.new_balance, billId);
|
||||
updateTx.run(billId, tx.id, userId);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,26 +10,11 @@ const HKDF_INFO = 'bill-tracker-token-encryption-v1';
|
|||
// Prefix that identifies ciphertext produced with HKDF key derivation.
|
||||
const V2_PREFIX = 'v2:';
|
||||
|
||||
let _warnedAutoKey = false;
|
||||
|
||||
// Returns the raw key material (IKM) without derivation — shared by both paths.
|
||||
// Returns the raw key material (IKM) without derivation.
|
||||
// The encryption key is auto-generated on first run and stored in the database
|
||||
// under `_auto_encryption_key`. No environment variable required — all settings
|
||||
// live in the app. The key is created once and reused across restarts.
|
||||
function getIkm() {
|
||||
const envRaw = process.env.TOKEN_ENCRYPTION_KEY || '';
|
||||
if (envRaw) {
|
||||
const buf = Buffer.from(envRaw, 'utf8');
|
||||
if (buf.length < 32) throw new Error('TOKEN_ENCRYPTION_KEY must be at least 32 bytes');
|
||||
return buf;
|
||||
}
|
||||
|
||||
if (!_warnedAutoKey) {
|
||||
_warnedAutoKey = true;
|
||||
console.warn(
|
||||
'[security] TOKEN_ENCRYPTION_KEY is not set. Using an auto-generated key ' +
|
||||
'stored in the database alongside the encrypted data. Set TOKEN_ENCRYPTION_KEY ' +
|
||||
'as an environment variable to keep the key separate from the data it protects.',
|
||||
);
|
||||
}
|
||||
|
||||
// Lazy-require to avoid circular dependency at module load time
|
||||
const { getSetting, setSetting } = require('../db/database');
|
||||
let stored = getSetting('_auto_encryption_key');
|
||||
|
|
|
|||
Loading…
Reference in New Issue