From 16d7919ee8c2e51f2ebce91ee2b5b27d8cd890d7 Mon Sep 17 00:00:00 2001 From: Lantum-Brendan Date: Thu, 6 Nov 2025 10:31:22 +0100 Subject: [PATCH 1/7] enh: Improve auth.token security --- composables/useAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composables/useAuth.ts b/composables/useAuth.ts index f689c6f..c2b2ab1 100644 --- a/composables/useAuth.ts +++ b/composables/useAuth.ts @@ -9,7 +9,7 @@ export const useAuth = () => { const tokenCookie = useCookie('auth.token', { default: () => null, - httpOnly: false, + httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7 From dfb1e3ef1c0e1a54aa5230e19e44027e42afa072 Mon Sep 17 00:00:00 2001 From: Lantum-Brendan Date: Tue, 11 Nov 2025 13:21:22 +0100 Subject: [PATCH 2/7] fix: Resolve cards display on large screens transactions page --- assets/scss/_transactions-cards.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/scss/_transactions-cards.scss b/assets/scss/_transactions-cards.scss index 75a5b37..2cc267b 100644 --- a/assets/scss/_transactions-cards.scss +++ b/assets/scss/_transactions-cards.scss @@ -2,7 +2,6 @@ .transactions-cards { width: 100%; - display: block; .cards-heading { display: flex; From c54fd7fb2d3842c852a57b0b23a48189ca941894 Mon Sep 17 00:00:00 2001 From: Lantum-Brendan Date: Tue, 18 Nov 2025 13:36:35 +0100 Subject: [PATCH 3/7] feat: Align onboarding with server configs --- assets/scss/_transaction-form.scss | 27 ++++ components/ContentCard.vue | 35 ++++- components/ContentTable.vue | 5 + components/TransactionForm.vue | 34 +++-- components/WalletForm.vue | 25 +++- components/settings/CollapsibleSection.vue | 2 +- components/settings/SettingsGeneral.vue | 103 +++++++++++--- components/settings/SettingsWallets.vue | 98 +++++++++++-- composables/useAuth.ts | 2 +- composables/useSharedData.ts | 144 +++++++++++++++---- composables/useStatistics.ts | 15 ++ pages/onboarding.vue | 158 +++++++++++++++++++-- pages/wallets.vue | 13 +- services/api/configurationsApi.ts | 93 +++++------- types/configuration.ts | 27 ++++ utils/configurationKeys.ts | 14 ++ 16 files changed, 652 insertions(+), 143 deletions(-) create mode 100644 types/configuration.ts create mode 100644 utils/configurationKeys.ts diff --git a/assets/scss/_transaction-form.scss b/assets/scss/_transaction-form.scss index 0465f93..987bf97 100644 --- a/assets/scss/_transaction-form.scss +++ b/assets/scss/_transaction-form.scss @@ -483,3 +483,30 @@ } } } + +.wallet-field-wrapper { + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + + .wallet-default-indicator { + position: absolute; + top: 34px; + right: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + background: $primary; + color: white; + font-size: 0.65rem; + font-weight: 700; + padding: 2px 6px; + border-radius: 3px; + white-space: nowrap; + letter-spacing: 0.4px; + text-transform: uppercase; + pointer-events: none; + } +} + diff --git a/components/ContentCard.vue b/components/ContentCard.vue index 5db4168..6c7065c 100644 --- a/components/ContentCard.vue +++ b/components/ContentCard.vue @@ -1,8 +1,11 @@ diff --git a/components/settings/SettingsWallets.vue b/components/settings/SettingsWallets.vue index da5ccd1..7b8c5d0 100644 --- a/components/settings/SettingsWallets.vue +++ b/components/settings/SettingsWallets.vue @@ -3,12 +3,20 @@
- + -

{{ wallet }}

+
+

{{ walletLabel || '—' }}

+ Currently Selected +
@@ -33,14 +41,20 @@ @@ -80,6 +128,32 @@ const handleSave = () => { font-weight: $font-medium; } +.wallet-display { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .text-display { + margin: 0; + } + + .wallet-badge { + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba($primary, 0.1); + color: $primary; + font-size: 0.75rem; + font-weight: 700; + padding: 4px 10px; + border-radius: 4px; + width: fit-content; + letter-spacing: 0.5px; + text-transform: uppercase; + border: 1px solid $primary; + } +} + .inline-icon { width: 18px; height: 18px; diff --git a/composables/useAuth.ts b/composables/useAuth.ts index c2b2ab1..f689c6f 100644 --- a/composables/useAuth.ts +++ b/composables/useAuth.ts @@ -9,7 +9,7 @@ export const useAuth = () => { const tokenCookie = useCookie('auth.token', { default: () => null, - httpOnly: true, + httpOnly: false, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7 diff --git a/composables/useSharedData.ts b/composables/useSharedData.ts index f4f6c33..5ff1a71 100644 --- a/composables/useSharedData.ts +++ b/composables/useSharedData.ts @@ -4,9 +4,9 @@ import type { Category } from '~/types/category'; import type { Party } from '~/types/party'; import type { Wallet } from '~/types/wallet'; import type { Group } from '~/services/api/groupsApi'; -import type { Configuration } from '~/services/api/configurationsApi'; import { checkAuth } from '~/utils/auth'; import { extractApiErrors } from '~/utils/apiErrors'; +import type { ConfigurationItem } from '~/types/configuration'; /** * Shared data composable for centralized state management @@ -18,7 +18,6 @@ const categories = ref([]); const parties = ref([]); const wallets = ref([]); const groups = ref([]); -const configurations = ref([]); // Loading states const categoriesLoading = ref(false); @@ -34,14 +33,15 @@ const walletsError = ref(null); const groupsError = ref(null); const configurationsError = ref(null); -// Cache timestamps for lightweight caching (30 seconds for dev, can increase in prod) +// Cache timestamps for lightweight caching const categoriesLastFetched = ref(null); const partiesLastFetched = ref(null); const walletsLastFetched = ref(null); const groupsLastFetched = ref(null); -const configurationsLastFetched = ref(null); +const configurationsLastSync = ref(null); +const configurationsMap = ref | null>(null); -const CACHE_DURATION = 30 * 1000; // 30 seconds +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes // Helper function to deduplicate arrays by ID function deduplicateById(items: T[]): T[] { @@ -122,6 +122,46 @@ export const useSharedData = () => { } ); + const loadConfigurations = async (forceReload = false) => { + if (!forceReload && configurationsMap.value && isCacheValid(configurationsLastSync.value)) { + return configurationsMap.value; + } + + if (configurationsLoading.value) { + while (configurationsLoading.value) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + return configurationsMap.value; + } + + if (!checkAuth()) { + configurationsLoading.value = false; + return configurationsMap.value; + } + + configurationsLoading.value = true; + configurationsError.value = null; + + try { + const response = await api.configurations.fetchAll(); + const items: ConfigurationItem[] = response?.data || []; + const map: Record = {}; + for (const item of items) { + map[item.key] = item.value; + } + configurationsMap.value = map; + configurationsLastSync.value = new Date().toISOString(); + return map; + } catch (err) { + const errorMsg = extractApiErrors(err); + configurationsError.value = errorMsg; + console.error('Error loading configurations:', errorMsg); + throw err; + } finally { + configurationsLoading.value = false; + } + }; + const loadParties = createDataLoader( 'parties', parties, @@ -149,15 +189,6 @@ export const useSharedData = () => { () => api.groups.fetchAll() ); - const loadConfigurations = createDataLoader( - 'configurations', - configurations, - configurationsLoading, - configurationsError, - configurationsLastFetched, - () => api.configurations.fetchAll() - ); - const loadAllData = async (forceReload = false) => { try { await Promise.all([ @@ -183,26 +214,78 @@ export const useSharedData = () => { ); const getDefaultGroup = computed(() => { - const config = configurations.value.find((c) => c.key === 'default-group'); - if (config?.value) { - const group = groups.value.find((g) => g.id === parseInt(config.value)); + const map = configurationsMap.value || {}; + const configured = map['default-group']; + if (configured) { + const groupId = typeof configured === 'object' ? configured.id : configured; + const group = groups.value.find((g) => g.id === parseInt(groupId)); if (group) return group; } return groups.value.length > 0 ? groups.value[0] : null; }); + // Default wallet from configurations (fallback to heuristic) const getDefaultWallet = computed(() => { - const config = configurations.value.find((c) => c.key === 'default-wallet'); - if (config?.value) { - const wallet = wallets.value.find((w) => w.id === parseInt(config.value)); - if (wallet) return wallet; + const map = configurationsMap.value || {}; + const configured = map['default-wallet']; + + // Try to resolve wallet id from config + const resolveConfiguredWalletId = (): string | null => { + if (!configured) return null; + if (typeof configured === 'string' || typeof configured === 'number') + return String(configured); + if (configured && typeof configured === 'object') { + if ( + 'id' in configured && + (typeof configured.id === 'string' || typeof configured.id === 'number') + ) { + return String(configured.id); + } + if ( + 'walletId' in configured && + (typeof configured.walletId === 'string' || typeof configured.walletId === 'number') + ) { + return String(configured.walletId); + } + if ('name' in configured && configured.name) { + const byName = wallets.value.find( + (w) => w.name.toLowerCase() === String(configured.name).toLowerCase() + ); + if (byName) return String(byName.id); + } + } + return null; + }; + + const configuredId = resolveConfiguredWalletId(); + if (configuredId) { + const byId = wallets.value.find( + (w) => + String(w.id) === configuredId || + String(w.sync_state?.client_generated_id || '') === configuredId + ); + if (byId) return byId; } - return wallets.value.length > 0 ? wallets.value[0] : null; + + // Fallback heuristic + return ( + wallets.value.find((wallet) => wallet.name.toLowerCase().includes('default')) || + (wallets.value.length > 0 ? wallets.value[0] : null) + ); }); + // Default currency prefers configuration; fallback to default wallet currency or 'USD' const getDefaultCurrency = computed(() => { - const config = configurations.value.find((c) => c.key === 'default-currency'); - return config?.value || getDefaultWallet.value?.currency || 'USD'; + const map = configurationsMap.value || {}; + const configured = map['default-currency']; + const extractCode = (val: any): string | null => { + if (!val) return null; + if (typeof val === 'string') return val; + if (typeof val === 'object' && 'code' in val) return String(val.code); + return null; + }; + const fromConfig = extractCode(configured); + return fromConfig || getDefaultWallet.value?.currency || 'USD'; }); // Category management functions @@ -274,7 +357,7 @@ export const useSharedData = () => { parties.value = []; wallets.value = []; groups.value = []; - configurations.value = []; + configurationsMap.value = null; categoriesLoading.value = false; partiesLoading.value = false; @@ -292,7 +375,7 @@ export const useSharedData = () => { partiesLastFetched.value = null; walletsLastFetched.value = null; groupsLastFetched.value = null; - configurationsLastFetched.value = null; + configurationsLastSync.value = null; console.log('✅ All shared data cleared for logout'); }; @@ -303,7 +386,7 @@ export const useSharedData = () => { parties: readonly(parties), wallets: readonly(wallets), groups: readonly(groups), - configurations: readonly(configurations), + configurationsMap: readonly(configurationsMap), // Loading states categoriesLoading: readonly(categoriesLoading), @@ -319,6 +402,13 @@ export const useSharedData = () => { groupsError: readonly(groupsError), configurationsError: readonly(configurationsError), + // Last sync timestamps + categoriesLastSync: readonly(categoriesLastFetched), + partiesLastSync: readonly(partiesLastFetched), + walletsLastSync: readonly(walletsLastFetched), + groupsLastSync: readonly(groupsLastFetched), + configurationsLastSync: readonly(configurationsLastSync), + // Computed getters getIncomeCategories, getExpenseCategories, diff --git a/composables/useStatistics.ts b/composables/useStatistics.ts index 9275ea6..efb77ea 100644 --- a/composables/useStatistics.ts +++ b/composables/useStatistics.ts @@ -898,6 +898,21 @@ export const useStatistics = () => { } }; + // Initialize selected wallet from configured default when available + const { getDefaultWallet } = useSharedData(); + watch( + [wallets, () => getDefaultWallet.value], + () => { + if (selectedWalletId.value === null) { + const dw = getDefaultWallet.value; + if (dw && typeof dw.id === 'number') { + selectedWalletId.value = dw.id; + } + } + }, + { immediate: true } + ); + // Watch for changes in selected wallet, period, custom filters, AND when the underlying data becomes available watch( [selectedWalletId, currentPeriod, customFilters, transactions, wallets], diff --git a/pages/onboarding.vue b/pages/onboarding.vue index 73b1c6f..c16eca7 100644 --- a/pages/onboarding.vue +++ b/pages/onboarding.vue @@ -226,12 +226,15 @@
-