Portal: Mitarbeiter-Login gesperrt (Hinweis aufs Ticketsystem), alte Mitarbeiter-Sitzungen enden

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-02 03:44:10 +00:00
1 parent d0ca3c41cd
commit 3a4fa581c0
13 files changed
+145 -559

No files matched your search

+61 -73
View File
@@ -4,12 +4,13 @@ import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
// Portal-Login mit E-Mail-Code fuer Mitarbeiter mit Appwrite-2FA; Kunden und Mitarbeiter ohne 2FA wie bisher
// Portal-Login: Kunden wie bisher; Mitarbeiter (mit oder ohne 2FA) arbeiten im Ticketsystem (Etappe 4)
const aw = await startAppwriteMock()
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, post, rawGet } = await import('./support/testApp.js')
const { startTestApp, post, rawGet, sessionCookie } = await import('./support/testApp.js')
const { viewAsCookie } = await import('./support/fixtures.js')
const { default: authRoutes } = await import('../server/routes/auth.js')
const app = await startTestApp({ '/api/auth': authRoutes })
after(() => Promise.all([app.close(), aw.close(), st.close()]))
@@ -17,98 +18,85 @@ after(() => Promise.all([app.close(), aw.close(), st.close()]))
aw.addUser({ $id: 'u_kunde', email: 'kunde@example.invalid', password: 'kunde-kunde' })
aw.addUser({ $id: 'u_ohne', email: 'justin@webklar.com', password: 'ohne-ohne-ohne' })
aw.addUser({ $id: 'u_mfa', email: 'kenso@webklar.com', password: 'mit-mit-mit', mfa: true, emailVerification: true, labels: ['admin'] })
aw.addUser({ $id: 'u_label', email: 'label@webklar.com', password: 'label-label', labels: ['admin'] })
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u_kunde', email: 'kunde@example.invalid', name: 'Kunde', customerStatus: 'active', portalAccessEnabled: true })
aw.seed('customerPortalAccess', { $id: 'pa1', customerId: 'c1', enabled: true })
aw.seed('employees', { $id: 'e1', userId: 'u_ohne', displayName: 'Justin', email: 'justin@webklar.com' })
aw.seed('employees', { $id: 'e2', userId: 'u_mfa', displayName: 'Kenso', email: 'kenso@webklar.com' })
const SESSION = config.cookieName
const STAFF_BLOCKED = { error: 'Mitarbeiter arbeiten im Ticketsystem.', ticketUrl: 'https://ticket.webklar.com' }
const setCookies = (res) => res.headers.getSetCookie()
const cookieNamed = (res, name) => setCookies(res).find((c) => c.startsWith(`${name}=`)) || null
const jar = (res) => setCookies(res).map((c) => c.split(';')[0]).join('; ')
const cleared = (c) => /Expires=Thu, 01 Jan 1970/.test(c)
const liveCookies = (res, name) => setCookies(res).filter((c) => c.startsWith(`${name}=`) && !cleared(c))
const clears = (res, name) => setCookies(res).some((c) => c.startsWith(`${name}=`) && cleared(c))
const login = (email, password) => post('/api/auth/login', { email, password })
test('Kunde ohne 2FA: Anmeldung wie bisher', async () => {
test('Kunde meldet sich weiter normal an', async () => {
const before = aw.sessionDeletes()
const res = await login('kunde@example.invalid', 'kunde-kunde')
assert.equal(res.status, 200)
assert.equal(res.body.mfaRequired, undefined)
assert.ok(cookieNamed(res, SESSION))
assert.equal(res.body.role, 'customer')
assert.equal(liveCookies(res, SESSION).length, 1)
assert.equal(aw.sessionDeletes(), before)
})
test('Mitarbeiter ohne 2FA: Admin-Anmeldung wie bisher', async () => {
test('Mitarbeiter ohne 2FA: 403 mit Link ins Ticketsystem, keine Sitzung, Appwrite-Sitzung beendet', async () => {
const before = aw.sessionDeletes()
const res = await login('justin@webklar.com', 'ohne-ohne-ohne')
assert.equal(res.status, 200)
assert.equal(res.body.role, 'admin')
assert.ok(cookieNamed(res, SESSION))
})
test('Mitarbeiter mit 2FA: vor dem Code keine Portal-Sitzung', async () => {
const res = await login('kenso@webklar.com', 'mit-mit-mit')
assert.equal(res.status, 200)
assert.deepEqual(res.body, { mfaRequired: true, email: 'k…@webklar.com' })
assert.ok(cookieNamed(res, 'webklar_portal_mfa'))
assert.equal(cookieNamed(res, SESSION), null)
assert.equal(setCookies(res).length, 1)
const me = await rawGet('/api/auth/me', { cookie: jar(res) })
assert.equal(me.body.authenticated, false)
})
test('Richtiger Code: Admin-Sitzung, Code-Cookie geloescht', async () => {
const first = await login('kenso@webklar.com', 'mit-mit-mit')
const res = await post('/api/auth/login/mfa', { otp: aw.lastOtp('u_mfa') }, { cookie: jar(first) })
assert.equal(res.status, 200)
assert.equal(res.body.role, 'admin')
assert.equal(res.body.admin.isAdmin, true)
assert.ok(cookieNamed(res, SESSION))
assert.match(cookieNamed(res, 'webklar_portal_mfa'), /Expires=Thu, 01 Jan 1970/)
const me = await rawGet('/api/auth/me', { cookie: jar(res) })
assert.equal(me.body.role, 'admin')
})
test('Falscher Code: Hinweis, nach 5 Fehlversuchen neu anmelden', async () => {
const first = await login('kenso@webklar.com', 'mit-mit-mit')
for (let i = 0; i < 5; i++) {
const res = await post('/api/auth/login/mfa', { otp: '000000' }, { cookie: jar(first) })
assert.equal(res.status, 401)
assert.equal(res.body.error, 'Code falsch oder abgelaufen.')
assert.equal(cookieNamed(res, SESSION), null)
}
const res = await post('/api/auth/login/mfa', { otp: aw.lastOtp('u_mfa') }, { cookie: jar(first) })
assert.equal(res.status, 410)
assert.equal(res.body.error, 'Bitte melde dich erneut an.')
})
test('Ohne Code-Cookie: neu anmelden', async () => {
const res = await post('/api/auth/login/mfa', { otp: '123456' })
assert.equal(res.status, 410)
const resend = await post('/api/auth/login/mfa/resend', {})
assert.equal(resend.status, 410)
})
test('Code erneut senden: zweite Mail', async () => {
const first = await login('kenso@webklar.com', 'mit-mit-mit')
const before = aw.mailsSent('u_mfa')
const res = await post('/api/auth/login/mfa/resend', {}, { cookie: jar(first) })
assert.equal(res.status, 200)
assert.equal(aw.mailsSent('u_mfa'), before + 1)
})
test('2FA an, E-Mail nicht bestaetigt: Hinweis statt Sitzung', async () => {
aw.addUser({ $id: 'u_unbest', email: 'andrej@webklar.com', password: 'unbest-unbest', mfa: true, emailVerification: false })
aw.seed('employees', { $id: 'e3', userId: 'u_unbest', displayName: 'Andrej', email: 'andrej@webklar.com' })
const res = await login('andrej@webklar.com', 'unbest-unbest')
assert.equal(res.status, 403)
assert.equal(res.body.error, '2FA ist für dein Konto noch nicht eingerichtet – bitte Kenso Bescheid geben.')
assert.equal(cookieNamed(res, SESSION), null)
assert.deepEqual(res.body, STAFF_BLOCKED)
assert.equal(liveCookies(res, SESSION).length, 0)
assert.equal(liveCookies(res, 'preview_session').length, 0)
assert.equal(aw.sessionDeletes(), before + 1)
})
test('2FA-Pruefung schlaegt nicht offen fehl, wenn der Konto-Abruf scheitert', async () => {
test('Mitarbeiter mit 2FA: dieselbe Antwort, keine Code-Mail', async () => {
const before = aw.sessionDeletes()
const res = await login('kenso@webklar.com', 'mit-mit-mit')
assert.equal(res.status, 403)
assert.deepEqual(res.body, STAFF_BLOCKED)
assert.equal(setCookies(res).filter((c) => !cleared(c)).length, 0)
assert.equal(aw.mailsSent('u_mfa'), 0)
assert.equal(aw.sessionDeletes(), before + 1)
})
test('Nur Label admin (ohne employees-Eintrag): ebenfalls gesperrt', async () => {
const res = await login('label@webklar.com', 'label-label')
assert.equal(res.status, 403)
assert.deepEqual(res.body, STAFF_BLOCKED)
assert.equal(liveCookies(res, SESSION).length, 0)
})
test('Konto-Abruf gestoert: Mitarbeiter bleibt gesperrt (employees-Treffer)', async () => {
aw.failUserLookups(true)
try {
const res = await login('kenso@webklar.com', 'mit-mit-mit')
assert.equal(cookieNamed(res, SESSION), null)
assert.equal(res.body.mfaRequired, true)
assert.equal(res.status, 403)
assert.deepEqual(res.body, STAFF_BLOCKED)
assert.equal(liveCookies(res, SESSION).length, 0)
} finally {
aw.failUserLookups(false)
}
})
test('/me mit alter Mitarbeiter-Sitzung: reason staff, Weiterleitung, Cookies geloescht', async () => {
const res = await rawGet('/api/auth/me', { cookie: sessionCookie({ role: 'admin', appwriteUserId: 'u_mfa', name: 'Kenso', email: 'kenso@webklar.com' }) })
assert.equal(res.status, 200)
assert.deepEqual(res.body, { authenticated: false, reason: 'staff', redirect: 'https://ticket.webklar.com' })
assert.ok(clears(res, SESSION))
assert.ok(clears(res, 'preview_session'))
assert.equal(liveCookies(res, 'preview_session').length, 0)
})
test('/me mit Alt-Ansicht (viewAs ohne viewId): ebenso reason staff', async () => {
const res = await rawGet('/api/auth/me', { cookie: viewAsCookie('c1') })
assert.deepEqual(res.body, { authenticated: false, reason: 'staff', redirect: 'https://ticket.webklar.com' })
assert.ok(clears(res, SESSION))
})
test('/login/mfa, /login/mfa/resend und /staff-switch gibt es nicht mehr (404)', async () => {
assert.equal((await post('/api/auth/login/mfa', { otp: '123456' })).status, 404)
assert.equal((await post('/api/auth/login/mfa/resend', {})).status, 404)
assert.equal((await rawGet('/api/auth/staff-switch?token=abc')).status, 404)
})