@@ -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' : ''}
-
-
-
-
- handleSync(conn.id)}
- disabled={syncing === conn.id}
- className="h-8 text-xs gap-1.5"
- >
- {syncing === conn.id
- ? <> Syncing…>
- : <> Sync Now>}
-
- setDisconnectTarget(conn)}
- className="h-8 text-xs gap-1.5 text-muted-foreground hover:text-destructive"
- >
-
- Disconnect
-
-
-
-
-
-
-
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"
- />
-
- {connecting ? <> Connecting…> : 'Connect'}
-
-
-
- )}
-
- {/* 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"
- />
-
- {connecting ? <> Connecting…> : 'Connect'}
-
-
-
- )}
- >
- )}
-
-
-
{ 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 };