-
-
-
{{ definition.title }}
-
{{ definition.description }}
-
-
-
- Entfernen
-
-
- Hinzufügen
-
-
+
+
+
+
+
+
+
Mandant auswählen
+
+ Wähle zuerst einen Mandanten, um dessen Dashboard zu laden.
+
+
+
Deinem Benutzer ist noch kein Mandant zugeordnet.
+
Mandanten verwalten
-
-
+
+
-
-
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))
+})