feat: multi-Oracle server support with runtime switching

Complete implementation of multi-server Oracle database support:

Backend:
- Multi-pool Oracle with lazy loading per server
- Email-to-server cache for automatic server discovery
- JWT tokens include server_id claim
- /auth/check-identity and /auth/check-email endpoints
- /auth/my-servers endpoint for listing user's accessible servers
- Server switch with password re-authentication

Frontend:
- New ServerSelector component for header dropdown
- Multi-step login flow (identity → server → password)
- Server switching from header with password modal
- Mobile drawer menu with server selection
- Dark mode support for all new components
- URL bookmark support with ?server= query param

Scripts:
- Unified start.sh replacing start-prod.sh/start-test.sh
- Unified ssh-tunnel.sh with multi-server support
- Updated status.sh for new architecture

Tests:
- E2E tests for multi-server and single-server login flows
- Backend unit tests for all new endpoints
- Oracle multi-pool integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-26 22:39:06 +00:00
parent 5f99ee2fd0
commit b137e80b71
102 changed files with 9398 additions and 2787 deletions

View File

@@ -10,8 +10,83 @@ import { createCompaniesStore } from '@shared/stores/companies'
import { createAccountingPeriodStore } from '@shared/stores/accountingPeriod'
import api from '@reports/services/api'
// Create auth store
export const useAuthStore = createAuthStore(api)
// Import module-specific stores that need to be reset on logout
import { useDashboardStore } from './dashboard'
import { useInvoicesStore } from './invoices'
import { useTreasuryStore } from './treasury'
import { useTrialBalanceStore } from './trialBalance'
/**
* US-028/US-031: Reset all module stores on logout
* Prevents stale data from previous company after re-login
*
* @param {Object} context - Context saved before logout (US-031)
* @param {string} context.username - Username from before logout
* @param {string} context.serverId - Server ID from before logout
*/
const resetAllStores = (context = {}) => {
const { username, serverId } = context
console.log('[Reports] Resetting all stores on logout...', { username, serverId })
// Reset module-specific stores
try {
const dashboardStore = useDashboardStore()
if (dashboardStore.reset) dashboardStore.reset()
} catch (e) {
console.warn('[Reports] Could not reset dashboard store:', e.message)
}
try {
const invoicesStore = useInvoicesStore()
if (invoicesStore.reset) invoicesStore.reset()
} catch (e) {
console.warn('[Reports] Could not reset invoices store:', e.message)
}
try {
const treasuryStore = useTreasuryStore()
if (treasuryStore.reset) treasuryStore.reset()
} catch (e) {
console.warn('[Reports] Could not reset treasury store:', e.message)
}
try {
const trialBalanceStore = useTrialBalanceStore()
if (trialBalanceStore.reset) trialBalanceStore.reset()
} catch (e) {
console.warn('[Reports] Could not reset trial balance store:', e.message)
}
// Reset shared stores (companies and periods)
// Note: These are reset AFTER module stores since they may depend on auth
// US-031: Use resetWithContext with explicit username/serverId
try {
const companyStore = useCompanyStore()
if (companyStore.resetWithContext && username) {
// Use the new method that accepts explicit parameters
companyStore.resetWithContext(username, serverId)
} else if (companyStore.reset) {
// Fallback to regular reset (won't clear localStorage properly)
companyStore.reset()
}
} catch (e) {
console.warn('[Reports] Could not reset company store:', e.message)
}
try {
const periodStore = useAccountingPeriodStore()
if (periodStore.reset) periodStore.reset()
} catch (e) {
console.warn('[Reports] Could not reset period store:', e.message)
}
console.log('[Reports] All stores reset complete')
}
// Create auth store with onLogout callback (US-028)
export const useAuthStore = createAuthStore(api, {
onLogout: resetAllStores
})
// Create companies store (needs auth store reference)
export const useCompanyStore = createCompaniesStore(api, useAuthStore)

View File

