feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
'use strict' ;
2026-05-16 20:26:09 -05:00
const router = require ( 'express' ) . Router ( ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
const { getDb } = require ( '../db/database' ) ;
const { standardizeError } = require ( '../middleware/errorFormatter' ) ;
2026-05-16 20:26:09 -05:00
const { decorateDataSource , ensureManualDataSource } = require ( '../services/transactionService' ) ;
2026-05-29 19:58:52 -05:00
const { connectSimplefin , syncDataSource , backfillDataSource , disconnectDataSource } = require ( '../services/bankSyncService' ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
const { sanitizeErrorMessage } = require ( '../services/simplefinService' ) ;
2026-05-28 22:06:15 -05:00
const { getBankSyncConfig } = require ( '../services/bankSyncConfigService' ) ;
2026-06-06 15:08:33 -05:00
const { syncLimiter } = require ( '../middleware/rateLimiter' ) ;
2026-05-16 20:26:09 -05:00
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
const VALID _TYPES = new Set ( [ 'manual' , 'file_import' , 'provider_sync' ] ) ;
2026-05-16 20:26:09 -05:00
const VALID _STATUSES = new Set ( [ 'active' , 'inactive' , 'error' ] ) ;
function cleanFilter ( value ) {
return typeof value === 'string' ? value . trim ( ) : '' ;
}
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
function safeError ( err , fallback ) {
const msg = sanitizeErrorMessage ( err ? . message || fallback ) ;
const status = typeof err ? . status === 'number' ? err . status : 500 ;
return { msg , status } ;
}
2026-06-03 21:09:26 -05:00
// ─── GET /api/data-sources/accounts/all ──────────────────────────────────────
// Returns all financial accounts for the user across all sources.
// Used by the bank tracking account picker.
router . get ( '/accounts/all' , ( req , res ) => {
try {
const db = getDb ( ) ;
const accounts = db . prepare ( `
SELECT
fa . id , fa . name , fa . org _name , fa . account _type ,
fa . balance , fa . available _balance , fa . currency ,
fa . monitored , ds . id AS source _id
FROM financial _accounts fa
JOIN data _sources ds ON ds . id = fa . data _source _id
WHERE fa . user _id = ?
ORDER BY fa . org _name COLLATE NOCASE ASC , fa . name COLLATE NOCASE ASC
` ).all(req.user.id);
res . json ( accounts . map ( a => ( {
... a ,
monitored : a . monitored === 1 ,
balance _dollars : a . balance !== null ? a . balance / 100 : null ,
} ) ) ) ;
} catch ( err ) {
res . status ( 500 ) . json ( standardizeError ( err . message || 'Failed to load accounts' , 'DB_ERROR' ) ) ;
}
} ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
// ─── GET /api/data-sources ────────────────────────────────────────────────────
2026-05-16 20:26:09 -05:00
router . get ( '/' , ( req , res ) => {
const db = getDb ( ) ;
ensureManualDataSource ( db , req . user . id ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
const type = cleanFilter ( req . query . type ) ;
2026-05-16 20:26:09 -05:00
const status = cleanFilter ( req . query . status ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
if ( type && ! VALID _TYPES . has ( type ) ) return res . status ( 400 ) . json ( standardizeError ( 'type must be manual, file_import, or provider_sync' , 'VALIDATION_ERROR' , 'type' ) ) ;
if ( status && ! VALID _STATUSES . has ( status ) ) return res . status ( 400 ) . json ( standardizeError ( 'status must be active, inactive, or error' , 'VALIDATION_ERROR' , 'status' ) ) ;
2026-05-16 20:26:09 -05:00
let query = `
SELECT
ds . id , ds . user _id , ds . type , ds . provider , ds . name , ds . status ,
ds . config _json , ds . last _sync _at , ds . last _error , ds . created _at , ds . updated _at ,
COUNT ( DISTINCT fa . id ) AS account _count ,
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
COUNT ( DISTINCT t . id ) AS transaction _count
2026-05-16 20:26:09 -05:00
FROM data _sources ds
LEFT JOIN financial _accounts fa ON fa . data _source _id = ds . id AND fa . user _id = ds . user _id
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
LEFT JOIN transactions t ON t . data _source _id = ds . id AND t . user _id = ds . user _id
2026-05-16 20:26:09 -05:00
WHERE ds . user _id = ?
` ;
const params = [ req . user . id ] ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
if ( type ) { query += ' AND ds.type = ?' ; params . push ( type ) ; }
if ( status ) { query += ' AND ds.status = ?' ; params . push ( status ) ; }
2026-05-16 20:26:09 -05:00
query += `
GROUP BY ds . id
ORDER BY
CASE WHEN ds . type = 'manual' THEN 0 ELSE 1 END ,
ds . name COLLATE NOCASE ASC
` ;
res . json ( db . prepare ( query ) . all ( ... params ) . map ( decorateDataSource ) ) ;
} ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
// ─── GET /api/data-sources/simplefin/status ──────────────────────────────────
router . get ( '/simplefin/status' , ( req , res ) => {
2026-05-29 02:23:19 -05:00
const { enabled , sync _days } = getBankSyncConfig ( ) ;
2026-06-03 21:59:50 -05:00
const db = getDb ( ) ;
const hasConnections = ! ! db . prepare (
"SELECT 1 FROM data_sources WHERE user_id = ? AND type = 'provider_sync' AND provider = 'simplefin' LIMIT 1"
) . get ( req . user . id ) ;
const hasMerchantRules = ! ! db . prepare (
'SELECT 1 FROM bill_merchant_rules WHERE user_id = ? LIMIT 1'
) . get ( req . user . id ) ;
res . json ( { enabled , sync _days , has _connections : hasConnections , has _merchant _rules : hasMerchantRules } ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
} ) ;
// ─── POST /api/data-sources/simplefin/connect ────────────────────────────────
router . post ( '/simplefin/connect' , async ( req , res ) => {
2026-05-28 22:06:15 -05:00
if ( ! getBankSyncConfig ( ) . enabled ) {
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
return res . status ( 503 ) . json ( standardizeError ( 'Bank sync is not enabled on this server' , 'BANK_SYNC_DISABLED' ) ) ;
}
const setupToken = typeof req . body ? . setupToken === 'string' ? req . body . setupToken . trim ( ) : '' ;
if ( ! setupToken ) {
return res . status ( 400 ) . json ( standardizeError ( 'setupToken is required' , 'VALIDATION_ERROR' , 'setupToken' ) ) ;
}
try {
const db = getDb ( ) ;
const result = await connectSimplefin ( db , req . user . id , setupToken ) ;
res . status ( 201 ) . json ( result ) ;
} catch ( err ) {
const { msg , status } = safeError ( err , 'Failed to connect SimpleFIN' ) ;
res . status ( status ) . json ( standardizeError ( msg , err ? . code || 'SIMPLEFIN_ERROR' ) ) ;
}
} ) ;
2026-05-29 01:06:20 -05:00
// ─── GET /api/data-sources/:sourceId/accounts ────────────────────────────────
router . get ( '/:sourceId/accounts' , ( req , res ) => {
const sourceId = parseInt ( req . params . sourceId , 10 ) ;
if ( ! Number . isInteger ( sourceId ) || sourceId < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid data source id' , 'VALIDATION_ERROR' , 'sourceId' ) ) ;
}
try {
const db = getDb ( ) ;
const source = db . prepare ( 'SELECT id FROM data_sources WHERE id = ? AND user_id = ?' ) . get ( sourceId , req . user . id ) ;
if ( ! source ) return res . status ( 404 ) . json ( standardizeError ( 'Data source not found' , 'NOT_FOUND' ) ) ;
const accounts = db . prepare ( `
SELECT
fa . id , fa . provider _account _id , fa . name , fa . org _name , fa . account _type ,
fa . balance , fa . available _balance , fa . currency , fa . monitored ,
fa . created _at , fa . updated _at ,
COUNT ( t . id ) AS transaction _count
FROM financial _accounts fa
LEFT JOIN transactions t ON t . account _id = fa . id AND t . user _id = fa . user _id
WHERE fa . data _source _id = ? AND fa . user _id = ?
GROUP BY fa . id
ORDER BY fa . name COLLATE NOCASE ASC
` ).all(sourceId, req.user.id);
const txStmt = db . prepare ( `
2026-05-29 03:02:36 -05:00
SELECT t . id , t . posted _date , t . transacted _at , t . amount , t . currency ,
t . payee , t . description , t . memo , t . match _status , t . ignored ,
t . matched _bill _id , b . name AS matched _bill _name
FROM transactions t
LEFT JOIN bills b ON b . id = t . matched _bill _id AND b . user _id = t . user _id AND b . deleted _at IS NULL
WHERE t . account _id = ? AND t . user _id = ?
ORDER BY COALESCE ( t . posted _date , substr ( t . transacted _at , 1 , 10 ) , t . created _at ) DESC , t . id DESC
2026-05-29 01:06:20 -05:00
LIMIT 50
` );
const result = accounts . map ( acc => ( {
... acc ,
monitored : acc . monitored === 1 ,
2026-05-30 21:20:51 -05:00
transactions : acc . monitored === 1 ? txStmt . all ( acc . id , req . user . id ) : [ ] ,
2026-05-29 01:06:20 -05:00
} ) ) ;
res . json ( result ) ;
} catch ( err ) {
res . status ( 500 ) . json ( standardizeError ( err . message || 'Failed to load accounts' , 'DB_ERROR' ) ) ;
}
} ) ;
// ─── PUT /api/data-sources/:sourceId/accounts/:accountId ─────────────────────
router . put ( '/:sourceId/accounts/:accountId' , ( req , res ) => {
const sourceId = parseInt ( req . params . sourceId , 10 ) ;
const accountId = parseInt ( req . params . accountId , 10 ) ;
if ( ! Number . isInteger ( sourceId ) || sourceId < 1 || ! Number . isInteger ( accountId ) || accountId < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid id' , 'VALIDATION_ERROR' ) ) ;
}
if ( typeof req . body ? . monitored !== 'boolean' ) {
return res . status ( 400 ) . json ( standardizeError ( 'monitored must be a boolean' , 'VALIDATION_ERROR' , 'monitored' ) ) ;
}
try {
const db = getDb ( ) ;
const result = db . prepare ( `
UPDATE financial _accounts
SET monitored = ? , updated _at = datetime ( 'now' )
WHERE id = ? AND data _source _id = ? AND user _id = ?
` ).run(req.body.monitored ? 1 : 0, accountId, sourceId, req.user.id);
if ( result . changes === 0 ) return res . status ( 404 ) . json ( standardizeError ( 'Account not found' , 'NOT_FOUND' ) ) ;
const account = db . prepare ( 'SELECT id, name, monitored FROM financial_accounts WHERE id = ?' ) . get ( accountId ) ;
res . json ( { ... account , monitored : account . monitored === 1 } ) ;
} catch ( err ) {
res . status ( 500 ) . json ( standardizeError ( err . message || 'Failed to update account' , 'DB_ERROR' ) ) ;
}
} ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
// ─── POST /api/data-sources/:id/sync ─────────────────────────────────────────
2026-06-06 15:08:33 -05:00
router . post ( '/:id/sync' , syncLimiter , async ( req , res ) => {
2026-05-28 22:06:15 -05:00
if ( ! getBankSyncConfig ( ) . enabled ) {
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
return res . status ( 503 ) . json ( standardizeError ( 'Bank sync is not enabled on this server' , 'BANK_SYNC_DISABLED' ) ) ;
}
const id = parseInt ( req . params . id , 10 ) ;
if ( ! Number . isInteger ( id ) || id < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid data source id' , 'VALIDATION_ERROR' , 'id' ) ) ;
}
try {
const db = getDb ( ) ;
const result = await syncDataSource ( db , req . user . id , id ) ;
res . json ( result ) ;
} catch ( err ) {
const { msg , status } = safeError ( err , 'Sync failed' ) ;
res . status ( status ) . json ( standardizeError ( msg , err ? . code || 'SIMPLEFIN_ERROR' ) ) ;
}
} ) ;
2026-06-03 21:59:50 -05:00
// ─── POST /api/data-sources/sync-all ─────────────────────────────────────────
// Syncs every SimpleFIN source for the current user. Returns aggregated stats.
2026-06-06 15:08:33 -05:00
router . post ( '/sync-all' , syncLimiter , async ( req , res ) => {
2026-06-03 21:59:50 -05:00
if ( ! getBankSyncConfig ( ) . enabled ) {
return res . status ( 503 ) . json ( standardizeError ( 'Bank sync is not enabled on this server' , 'BANK_SYNC_DISABLED' ) ) ;
}
try {
const db = getDb ( ) ;
const sources = db . prepare (
"SELECT id FROM data_sources WHERE user_id = ? AND type = 'provider_sync' AND provider = 'simplefin'"
) . all ( req . user . id ) ;
if ( sources . length === 0 ) {
return res . status ( 404 ) . json ( standardizeError ( 'No SimpleFIN connections found' , 'NOT_FOUND' ) ) ;
}
2026-06-04 00:06:16 -05:00
let accountsUpserted = 0 ;
let transactionsNew = 0 ;
let transactionsSkip = 0 ;
let autoMatched = 0 ;
const matchedBillSet = new Set ( ) ;
const lateAttrAll = [ ] ;
2026-06-03 21:59:50 -05:00
const errors = [ ] ;
for ( const source of sources ) {
try {
const result = await syncDataSource ( db , req . user . id , source . id ) ;
accountsUpserted += result . accountsUpserted ? ? 0 ;
transactionsNew += result . transactionsNew ? ? 0 ;
transactionsSkip += result . transactionsSkip ? ? 0 ;
autoMatched += result . autoMatched ? ? 0 ;
2026-06-03 23:29:30 -05:00
for ( const name of result . matched _bills ? ? [ ] ) matchedBillSet . add ( name ) ;
2026-06-04 00:06:16 -05:00
for ( const attr of result . late _attributions ? ? [ ] ) lateAttrAll . push ( attr ) ;
2026-06-03 21:59:50 -05:00
} catch ( err ) {
errors . push ( sanitizeErrorMessage ( err ? . message || 'Sync failed' ) ) ;
}
}
res . json ( {
2026-06-04 00:06:16 -05:00
accounts _upserted : accountsUpserted ,
transactions _new : transactionsNew ,
transactions _skip : transactionsSkip ,
auto _matched : autoMatched ,
matched _bills : [ ... matchedBillSet ] ,
late _attributions : lateAttrAll ,
2026-06-03 21:59:50 -05:00
errors ,
} ) ;
} catch ( err ) {
const { msg , status } = safeError ( err , 'Sync failed' ) ;
res . status ( status ) . json ( standardizeError ( msg , 'SIMPLEFIN_ERROR' ) ) ;
}
} ) ;
2026-05-29 19:58:52 -05:00
// ─── POST /api/data-sources/:id/backfill ─────────────────────────────────────
2026-06-06 15:08:33 -05:00
router . post ( '/:id/backfill' , syncLimiter , async ( req , res ) => {
2026-05-29 19:58:52 -05:00
if ( ! getBankSyncConfig ( ) . enabled ) {
return res . status ( 503 ) . json ( standardizeError ( 'Bank sync is not enabled on this server' , 'BANK_SYNC_DISABLED' ) ) ;
}
const id = parseInt ( req . params . id , 10 ) ;
if ( ! Number . isInteger ( id ) || id < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid data source id' , 'VALIDATION_ERROR' , 'id' ) ) ;
}
try {
const db = getDb ( ) ;
const result = await backfillDataSource ( db , req . user . id , id ) ;
res . json ( result ) ;
} catch ( err ) {
const { msg , status } = safeError ( err , 'Backfill failed' ) ;
res . status ( status ) . json ( standardizeError ( msg , err ? . code || 'SIMPLEFIN_ERROR' ) ) ;
}
} ) ;
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
// ─── DELETE /api/data-sources/:id ────────────────────────────────────────────
router . delete ( '/:id' , ( req , res ) => {
2026-05-28 22:06:15 -05:00
if ( ! getBankSyncConfig ( ) . enabled ) {
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
return res . status ( 503 ) . json ( standardizeError ( 'Bank sync is not enabled on this server' , 'BANK_SYNC_DISABLED' ) ) ;
}
const id = parseInt ( req . params . id , 10 ) ;
if ( ! Number . isInteger ( id ) || id < 1 ) {
return res . status ( 400 ) . json ( standardizeError ( 'Invalid data source id' , 'VALIDATION_ERROR' , 'id' ) ) ;
}
try {
disconnectDataSource ( getDb ( ) , req . user . id , id ) ;
res . json ( { ok : true } ) ;
} catch ( err ) {
const { msg , status } = safeError ( err , 'Failed to disconnect' ) ;
res . status ( status ) . json ( standardizeError ( msg , err ? . code || 'DISCONNECT_ERROR' ) ) ;
}
} ) ;
2026-05-16 20:26:09 -05:00
module . exports = router ;