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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user