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:
1 parent
d0ca3c41cd
commit
3a4fa581c0
13 files changed
+145
-559
No files matched your search
+16
-140
@@ -10,7 +10,7 @@ import {
|
||||
updateDocument,
|
||||
Query,
|
||||
} from '../services/appwriteAdmin.js'
|
||||
import { loginWithAppwrite, getLoginCooldownRemainingSec, sessionNeedsSecondFactor } from '../services/appwriteClient.js'
|
||||
import { appwriteSessionFetch, loginWithAppwrite, getLoginCooldownRemainingSec } from '../services/appwriteClient.js'
|
||||
import {
|
||||
SESSION_REVOKED_MESSAGE,
|
||||
clearPortalSession,
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
} from '../services/kundenansicht.js'
|
||||
import { logViewAction } from '../services/customerViewLog.js'
|
||||
import { resolveStaffAccount } from '../services/staffAccount.js'
|
||||
import { MFA_COOKIE, MFA_TTL_MS, resendStaffMfa, startStaffMfa, verifyStaffMfa } from '../services/staffMfa.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -59,14 +58,7 @@ function sanitizeCustomer(customer) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backoffice-Login: Jeder Mitarbeiter aus der employees-Collection darf ins
|
||||
* Admin-Panel (/admin.html). Das Appwrite-Label "admin" ist ausschliesslich die
|
||||
* WOMS-Adminrolle (Mitarbeiterverwaltung) und KEINE Voraussetzung fuer den
|
||||
* Portalzugang - es wird nur als Flag in der Session mitgefuehrt.
|
||||
* Gibt null zurueck, wenn der Login kein Mitarbeiter ist (dann greift der
|
||||
* normale Kunden-Login).
|
||||
*/
|
||||
/** Kunden-Login: Kundenkonto mit freigeschaltetem, aktivem Portalzugang, sonst Fehler mit status 403. */
|
||||
async function validatePortalAccess(appwriteUserId, email) {
|
||||
let customer = await getCustomerByAppwriteUserId(appwriteUserId)
|
||||
if (!customer && email) {
|
||||
@@ -152,6 +144,8 @@ function safePreviewRedirectUrl(url) {
|
||||
return null
|
||||
}
|
||||
|
||||
const STAFF_LOGIN_BLOCKED = 'Mitarbeiter arbeiten im Ticketsystem.'
|
||||
|
||||
router.get('/login-status', (_req, res) => {
|
||||
const retryAfterSeconds = getLoginCooldownRemainingSec()
|
||||
res.json({
|
||||
@@ -171,30 +165,14 @@ router.post('/login', async (req, res) => {
|
||||
// Appwrite-Konto: Label "admin" und Passwortstand fuer die Kunden-Session (passwordVersion.js)
|
||||
const account = await getUserById(user.$id).catch(() => null)
|
||||
|
||||
// Ticketsystem-Mitarbeiter (employees-Collection) bekommen die Admin-Ansicht
|
||||
// Mitarbeiter (employees-Collection oder Label admin, mit oder ohne 2FA) arbeiten im Ticketsystem:
|
||||
// keine Portal-Sitzung, die eben erzeugte Appwrite-Sitzung wieder beenden (best effort)
|
||||
const staff = await resolveStaffAccount(user.$id, email.trim(), account)
|
||||
if (staff) {
|
||||
// Mitarbeiter mit Appwrite-2FA: erst der Code aus der Mail macht daraus eine Portal-Sitzung
|
||||
// mfa-Flag ODER Appwrite verlangt einen zweiten Faktor: scheitert der Konto-Abruf, entscheidet Appwrite
|
||||
if (account?.mfa === true || (await sessionNeedsSecondFactor(user.sessionCookie))) {
|
||||
const { key, maskedEmail } = await startStaffMfa({ userId: user.$id, email: staff.email, sessionCookie: user.sessionCookie }).catch((err) => {
|
||||
if (err.type === 'user_email_not_verified') {
|
||||
err.status = 403
|
||||
err.message = '2FA ist für dein Konto noch nicht eingerichtet – bitte Kenso Bescheid geben.'
|
||||
} else if (err.status === 429) {
|
||||
err.message = 'Zu viele Versuche. Bitte warte einige Minuten.'
|
||||
}
|
||||
throw err
|
||||
})
|
||||
res.cookie(MFA_COOKIE, key, { ...mfaCookieOptions(), maxAge: MFA_TTL_MS })
|
||||
return res.json({ mfaRequired: true, email: maskedEmail })
|
||||
}
|
||||
await startStaffPortalSession(res, { userId: user.$id, staff })
|
||||
return res.json({
|
||||
success: true,
|
||||
role: 'admin',
|
||||
admin: { name: staff.name, email: staff.email, isAdmin: staff.isAdmin },
|
||||
})
|
||||
await appwriteSessionFetch('/account/sessions/current', { method: 'DELETE', sessionCookie: user.sessionCookie }).catch((err) =>
|
||||
console.warn('[auth] Appwrite-Sitzung eines Mitarbeiters nicht beendet:', err.message)
|
||||
)
|
||||
return res.status(403).json({ error: STAFF_LOGIN_BLOCKED, ticketUrl: config.womsBaseUrl })
|
||||
}
|
||||
|
||||
let { customer, portalAccess } = await validatePortalAccess(user.$id, email.trim())
|
||||
@@ -351,19 +329,6 @@ router.get('/staff-preview', async (req, res) => {
|
||||
return res.redirect(previewUrl)
|
||||
})
|
||||
|
||||
// Wechsel aus dem Ticketsystem (POST /api/admin/switch-link): Einmal-Link -> Admin-Sitzung
|
||||
router.get('/staff-switch', async (req, res) => {
|
||||
const tokenData = consumeStaffPreviewToken(String(req.query.token || ''), 'switch')
|
||||
if (!tokenData) {
|
||||
return res.redirect(`/login.html?error=${encodeURIComponent('Link abgelaufen, bitte anmelden')}`)
|
||||
}
|
||||
await startStaffPortalSession(res, {
|
||||
userId: tokenData.userId,
|
||||
staff: { isAdmin: Boolean(tokenData.isAdmin), name: tokenData.name, email: tokenData.email },
|
||||
})
|
||||
return res.redirect('/admin.html')
|
||||
})
|
||||
|
||||
// Kundenansicht aus dem Ticketsystem (POST /api/team/customers/:id/kundenansicht): Einmal-Link -> Portal des Kunden
|
||||
router.get('/kundenansicht', async (req, res) => {
|
||||
const gone = (text) => res.status(410).type('text/plain; charset=utf-8').send(text)
|
||||
@@ -442,36 +407,12 @@ router.get('/me', async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Admin-Session (Ticketsystem-Mitarbeiter), optional im "Als Kunde ansehen"-Modus
|
||||
if (session.role === 'admin' && session.appwriteUserId) {
|
||||
// Sessions von vor der Mitarbeiter-Freigabe kennen das Flag nicht - die
|
||||
// konnten nur Admins bekommen, also als Admin werten.
|
||||
const isAdmin = session.isAdmin === undefined ? true : Boolean(session.isAdmin)
|
||||
let customer = null
|
||||
if (session.customerId) {
|
||||
const doc = await getDocument(config.collections.customers, session.customerId).catch(() => null)
|
||||
if (doc) customer = sanitizeCustomer(doc)
|
||||
}
|
||||
await refreshPreviewCookie(req, res, {
|
||||
userId: session.appwriteUserId,
|
||||
customerId: '',
|
||||
email: session.adminEmail || session.email || '',
|
||||
name: session.adminName || session.name || 'Admin',
|
||||
role: 'admin',
|
||||
labels: isAdmin ? ['admin'] : [],
|
||||
authSource: 'kundenbereich-admin',
|
||||
})
|
||||
return res.json({
|
||||
authenticated: true,
|
||||
role: 'admin',
|
||||
viewAs: Boolean(session.viewAs && customer),
|
||||
admin: {
|
||||
name: session.adminName || session.name || 'Admin',
|
||||
email: session.adminEmail || session.email || '',
|
||||
isAdmin,
|
||||
},
|
||||
customer,
|
||||
})
|
||||
// Mitarbeiter-Sitzung von frueher (Login im Portal oder "Als Kunde ansehen" im frueheren Admin-Panel):
|
||||
// beenden, Mitarbeiter arbeiten im Ticketsystem
|
||||
if (session.role === 'admin') {
|
||||
clearPortalSession(res)
|
||||
clearPreviewSession(res)
|
||||
return res.json({ authenticated: false, reason: 'staff', redirect: config.womsBaseUrl })
|
||||
}
|
||||
|
||||
if (!session.customerId || !session.appwriteUserId) {
|
||||
@@ -524,69 +465,4 @@ router.get('/me', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
function mfaCookieOptions() {
|
||||
return { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', signed: true }
|
||||
}
|
||||
|
||||
/** Portal-Sitzung + Preview-Cookie fuer einen Mitarbeiter (Login, Code-Bestaetigung, Wechsel aus dem Ticketsystem). */
|
||||
async function startStaffPortalSession(res, { userId, staff }) {
|
||||
setPortalSession(res, {
|
||||
role: 'admin',
|
||||
isAdmin: staff.isAdmin,
|
||||
appwriteUserId: userId,
|
||||
name: staff.name,
|
||||
email: staff.email,
|
||||
})
|
||||
const staffPreviewToken = await createPreviewSessionToken({
|
||||
userId,
|
||||
customerId: '',
|
||||
email: staff.email,
|
||||
name: staff.name,
|
||||
role: 'admin',
|
||||
labels: staff.isAdmin ? ['admin'] : [],
|
||||
authSource: 'kundenbereich-admin',
|
||||
})
|
||||
res.cookie(PREVIEW_COOKIE_NAME, staffPreviewToken, previewSessionCookieOptions())
|
||||
}
|
||||
|
||||
const MFA_GONE = 'Bitte melde dich erneut an.'
|
||||
|
||||
router.post('/login/mfa', async (req, res) => {
|
||||
const key = req.signedCookies?.[MFA_COOKIE]
|
||||
try {
|
||||
const result = await verifyStaffMfa(key, req.body?.otp)
|
||||
if (!result.ok) {
|
||||
if (result.reason === 'invalid') return res.status(401).json({ error: 'Code falsch oder abgelaufen.' })
|
||||
res.clearCookie(MFA_COOKIE, mfaCookieOptions())
|
||||
return res.status(410).json({ error: MFA_GONE })
|
||||
}
|
||||
res.clearCookie(MFA_COOKIE, mfaCookieOptions())
|
||||
const account = await getUserById(result.userId).catch(() => null)
|
||||
const staff = await resolveStaffAccount(result.userId, result.email, account)
|
||||
if (!staff) return res.status(403).json({ error: 'Kein Mitarbeiterkonto.' })
|
||||
await startStaffPortalSession(res, { userId: result.userId, staff })
|
||||
return res.json({
|
||||
success: true,
|
||||
role: 'admin',
|
||||
admin: { name: staff.name, email: staff.email, isAdmin: staff.isAdmin },
|
||||
})
|
||||
} catch (err) {
|
||||
if (err.status === 429) return res.status(429).json({ error: 'Zu viele Versuche. Bitte warte einige Minuten.' })
|
||||
return res.status(err.status || 500).json({ error: err.message || 'Anmeldung fehlgeschlagen' })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/login/mfa/resend', async (req, res) => {
|
||||
try {
|
||||
if (!(await resendStaffMfa(req.signedCookies?.[MFA_COOKIE]))) {
|
||||
res.clearCookie(MFA_COOKIE, mfaCookieOptions())
|
||||
return res.status(410).json({ error: MFA_GONE })
|
||||
}
|
||||
return res.json({ success: true })
|
||||
} catch (err) {
|
||||
if (err.status === 429) return res.status(429).json({ error: 'Zu viele Versuche. Bitte warte einige Minuten.' })
|
||||
return res.status(err.status || 500).json({ error: err.message || 'Code konnte nicht gesendet werden' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in new issue
Block a user