Private
Public Access
1
0

Make Plaid credentials per-user, configurable via Settings UI

Plaid ClientId/Secret are now stored per-user in the database
(encrypted) instead of in appsettings.json. Users enter their own
Plaid developer credentials in the Settings > Bank Connections tab.

- New BankSyncSettings model (1:1 with User) for encrypted credentials
- PlaidSyncProvider is now stateless, receives credentials per-call
- Settings UI shows a credentials card with save form
- Connect via Plaid button disabled until credentials are configured
- Removed Plaid section from appsettings.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 16:50:48 -05:00
parent 849a392d14
commit 9d6a006045
17 changed files with 1847 additions and 160 deletions
+97 -5
View File
@@ -25,8 +25,63 @@
<!-- Tab 2: Bank Connections -->
<v-tabs-window-item value="connections">
<!-- Plaid API Credentials -->
<v-card class="mb-4">
<v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-key</v-icon>
Plaid API Credentials
<v-chip v-if="plaidCredentials.isConfigured" color="success" size="small" class="ml-2">Configured</v-chip>
<v-chip v-else color="warning" size="small" class="ml-2">Not configured</v-chip>
</v-card-title>
<v-card-text>
<p class="text-body-2 text-medium-emphasis mb-3">
Enter your Plaid developer credentials. Get them from your Plaid dashboard.
</p>
<v-row dense>
<v-col cols="12" sm="4">
<v-text-field
v-model="plaidForm.clientId"
label="Client ID"
variant="outlined"
density="compact"
:placeholder="plaidCredentials.isConfigured ? '(saved)' : ''"
/>
</v-col>
<v-col cols="12" sm="4">
<v-text-field
v-model="plaidForm.secret"
label="Secret"
variant="outlined"
density="compact"
type="password"
:placeholder="plaidCredentials.isConfigured ? '(saved)' : ''"
/>
</v-col>
<v-col cols="12" sm="2">
<v-select
v-model="plaidForm.environment"
:items="plaidEnvironments"
label="Environment"
variant="outlined"
density="compact"
/>
</v-col>
<v-col cols="12" sm="2" class="d-flex align-center">
<v-btn
color="primary"
:loading="plaidCredsSaving"
:disabled="!plaidForm.clientId || !plaidForm.secret"
@click="savePlaidCredentials"
>
Save
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
<div class="d-flex justify-end mb-4 ga-2">
<v-btn color="primary" prepend-icon="mdi-bank-plus" @click="startPlaidLink" :loading="plaidLoading">
<v-btn color="primary" prepend-icon="mdi-bank-plus" @click="startPlaidLink" :loading="plaidLoading" :disabled="!plaidCredentials.isConfigured">
Connect via Plaid
</v-btn>
<v-btn color="secondary" prepend-icon="mdi-link-plus" @click="showSimpleFinDialog = true">
@@ -35,7 +90,7 @@
</div>
<v-alert v-if="connections.length === 0 && !loading" type="info" variant="tonal" class="mb-4">
No bank connections yet. Click a button above to connect your bank.
No bank connections yet. Configure your API credentials above, then click a button to connect your bank.
</v-alert>
<v-card v-for="conn in connections" :key="conn.id" class="mb-4">
@@ -308,7 +363,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { bankSyncApi } from '@/services/bankSync'
import { accountsApi } from '@/services/accounts'
import type { SyncConnection, LinkedAccountInfo, Account, SyncLogEntry } from '@/types'
import type { SyncConnection, LinkedAccountInfo, Account, SyncLogEntry, PlaidCredentials } from '@/types'
const authStore = useAuthStore()
@@ -319,7 +374,13 @@ const accounts = ref<Account[]>([])
const syncIntervals = reactive<Record<string, number>>({})
const syncingConnections = reactive<Record<string, boolean>>({})
// Plaid
// Plaid credentials
const plaidCredentials = ref<PlaidCredentials>({ isConfigured: false, environment: 'sandbox' })
const plaidForm = reactive({ clientId: '', secret: '', environment: 'sandbox' })
const plaidCredsSaving = ref(false)
const plaidEnvironments = ['sandbox', 'development', 'production']
// Plaid link
const plaidLoading = ref(false)
// SimpleFIN
@@ -396,6 +457,37 @@ function formatCurrency(value: number) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value)
}
async function loadPlaidCredentials() {
try {
const { data } = await bankSyncApi.getPlaidCredentials()
plaidCredentials.value = data
plaidForm.environment = data.environment
} catch {
// Not configured yet
}
}
async function savePlaidCredentials() {
plaidCredsSaving.value = true
try {
const { data } = await bankSyncApi.savePlaidCredentials({
clientId: plaidForm.clientId,
secret: plaidForm.secret,
environment: plaidForm.environment
})
plaidCredentials.value = data
plaidForm.clientId = ''
plaidForm.secret = ''
syncResultMessage.value = 'Plaid credentials saved'
showSyncResult.value = true
} catch (err: any) {
errorMessage.value = err.response?.data?.error || 'Failed to save Plaid credentials'
showError.value = true
} finally {
plaidCredsSaving.value = false
}
}
async function loadConnections() {
loading.value = true
try {
@@ -583,6 +675,6 @@ async function deleteConnection() {
}
onMounted(async () => {
await Promise.all([loadConnections(), loadAccounts(), loadSyncLogs()])
await Promise.all([loadPlaidCredentials(), loadConnections(), loadAccounts(), loadSyncLogs()])
})
</script>