diff --git a/src/lib/openInNewTab.js b/src/lib/openInNewTab.js new file mode 100644 index 0000000..b922ce6 --- /dev/null +++ b/src/lib/openInNewTab.js @@ -0,0 +1,16 @@ +// Einmal-Links des Kundenportals (Kundenansicht, Preview) in einem neuen Tab oeffnen. Der Tab geht synchron im Klick +// auf (sonst blockiert der Browser das Pop-up), die Adresse kommt erst nach der Anfrage ans Portal. Rein (ohne React), +// damit unter Node testbar. + +/** getUrl: () => Promise. Fehler schliessen den leeren Tab wieder und werden weitergereicht. */ +export async function openInNewTab(getUrl, { win = globalThis.window } = {}) { + const tab = win.open('', '_blank') + if (!tab) throw new Error('Neuer Tab wurde vom Browser blockiert.') + tab.opener = null + try { + tab.location.href = await getUrl() + } catch (err) { + tab.close() + throw err + } +} diff --git a/src/lib/teamApi.js b/src/lib/teamApi.js new file mode 100644 index 0000000..c34a9ed --- /dev/null +++ b/src/lib/teamApi.js @@ -0,0 +1,31 @@ +import { account } from './appwrite' +import { createFinanceRequest } from './financeRequest' + +/** + * Client fuer den Team-Eingang des Kundenportals (/api/team): alle Mitarbeiter, Appwrite-JWT (Bearer). + * Anfrage-Kern (JWT-Cache, Wiederholung bei 401, deutsche Fehlertexte) wie bei den Finanzen: financeRequest.js. + */ +const PROJECT_ADMIN_URL = + import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com' + +const request = createFinanceRequest({ baseUrl: `${PROJECT_ADMIN_URL}/api/team`, createJWT: () => account.createJWT() }) + +const enc = encodeURIComponent + +/** { hasAccess, lastLoginAt } */ +export const getCustomerPortalStatus = (customerId) => request(`/customers/${enc(customerId)}/portal`) + +/** { entries: [{ id, at, staffName, action, detail }] }, neueste zuerst */ +export const getCustomerViewLog = (customerId) => request(`/customers/${enc(customerId)}/protokoll`) + +/** Einmal-Link (60 s) in die Kundenansicht des Portals */ +export async function createKundenansichtLink(customerId) { + const { openUrl } = await request(`/customers/${enc(customerId)}/kundenansicht`, { method: 'POST', body: {} }) + return openUrl +} + +/** Einmal-Link fuer die Preview eines Projekts */ +export async function createPreviewLink({ subdomain, previewUrl }) { + const { openUrl } = await request('/preview-link', { method: 'POST', body: { subdomain, previewUrl } }) + return openUrl +} diff --git a/tests/unit/openInNewTab.test.mjs b/tests/unit/openInNewTab.test.mjs new file mode 100644 index 0000000..525ab55 --- /dev/null +++ b/tests/unit/openInNewTab.test.mjs @@ -0,0 +1,52 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +// Neuer Tab fuer Einmal-Links (Kundenansicht, Preview): der Tab muss im Klick synchron aufgehen, sonst blockiert +// der Browser das Pop-up; die Adresse kommt erst nach der Anfrage ans Portal. +const { openInNewTab } = await loadModule('src/lib/openInNewTab.js') + +function fakeWindow() { + const order = [] + const tab = { location: {}, opener: 'ticketsystem', close: () => order.push('close') } + const win = { + open: (...args) => { + order.push('open') + win.openArgs = args + return tab + }, + } + return { win, tab, order } +} + +test('open laeuft vor getUrl, danach wird die Adresse gesetzt', async () => { + const { win, tab, order } = fakeWindow() + await openInNewTab(async () => { + order.push('getUrl') + return 'https://project.webklar.com/api/auth/kundenansicht?token=abc' + }, { win }) + assert.deepEqual(order, ['open', 'getUrl']) + assert.deepEqual(win.openArgs, ['', '_blank']) + assert.equal(tab.location.href, 'https://project.webklar.com/api/auth/kundenansicht?token=abc') + assert.equal(tab.opener, null) +}) + +test('Fehler beim Holen -> Tab wird geschlossen, Fehler weitergereicht', async () => { + const { win, tab, order } = fakeWindow() + await assert.rejects( + () => openInNewTab(async () => { throw new Error('Kein Portal-Zugang') }, { win }), + /Kein Portal-Zugang/ + ) + assert.deepEqual(order, ['open', 'close']) + assert.equal(tab.location.href, undefined) +}) + +test('Pop-up blockiert -> Fehlermeldung, getUrl nicht aufgerufen', async () => { + let called = false + const win = { open: () => null } + await assert.rejects( + () => openInNewTab(async () => { called = true; return 'x' }, { win }), + { message: 'Neuer Tab wurde vom Browser blockiert.' } + ) + assert.equal(called, false) +})