From 60553bbba893919dbd8e8bb1e2b657aeda20e60c Mon Sep 17 00:00:00 2001 From: flfeders Date: Wed, 23 Sep 2026 18:21:40 +0200 Subject: [PATCH] fix(auth): defer dashboard loading until tenant selection --- frontend/components/DashboardHome.vue | 595 +++++++++++++++++++++ frontend/pages/index.client.vue | 605 +--------------------- frontend/stores/auth.ts | 8 +- frontend/tests/dashboard-session.test.cjs | 132 +++++ 4 files changed, 747 insertions(+), 593 deletions(-) create mode 100644 frontend/components/DashboardHome.vue create mode 100644 frontend/tests/dashboard-session.test.cjs diff --git a/frontend/components/DashboardHome.vue b/frontend/components/DashboardHome.vue new file mode 100644 index 00000000..1baaa508 --- /dev/null +++ b/frontend/components/DashboardHome.vue @@ -0,0 +1,595 @@ + + + + + diff --git a/frontend/pages/index.client.vue b/frontend/pages/index.client.vue index 8a54db82..f96f66b4 100644 --- a/frontend/pages/index.client.vue +++ b/frontend/pages/index.client.vue @@ -1,598 +1,27 @@ - - diff --git a/frontend/stores/auth.ts b/frontend/stores/auth.ts index 538c096f..2fb797b3 100644 --- a/frontend/stores/auth.ts +++ b/frontend/stores/auth.ts @@ -280,7 +280,6 @@ export const useAuthStore = defineStore("auth", { body: { email, password, rememberMe } }) - console.log("Token: " + token) // Tokens abhängig von „Angemeldet bleiben“ dauerhaft oder nur für diese Sitzung speichern this.setSession(token, refreshToken, rememberMe) @@ -289,7 +288,6 @@ export const useAuthStore = defineStore("auth", { // 2. User Daten laden await this.fetchMe(token) - console.log(this.user) // 3. WICHTIG: Jetzt explizit weiterleiten! // Prüfen, ob der User geladen wurde @@ -380,7 +378,6 @@ export const useAuthStore = defineStore("auth", { }) // ... (Deine Logik für tenants, sorting etc. bleibt gleich) ... - console.log(me) this.user = me.user this.permissions = me.permissions this.tenants = me.tenants @@ -394,6 +391,9 @@ export const useAuthStore = defineStore("auth", { if(this.profile?.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config) + this.activeTenant = null + this.activeTenantData = null + if(me.activeTenant > 0) { const normalizedActiveTenant = String(me.activeTenant) this.activeTenant = normalizedActiveTenant @@ -417,7 +417,6 @@ export const useAuthStore = defineStore("auth", { this.scheduleSessionTimers(tokenToUse) - console.log(this) } catch (err: any) { // WICHTIG: Hier NICHT this.logout() aufrufen, weil das navigiert! @@ -477,7 +476,6 @@ export const useAuthStore = defineStore("auth", { refreshToken: this.getStoredRefreshToken() } }) - console.log(res) const {token, refreshToken} = res this.setSession(token, refreshToken, this.sessionPersistent) diff --git a/frontend/tests/dashboard-session.test.cjs b/frontend/tests/dashboard-session.test.cjs new file mode 100644 index 00000000..5ab8be67 --- /dev/null +++ b/frontend/tests/dashboard-session.test.cjs @@ -0,0 +1,132 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const fs = require('node:fs') +const vm = require('node:vm') +const ts = require('typescript') +const vue = require('vue') +const { parse } = require('@vue/compiler-sfc') +const { compile } = require('@vue/compiler-dom') + +function mountHome(auth) { + const requests = [] + const { descriptor } = parse(fs.readFileSync(__dirname + '/../pages/index.client.vue', 'utf8')) + const context = vm.createContext({ useAuthStore: () => auth, setPageLayout() {} }) + vm.runInContext(descriptor.scriptSetup.content.replace(/^import .*$/gm, ''), context) + const remove = node => { + if (node.parent) node.parent.children.splice(node.parent.children.indexOf(node), 1) + node.parent = null + } + const renderer = vue.createRenderer({ + createElement: type => ({ type, children: [], parent: null }), + createText: text => ({ text, children: [], parent: null }), + createComment: text => ({ text, children: [], parent: null }), + setElementText: (node, text) => { node.text = text }, + setText: (node, text) => { node.text = text }, + parentNode: node => node.parent, + nextSibling: node => node.parent?.children[node.parent.children.indexOf(node) + 1] || null, + insert(node, parent, anchor) { + remove(node) + node.parent = parent + const index = anchor ? parent.children.indexOf(anchor) : -1 + if (index < 0) parent.children.push(node) + else parent.children.splice(index, 0, node) + }, + remove, patchProp() {}, + }) + const component = { + setup: () => vm.runInContext('({ auth })', context), + render: new Function('Vue', compile(descriptor.template.content, { mode: 'function', prefixIdentifiers: true }).code)(vue), + } + const app = renderer.createApp(component) + app.component('DashboardHome', { + setup() { + requests.push(auth.activeTenant) + return () => vue.h('div', 'Dashboard ' + auth.activeTenant) + }, + }) + for (const name of ['UDashboardNavbar', 'UDashboardPanelContent', 'UCard', 'UButton', 'TenantDropdown']) { + app.component(name, { setup: (_, { slots }) => () => vue.h('div', slots.default?.()) }) + } + const root = { children: [] } + app.mount(root) + const content = node => [node.text || '', ...node.children.map(content)].join(' ') + return { requests, close: () => app.unmount(), text: () => content(root) } +} + +test('dashboard data only mounts with a tenant and remounts when the tenant changes', async t => { + const auth = vue.reactive({ activeTenant: null, tenants: [{ id: '1' }, { id: '2' }], user: {} }) + const ui = mountHome(auth) + t.after(ui.close) + assert.deepEqual(ui.requests, []) + assert.match(ui.text(), /Mandant auswählen/) + auth.activeTenant = '1' + await vue.nextTick() + assert.deepEqual(ui.requests, ['1']) + auth.activeTenant = '2' + await vue.nextTick() + assert.deepEqual(ui.requests, ['1', '2']) + auth.activeTenant = null + await vue.nextTick() + assert.deepEqual(ui.requests, ['1', '2']) + assert.match(ui.text(), /Mandant auswählen/) +}) + +test('an account without assigned tenants receives an explanation without loading widgets', t => { + const ui = mountHome(vue.reactive({ activeTenant: null, tenants: [], user: { is_admin: true } })) + t.after(ui.close) + assert.deepEqual(ui.requests, []) + assert.match(ui.text(), /noch kein Mandant zugeordnet/) + assert.match(ui.text(), /Mandanten verwalten/) +}) + +function authFixture() { + const logs = [], calls = [], sessions = [] + const token = 'test-access-token' + const refreshToken = 'test-refresh-token' + let me = { user: { email: 'test@example.com' }, activeTenant: null, tenants: [], permissions: [], profile: null } + const context = vm.createContext({ + exports: {}, process: { client: false }, + defineStore: (_, definition) => definition, + console: { log: (...args) => logs.push(args), error: (...args) => logs.push(args) }, + useTempStore: () => ({ setStoredTempConfig() {} }), + navigateTo() {}, + useNuxtApp: () => ({ $api: async (url, options) => { + calls.push({ url, options }) + if (url === '/api/me') return me + if (url === '/auth/login' || url === '/api/tenant/switch') return { token, refreshToken } + throw Error('Unexpected API request: ' + url) + } }), + }) + const source = fs.readFileSync(__dirname + '/../stores/auth.ts', 'utf8').replace(/^import .*$/gm, '') + vm.runInContext(ts.transpile(source, { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }), context) + const definition = context.exports.useAuthStore + const store = definition.state() + for (const [name, action] of Object.entries(definition.actions)) store[name] = action.bind(store) + store.getStoredToken = () => token + store.getStoredRefreshToken = () => refreshToken + store.scheduleSessionTimers = () => {} + store.setSession = (...args) => sessions.push(args) + return { store, logs, calls, sessions, token, refreshToken, setMe: value => { me = value } } +} + +test('refreshing a session without an active tenant clears the previous tenant state', async () => { + const f = authFixture() + f.store.activeTenant = '12' + f.store.activeTenantData = { id: '12', name: 'Old tenant' } + await f.store.fetchMe() + assert.equal(f.store.activeTenant, null) + assert.equal(f.store.activeTenantData, null) + assert.deepEqual(f.calls.map(call => call.url), ['/api/me']) +}) + +test('login and tenant switching store credentials without writing them to the console', async () => { + const f = authFixture() + await f.store.login('test@example.com', 'test-password') + await f.store.switchTenant('1') + assert.equal(f.sessions.length, 2) + assert.equal(f.sessions[0][0], f.token) + assert.equal(f.sessions[1][1], f.refreshToken) + const output = JSON.stringify(f.logs) + assert.ok(!output.includes(f.token)) + assert.ok(!output.includes(f.refreshToken)) +})