@@ -19,7 +19,11 @@
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
:auth-store="authStore"
@logout="handleLogout"
@server-switched="handleServerSwitched"
/>
<!-- US-109: Filter BottomSheet for mobile -->
@@ -425,6 +429,16 @@ const handleLogout = async () => {
router.push('/login');
};
// Handle server switch completed - reload data for new server
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// Reload data for the new server
await companyStore.loadCompanies();
if (companyStore.selectedCompany?.id_firma) {
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma);
}
};
// US-501: Mobile TopBar actions (filter, reset, export dropdown)
const mobileTopBarActions = computed(() => [
{

View File

@@ -19,7 +19,11 @@
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
:auth-store="authStore"
@logout="handleLogout"
@server-switched="handleServerSwitched"
/>
<!-- US-109: Filter BottomSheet for mobile -->
@@ -422,6 +426,16 @@ const handleLogout = async () => {
router.push('/login');
};
// Handle server switched event from drawer menu
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// Reload data for the new server
await companyStore.loadCompanies();
if (companyStore.selectedCompany?.id_firma) {
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma);
}
};
// Mobile TopBar actions
const mobileTopBarActions = computed(() => [
{

View File

@@ -19,7 +19,11 @@
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
:auth-store="authStore"
@logout="handleLogout"
@server-switched="handleServerSwitched"
/>
<!-- US-109: Filter BottomSheet for mobile -->
@@ -422,6 +426,16 @@ const handleLogout = async () => {
router.push('/login');
};
// Handle server switched event from drawer menu
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// Reload data for the new server
await companyStore.loadCompanies();
if (companyStore.selectedCompany?.id_firma) {
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma);
}
};
// Mobile TopBar actions
const mobileTopBarActions = computed(() => [
{

View File

@@ -15,7 +15,11 @@
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
:auth-store="authStore"
@logout="handleLogout"
@server-switched="handleServerSwitched"
/>
<main class="main-content" :class="{ 'mobile-layout': isMobile }">
@@ -628,6 +632,17 @@ const handleLogout = async () => {
router.push('/login');
};
// Handle server switched from drawer menu (password already verified in modal)
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// Reload companies and periods for the new server
await companyStore.loadCompanies();
if (companyStore.selectedCompany?.id_firma) {
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma);
}
// Dashboard will reload automatically via watchers
};
// US-2008: Mobile top bar actions with refresh button
const mobileTopBarActions = computed(() => [
{
@@ -1048,6 +1063,14 @@ const loadNetBalanceBreakdown = async () => {
const loadDashboardData = async () => {
if (!companyStore.selectedCompany) return;
// FIX: Guard against null period - don't load dashboard without valid period
// This prevents API calls with luna=null, an=null which cause backend errors
if (!periodStore.selectedPeriod?.luna || !periodStore.selectedPeriod?.an) {
console.log('[DashboardView] Skipping load - no valid period selected, luna:', periodStore.selectedPeriod?.luna, 'an:', periodStore.selectedPeriod?.an)
return;
}
isLoading.value = true;
// FIX: Reset state înainte de a încărca date noi

View File

@@ -15,9 +15,13 @@
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
:auth-store="authStore"
@logout="handleLogout"
@company-changed="handleCompanyChanged"
@period-changed="handlePeriodChanged"
@server-switched="handleServerSwitched"
/>
<!-- US-603: Mobile Tabs for Clienți/Furnizori -->
@@ -762,6 +766,16 @@ const handleLogout = async () => {
router.push('/login')
}
// Handle server switch completed - reload data for new server
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// Reload data for the new server
await companyStore.loadCompanies()
if (companyStore.selectedCompany?.id_firma) {
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma)
}
}
const handleCompanyChanged = (company) => {
// Company store watcher handles the refresh
if (company) {

View File

@@ -36,9 +36,13 @@
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
@logout="handleLogout"
@company-changed="handleCompanyChanged"
@period-changed="handlePeriodChanged"
:auth-store="authStore"
@server-switched="handleServerSwitched"
/>
<!-- US-107: Filter BottomSheet for mobile -->
@@ -436,6 +440,16 @@ const handleLogout = async () => {
router.push('/login');
};
// Handle server switched event from drawer menu
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// Reload data for the new server
await companyStore.loadCompanies();
if (companyStore.selectedCompany?.id_firma) {
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma);
}
};
// US-107/US-609: Mobile TopBar actions (filter, reset, refresh, export)
const mobileTopBarActions = computed(() => [
{

View File

@@ -13,9 +13,13 @@
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
:auth-store="authStore"
@logout="handleLogout"
@company-changed="handleCompanyChanged"
@period-changed="handlePeriodChanged"
@server-switched="handleServerSwitched"
/>
<!-- US-608: Mobile Tabs (sticky below MobileTopBar) - like DetailedInvoicesView -->
@@ -166,6 +170,16 @@ const handleLogout = async () => {
router.push('/login')
}
// Handle server switch completed - reload data for new server
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// Reload data for the new server
await companyStore.loadCompanies()
if (companyStore.selectedCompany?.id_firma) {
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma)
}
}
const handleCompanyChanged = (company) => {
// Company store watcher handles the refresh
console.log('Company changed:', company?.id_firma)

View File

@@ -11,7 +11,11 @@
<MobileDrawerMenu
v-model="showDrawer"
:user="authStore.user"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
:auth-store="authStore"
@logout="handleLogout"
@server-switched="handleServerSwitched"
/>
<main class="main-content" :class="{ 'mobile-layout': isMobile }">
@@ -111,6 +115,12 @@ const handleLogout = async () => {
router.push('/login')
}
// Handle server switch completed
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// SettingsHubView doesn't need to reload data - it's just a navigation hub
}
// US-307: Removed custom mobileNavItems - using MobileBottomNav defaults
// Lifecycle

View File

@@ -19,7 +19,11 @@
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
:available-servers="authStore.availableServers"
:current-server-id="authStore.selectedServerId"
:auth-store="authStore"
@logout="handleLogout"
@server-switched="handleServerSwitched"
/>
<!-- US-108: Filter BottomSheet for mobile -->
@@ -403,6 +407,16 @@ const handleLogout = async () => {
router.push('/login');
};
// Handle server switch completed - reload data for new server
const handleServerSwitched = async (newServerId) => {
// Server switch already completed in MobileDrawerMenu modal
// Reload data for the new server
await companyStore.loadCompanies();
if (companyStore.selectedCompany?.id_firma) {
await periodStore.loadPeriods(companyStore.selectedCompany.id_firma);
}
};
// US-501: Mobile TopBar actions (filter, reset, export dropdown handled via menu)
const mobileTopBarActions = computed(() => [
{