From 88a4b64924df83eb85728254944fd54c8c2a22c2 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 28 May 2026 22:06:15 -0500 Subject: [PATCH] feat: DB-first bank sync config, admin toggle, extracted BankSyncSection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New: services/bankSyncConfigService.js — bank_sync_enabled from settings table, env fallback client/components/admin/BankSyncAdminCard.jsx — single toggle + encryption key status client/components/data/BankSyncSection.jsx — full connection management extracted from SettingsPage Modified: routes/dataSources.js — per-request getBankSyncConfig() instead of module-level env check routes/admin.js — GET/PUT /api/admin/bank-sync-config AdminPage.jsx — renders BankSyncAdminCard after EmailNotifCard SettingsPage.jsx — BankSyncSection removed, 580->352 lines DataPage.jsx — BankSyncSection first, passes simplefinConn to TransactionMatchingSection TransactionMatchingSection.jsx — compact sync bar with green dot + Sync Now Layout.jsx — SimplefinBadge shows muted dot when enabled client/api.js — bankSyncConfig API calls --- client/api.js | 4 + client/components/admin/BankSyncAdminCard.jsx | 99 +++++++ client/components/data/BankSyncSection.jsx | 230 +++++++++++++++++ .../data/TransactionMatchingSection.jsx | 53 +++- client/components/layout/Layout.jsx | 22 ++ client/pages/AdminPage.jsx | 2 + client/pages/DataPage.jsx | 15 +- client/pages/SettingsPage.jsx | 241 +----------------- routes/admin.js | 21 ++ routes/dataSources.js | 12 +- services/bankSyncConfigService.js | 50 ++++ 11 files changed, 499 insertions(+), 250 deletions(-) create mode 100644 client/components/admin/BankSyncAdminCard.jsx create mode 100644 client/components/data/BankSyncSection.jsx create mode 100644 services/bankSyncConfigService.js diff --git a/client/api.js b/client/api.js index ba84dd3..ad51051 100644 --- a/client/api.js +++ b/client/api.js @@ -309,6 +309,10 @@ export const api = { syncDataSource: (id) => post(`/data-sources/${id}/sync`), deleteDataSource: (id) => del(`/data-sources/${id}`), + // Admin — bank sync feature flag + bankSyncConfig: () => get('/admin/bank-sync-config'), + setBankSyncConfig: (data) => put('/admin/bank-sync-config', data), + // User SQLite import previewUserDbImport: async (file) => { const res = await fetch('/api/import/user-db/preview', { diff --git a/client/components/admin/BankSyncAdminCard.jsx b/client/components/admin/BankSyncAdminCard.jsx new file mode 100644 index 0000000..62ffc64 --- /dev/null +++ b/client/components/admin/BankSyncAdminCard.jsx @@ -0,0 +1,99 @@ +import React, { useState, useEffect } from 'react'; +import { toast } from 'sonner'; +import { api } from '@/api'; +import { Button } from '@/components/ui/button'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Toggle } from './adminShared'; + +export default function BankSyncAdminCard() { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + api.bankSyncConfig() + .then(d => { + setConfig(d); + setEnabled(!!d.enabled); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + const handleSave = async () => { + setSaving(true); + try { + const result = await api.setBankSyncConfig({ enabled }); + setConfig(result); + setEnabled(!!result.enabled); + toast.success(enabled ? 'Bank sync enabled.' : 'Bank sync disabled.'); + } catch (err) { + toast.error(err.message || 'Failed to update bank sync setting.'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( + + + Loading… + + + ); + } + + const keySet = config?.encryption_key_set; + const changed = enabled !== !!config?.enabled; + + return ( + + + Bank Sync (SimpleFIN) +

+ Allow users to connect their own SimpleFIN Bridge account to sync + read-only bank transactions. Each user manages their own connection + from the Data page — no bank credentials are stored. +

+
+ + + {/* Encryption key status */} +
+ {keySet + ? 'TOKEN_ENCRYPTION_KEY is configured. Bank sync can be enabled.' + : 'TOKEN_ENCRYPTION_KEY is not set. Add a 32+ character key to your environment before enabling bank sync.'} +
+ + {/* Enable toggle */} +
+
+

Allow users to connect SimpleFIN

+

+ When enabled, users see a Bank Sync section on their Data page. +

+
+ setEnabled(v)} + disabled={!keySet} + label="Enable bank sync" + /> +
+ +
+ +
+ +
+
+ ); +} diff --git a/client/components/data/BankSyncSection.jsx b/client/components/data/BankSyncSection.jsx new file mode 100644 index 0000000..d325134 --- /dev/null +++ b/client/components/data/BankSyncSection.jsx @@ -0,0 +1,230 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { toast } from 'sonner'; +import { Building2, Link2Off, Loader2, RefreshCw } from 'lucide-react'; +import { api } from '@/api'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { SectionCard } from './dataShared'; + +export default function BankSyncSection({ onConnectionChange }) { + const [enabled, setEnabled] = useState(null); + const [connections, setConnections] = useState([]); + const [loadError, setLoadError] = useState(''); + const [setupToken, setSetupToken] = useState(''); + const [connecting, setConnecting] = useState(false); + const [syncing, setSyncing] = useState(null); + const [disconnectTarget, setDisconnectTarget] = useState(null); + const [disconnecting, setDisconnecting] = useState(false); + + const load = useCallback(async () => { + setLoadError(''); + try { + const [status, sources] = await Promise.all([ + api.simplefinStatus(), + api.dataSources({ type: 'provider_sync' }), + ]); + setEnabled(status.enabled); + const conns = Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : []; + setConnections(conns); + onConnectionChange?.(conns[0] || null); + } catch (err) { + setEnabled(false); + setLoadError(err.message || 'Failed to load bank sync status'); + } + }, [onConnectionChange]); + + useEffect(() => { load(); }, [load]); + + const handleConnect = async () => { + const token = setupToken.trim(); + if (!token) { toast.error('Paste your SimpleFIN setup token first.'); return; } + setConnecting(true); + try { + const result = await api.connectSimplefin(token); + toast.success(`Connected — ${result.accountsUpserted} account(s), ${result.transactionsNew} new transaction(s).`); + setSetupToken(''); + await load(); + } catch (err) { + toast.error(err.message || 'Failed to connect SimpleFIN'); + } finally { + setConnecting(false); + } + }; + + const handleSync = async (id) => { + setSyncing(id); + try { + const result = await api.syncDataSource(id); + toast.success(`Synced — ${result.transactionsNew} new transaction(s).`); + await load(); + } catch (err) { + toast.error(err.message || 'Sync failed'); + await load(); + } finally { + setSyncing(null); + } + }; + + const handleDisconnect = async () => { + if (!disconnectTarget) return; + setDisconnecting(true); + try { + await api.deleteDataSource(disconnectTarget.id); + toast.success('SimpleFIN disconnected.'); + setDisconnectTarget(null); + await load(); + } catch (err) { + toast.error(err.message || 'Failed to disconnect'); + } finally { + setDisconnecting(false); + } + }; + + function fmtDate(iso) { + if (!iso) return '—'; + return new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); + } + + if (enabled === null) { + return ( + +
+ + Loading… +
+
+ ); + } + + return ( + <> + + {!enabled ? ( +
+ Bank sync is not enabled on this server. Ask your administrator to enable it in the Admin panel. +
+ ) : ( + <> + {loadError && ( +
{loadError}
+ )} + + {connections.length > 0 && connections.map(conn => ( +
+
+
+ +
+

{conn.name}

+

+ {conn.account_count} account{conn.account_count !== 1 ? 's' : ''} ·{' '} + {conn.transaction_count} transaction{conn.transaction_count !== 1 ? 's' : ''} +

+
+
+
+ + +
+
+ +
+
+

Last sync

+

{fmtDate(conn.last_sync_at)}

+
+
+

Status

+

+ {conn.last_error ? conn.last_error : (conn.status === 'active' ? 'Active' : conn.status)} +

+
+
+
+ ))} + + {connections.length === 0 && ( +
+
+

Connect a SimpleFIN Bridge account

+

Paste your SimpleFIN setup token below. BillTracker only stores an encrypted access URL — no bank credentials are saved.

+
+
+ setSetupToken(e.target.value)} + placeholder="Paste SimpleFIN setup token…" + className="flex-1 font-mono text-xs" + /> + +
+
+ )} + + {connections.length > 0 && ( +
+

Add another connection

+
+ setSetupToken(e.target.value)} + placeholder="Paste SimpleFIN setup token…" + className="flex-1 font-mono text-xs" + /> + +
+
+ )} + + )} +
+ + { if (!open) setDisconnectTarget(null); }}> + + + Disconnect SimpleFIN? + + This removes the connection and deletes synced accounts. Previously synced transactions + are kept but will no longer be associated with a data source. + + + + Cancel + + {disconnecting ? 'Disconnecting…' : 'Disconnect'} + + + + + + ); +} diff --git a/client/components/data/TransactionMatchingSection.jsx b/client/components/data/TransactionMatchingSection.jsx index 4098211..00a96ed 100644 --- a/client/components/data/TransactionMatchingSection.jsx +++ b/client/components/data/TransactionMatchingSection.jsx @@ -294,7 +294,16 @@ function MatchBillDialog({ open, onOpenChange, transaction, bills, onConfirm, lo ); } -export default function TransactionMatchingSection({ refreshKey }) { +function timeAgo(iso) { + if (!iso) return null; + const secs = Math.floor((Date.now() - new Date(iso).getTime()) / 1000); + if (secs < 60) return 'just now'; + if (secs < 3600) return `${Math.floor(secs / 60)}m ago`; + if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`; + return `${Math.floor(secs / 86400)}d ago`; +} + +export default function TransactionMatchingSection({ refreshKey, simplefinConn }) { const [transactions, setTransactions] = useState([]); const [suggestions, setSuggestions] = useState([]); const [bills, setBills] = useState([]); @@ -423,11 +432,53 @@ export default function TransactionMatchingSection({ refreshKey }) { } }; + const [quickSyncing, setQuickSyncing] = useState(false); + + const handleQuickSync = async () => { + if (!simplefinConn) return; + setQuickSyncing(true); + try { + await api.syncDataSource(simplefinConn.id); + await refreshTransactionWorkbench(); + } catch (err) { + toast.error(err.message || 'Sync failed'); + } finally { + setQuickSyncing(false); + } + }; + return ( + {simplefinConn && ( +
+ + + SimpleFIN + {simplefinConn.last_sync_at && ( + · synced {timeAgo(simplefinConn.last_sync_at)} + )} + {simplefinConn.last_error && ( + · {simplefinConn.last_error} + )} + + +
+ )}
diff --git a/client/components/layout/Layout.jsx b/client/components/layout/Layout.jsx index 0440d55..394c482 100644 --- a/client/components/layout/Layout.jsx +++ b/client/components/layout/Layout.jsx @@ -1,5 +1,26 @@ +import React, { useState, useEffect } from 'react'; import { Link, Outlet } from 'react-router-dom'; import AppNavigation from './Sidebar'; +import { api } from '@/api'; + +function SimplefinBadge() { + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + api.simplefinStatus() + .then(d => setEnabled(!!d.enabled)) + .catch(() => {}); + }, []); + + if (!enabled) return null; + + return ( + + + ); +} export default function Layout({ mainContentId }) { return ( @@ -21,6 +42,7 @@ export default function Layout({ mainContentId }) { > About Release Notes +
); diff --git a/client/pages/AdminPage.jsx b/client/pages/AdminPage.jsx index 74ec73e..9db6eb9 100644 --- a/client/pages/AdminPage.jsx +++ b/client/pages/AdminPage.jsx @@ -4,6 +4,7 @@ import { api } from '@/api'; import AppNavigation from '@/components/layout/Sidebar'; import OnboardingWizard from '@/components/admin/OnboardingWizard'; import EmailNotifCard from '@/components/admin/EmailNotifCard'; +import BankSyncAdminCard from '@/components/admin/BankSyncAdminCard'; import LoginModeCard from '@/components/admin/LoginModeCard'; import AuthMethodsCard from '@/components/admin/AuthMethodsCard'; import UsersTable from '@/components/admin/UsersTable'; @@ -69,6 +70,7 @@ export default function AdminPage() { ) : (
+ diff --git a/client/pages/DataPage.jsx b/client/pages/DataPage.jsx index 4b96676..33eee63 100644 --- a/client/pages/DataPage.jsx +++ b/client/pages/DataPage.jsx @@ -1,5 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { api } from '@/api'; +import BankSyncSection from '@/components/data/BankSyncSection'; import ImportTransactionCsvSection from '@/components/data/ImportTransactionCsvSection'; import TransactionMatchingSection from '@/components/data/TransactionMatchingSection'; import ImportSpreadsheetSection from '@/components/data/ImportSpreadsheetSection'; @@ -9,9 +10,10 @@ import DownloadMyDataSection from '@/components/data/DownloadMyDataSection'; import ImportHistorySection from '@/components/data/ImportHistorySection'; export default function DataPage() { - const [history, setHistory] = useState(null); + const [history, setHistory] = useState(null); const [historyLoading, setHistoryLoading] = useState(true); const [transactionRefreshKey, setTransactionRefreshKey] = useState(0); + const [simplefinConn, setSimplefinConn] = useState(null); const loadHistory = async () => { setHistoryLoading(true); @@ -32,6 +34,12 @@ export default function DataPage() { setTransactionRefreshKey(key => key + 1); }; + // Called by BankSyncSection when connection state changes (connect/sync/disconnect) + const handleConnectionChange = useCallback((conn) => { + setSimplefinConn(conn || null); + setTransactionRefreshKey(key => key + 1); + }, []); + return (
@@ -47,8 +55,9 @@ export default function DataPage() {
+ - +
diff --git a/client/pages/SettingsPage.jsx b/client/pages/SettingsPage.jsx index b335c54..7332311 100644 --- a/client/pages/SettingsPage.jsx +++ b/client/pages/SettingsPage.jsx @@ -1,15 +1,11 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; -import { AlertCircle, Building2, Link2Off, Loader2, Moon, RefreshCw, Sun, Users } from 'lucide-react'; +import { AlertCircle, Moon, RefreshCw, Sun, Users } from 'lucide-react'; import { api } from '@/api'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { - AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, - AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from '@/components/ui/alert-dialog'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; @@ -212,239 +208,6 @@ function SettingsSkeleton() { ); } -// ─── Bank Sync Section ──────────────────────────────────────────────────────── - -function BankSyncSection() { - const [enabled, setEnabled] = useState(null); // null = loading - const [connections, setConnections] = useState([]); - const [loadError, setLoadError] = useState(''); - const [setupToken, setSetupToken] = useState(''); - const [connecting, setConnecting] = useState(false); - const [syncing, setSyncing] = useState(null); // id being synced - const [disconnectTarget, setDisconnectTarget] = useState(null); - const [disconnecting, setDisconnecting] = useState(false); - - const load = useCallback(async () => { - setLoadError(''); - try { - const [status, sources] = await Promise.all([ - api.simplefinStatus(), - api.dataSources({ type: 'provider_sync' }), - ]); - setEnabled(status.enabled); - setConnections(Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : []); - } catch (err) { - setEnabled(false); - setLoadError(err.message || 'Failed to load bank sync status'); - } - }, []); - - useEffect(() => { load(); }, [load]); - - const handleConnect = async () => { - const token = setupToken.trim(); - if (!token) { toast.error('Paste your SimpleFIN setup token first.'); return; } - setConnecting(true); - try { - const result = await api.connectSimplefin(token); - toast.success(`Connected — ${result.accountsUpserted} account(s), ${result.transactionsNew} new transaction(s).`); - setSetupToken(''); - await load(); - } catch (err) { - toast.error(err.message || 'Failed to connect SimpleFIN'); - } finally { - setConnecting(false); - } - }; - - const handleSync = async (id) => { - setSyncing(id); - try { - const result = await api.syncDataSource(id); - toast.success(`Synced — ${result.transactionsNew} new transaction(s).`); - await load(); - } catch (err) { - toast.error(err.message || 'Sync failed'); - await load(); - } finally { - setSyncing(null); - } - }; - - const handleDisconnect = async () => { - if (!disconnectTarget) return; - setDisconnecting(true); - try { - await api.deleteDataSource(disconnectTarget.id); - toast.success('SimpleFIN disconnected.'); - setDisconnectTarget(null); - await load(); - } catch (err) { - toast.error(err.message || 'Failed to disconnect'); - } finally { - setDisconnecting(false); - } - }; - - function fmtDate(iso) { - if (!iso) return '—'; - return new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); - } - - if (enabled === null) { - return ( - -
- - Loading… -
-
- ); - } - - return ( - <> - - {!enabled ? ( -
- Bank sync is not enabled on this server. - Set BANK_SYNC_ENABLED=true and - a TOKEN_ENCRYPTION_KEY in your - environment to enable SimpleFIN. -
- ) : ( - <> - {loadError && ( -
{loadError}
- )} - - {/* Connected accounts */} - {connections.length > 0 && connections.map(conn => ( -
-
-
- -
-

{conn.name}

-

- {conn.account_count} account{conn.account_count !== 1 ? 's' : ''} ·{' '} - {conn.transaction_count} transaction{conn.transaction_count !== 1 ? 's' : ''} -

-
-
-
- - -
-
- -
-
-

Last sync

-

{fmtDate(conn.last_sync_at)}

-
-
-

Status

-

- {conn.last_error ? conn.last_error : (conn.status === 'active' ? 'Active' : conn.status)} -

-
-
-
- ))} - - {/* Connect form */} - {connections.length === 0 && ( -
-
-

Connect a SimpleFIN Bridge account

-

Paste your SimpleFIN setup token below. BillTracker only stores an encrypted access URL — no bank credentials are saved.

-
-
- setSetupToken(e.target.value)} - placeholder="Paste SimpleFIN setup token…" - className="flex-1 font-mono text-xs" - /> - -
-
- )} - - {/* Add another connection — only show if already connected */} - {connections.length > 0 && ( -
-

Add another connection

-
- setSetupToken(e.target.value)} - placeholder="Paste SimpleFIN setup token…" - className="flex-1 font-mono text-xs" - /> - -
-
- )} - - )} -
- - { if (!open) setDisconnectTarget(null); }}> - - - Disconnect SimpleFIN? - - This removes the connection and deletes synced accounts. Previously synced transactions - are kept but will no longer be associated with a data source. - - - - Cancel - - {disconnecting ? 'Disconnecting…' : 'Disconnect'} - - - - - - ); -} - // ─── SettingsPage ───────────────────────────────────────────────────────────── export default function SettingsPage() { @@ -576,8 +339,6 @@ export default function SettingsPage() { - {/* Bank Sync */} - {/* Save button — right-aligned below all cards */}
diff --git a/routes/admin.js b/routes/admin.js index 205e9a2..047197f 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const { getDb, rollbackMigration } = require('../db/database'); +const { getBankSyncConfig, setBankSyncEnabled } = require('../services/bankSyncConfigService'); const { hashPassword } = require('../services/authService'); const { logAudit } = require('../services/auditService'); const { @@ -396,6 +397,26 @@ router.put('/auth-mode', (req, res) => { } }); +// ── Bank Sync Config ────────────────────────────────────────────────────────── + +// GET /api/admin/bank-sync-config +router.get('/bank-sync-config', (req, res) => { + res.json(getBankSyncConfig()); +}); + +// PUT /api/admin/bank-sync-config +router.put('/bank-sync-config', (req, res) => { + const enabled = req.body?.enabled; + if (typeof enabled !== 'boolean') { + return res.status(400).json({ error: 'enabled must be a boolean' }); + } + try { + res.json(setBankSyncEnabled(enabled)); + } catch (err) { + res.status(err.status || 500).json({ error: err.message || 'Failed to update bank sync config' }); + } +}); + // ── Migration Rollback ──────────────────────────────────────────────────────── router.post('/migrations/rollback', async (req, res) => { const { version } = req.body; diff --git a/routes/dataSources.js b/routes/dataSources.js index 41f6517..3f4d73a 100644 --- a/routes/dataSources.js +++ b/routes/dataSources.js @@ -6,8 +6,7 @@ const { standardizeError } = require('../middleware/erro const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService'); const { connectSimplefin, syncDataSource, disconnectDataSource } = require('../services/bankSyncService'); const { sanitizeErrorMessage } = require('../services/simplefinService'); - -const BANK_SYNC_ENABLED = process.env.BANK_SYNC_ENABLED === 'true'; +const { getBankSyncConfig } = require('../services/bankSyncConfigService'); const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']); const VALID_STATUSES = new Set(['active', 'inactive', 'error']); @@ -63,13 +62,14 @@ router.get('/', (req, res) => { // ─── GET /api/data-sources/simplefin/status ────────────────────────────────── router.get('/simplefin/status', (req, res) => { - res.json({ enabled: BANK_SYNC_ENABLED }); + const { enabled } = getBankSyncConfig(); + res.json({ enabled }); }); // ─── POST /api/data-sources/simplefin/connect ──────────────────────────────── router.post('/simplefin/connect', async (req, res) => { - if (!BANK_SYNC_ENABLED) { + if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } @@ -91,7 +91,7 @@ router.post('/simplefin/connect', async (req, res) => { // ─── POST /api/data-sources/:id/sync ───────────────────────────────────────── router.post('/:id/sync', async (req, res) => { - if (!BANK_SYNC_ENABLED) { + if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } @@ -113,7 +113,7 @@ router.post('/:id/sync', async (req, res) => { // ─── DELETE /api/data-sources/:id ──────────────────────────────────────────── router.delete('/:id', (req, res) => { - if (!BANK_SYNC_ENABLED) { + if (!getBankSyncConfig().enabled) { return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED')); } diff --git a/services/bankSyncConfigService.js b/services/bankSyncConfigService.js new file mode 100644 index 0000000..d451c7a --- /dev/null +++ b/services/bankSyncConfigService.js @@ -0,0 +1,50 @@ +'use strict'; + +const { getSetting, setSetting } = require('../db/database'); + +const SYNC_DAYS_DEFAULT = 90; + +function encryptionKeyReady() { + const key = process.env.TOKEN_ENCRYPTION_KEY || ''; + return Buffer.from(key, 'utf8').length >= 32; +} + +function getBankSyncConfig() { + const dbValue = getSetting('bank_sync_enabled'); + const envValue = process.env.BANK_SYNC_ENABLED; + + let enabled; + if (dbValue !== null && dbValue !== undefined && dbValue !== '') { + enabled = dbValue === 'true'; + } else if (envValue !== undefined && envValue !== '') { + enabled = envValue === 'true'; + } else { + enabled = false; + } + + const syncDaysDb = parseInt(getSetting('simplefin_sync_days') || '', 10); + const syncDaysEnv = parseInt(process.env.SIMPLEFIN_SYNC_DAYS || '', 10); + const syncDays = Number.isFinite(syncDaysDb) && syncDaysDb > 0 + ? syncDaysDb + : Number.isFinite(syncDaysEnv) && syncDaysEnv > 0 + ? syncDaysEnv + : SYNC_DAYS_DEFAULT; + + return { + enabled, + encryption_key_set: encryptionKeyReady(), + sync_days: syncDays, + }; +} + +function setBankSyncEnabled(enabled) { + if (enabled && !encryptionKeyReady()) { + const err = new Error('TOKEN_ENCRYPTION_KEY must be set (32+ chars) before enabling bank sync'); + err.status = 400; + throw err; + } + setSetting('bank_sync_enabled', enabled ? 'true' : 'false'); + return getBankSyncConfig(); +} + +module.exports = { getBankSyncConfig, setBankSyncEnabled };