Source: RouterAddons/visibilityProxy.js

/**
 * Addon Visibility Proxy — leitet visibilityCheck-Pfade an den Bot-Host weiter
 *
 * Die Host-App ruft `apiSelect(a.visibilityCheck.replace('{UID}', UID))` gegen
 * den Members-Host auf (siehe members-app src/show/group/Group.jsx). Der
 * visibilityCheck ist ein relativer Pfad wie `/project-bot/groups/{UID}/visibility`.
 *
 * Dieses Modul stellt einen generischen Proxy bereit: Für jede Anfrage wird aus
 * den registrierten Addons (Runtime-Config-Cache) der passende Bot-Host
 * ermittelt — abgeleitet aus der `url` des Addons (`https://{botHost}/addons/...`).
 * Die Anfrage wird unverändert (Headers, Query, Body) an den Bot weitergeleitet.
 *
 * Der Proxy ist bewusst generisch gehalten, damit jeder Bot mit `visibilityCheck`
 * funktioniert, ohne dass der Members-Host Bot-spezifische Routen kennt.
 *
 * @module RouterAddons/visibilityProxy
 */

import { Router } from 'express'
import { configs } from '../utils/compileTemplates.js'
import { errorLogger } from '../utils/requestLogger.js'

const router = Router()

/**
 * Sammelt aus dem Config-Cache alle Addons mit `visibilityCheck` und leitet
 * daraus das Präfix → Bot-Basis-URL Mapping ab.
 *
 * @returns {Map<string, string>} Map: Pfad-Präfix (z.B. `/project-bot`) → Basis-URL (z.B. `https://project-bot.dev.commtool.org`)
 */
const buildPrefixMap = () => {
    const prefixMap = new Map()
    for (const app of Object.keys(configs)) {
        const appConfigs = configs[app]
        if (!appConfigs) continue
        for (const orga of Object.keys(appConfigs)) {
            const addons = appConfigs[orga]?.addons
            if (!Array.isArray(addons)) continue
            for (const addon of addons) {
                if (!addon?.visibilityCheck || !addon?.url) continue
                const prefix = visibilityPrefix(addon.visibilityCheck)
                if (!prefix) continue
                try {
                    const base = new URL(addon.url).origin
                    // Erstes Mapping gewinnt — bei mehreren Addons mit gleichem
                    // Präfix verhält sich der Proxy deterministisch.
                    if (!prefixMap.has(prefix)) {
                        prefixMap.set(prefix, base)
                    }
                } catch {
                    // Ungültige url ignorieren
                }
            }
        }
    }
    return prefixMap
}

/**
 * Extrahiert das statische Präfix aus einem visibilityCheck-Pfad.
 * `/project-bot/groups/{UID}/visibility` → `/project-bot`
 *
 * @param {string} visibilityCheck
 * @returns {string|null} Präfix oder `null` wenn keines ableitbar
 */
const visibilityPrefix = (visibilityCheck) => {
    const match = visibilityCheck.match(/^(\/[^/{}]+)/)
    return match ? match[1] : null
}

/**
 * Proxy-Middleware: leitet Anfragen, deren Pfad mit einem visibilityCheck-Präfix
 * beginnt, an den zugehörigen Bot-Host weiter. Für alle anderen Pfade ruft sie
 * `next()` auf, damit bestehende Routen nicht blockiert werden.
 *
 * @type {import('express').RequestHandler}
 */
const proxyHandler = async (req, res, next) => {
    const prefixMap = buildPrefixMap()
    let targetBase = null
    for (const [prefix, base] of prefixMap) {
        if (req.path === prefix || req.path.startsWith(prefix + '/')) {
            targetBase = base
            break
        }
    }

    if (!targetBase) {
        return next()
    }

    // 1:1-Weiterleitung: Der vollständige Originalpfad (inkl. Präfix) bleibt
    // erhalten, weil der Bot die visibilityCheck-Route unter demselben Pfad
    // bereitstellt wie im visibilityCheck-Wert definiert.
    const targetUrl = new URL(req.originalUrl, targetBase)

    try {
        const headers = new Headers()
        for (const [key, value] of Object.entries(req.headers)) {
            if (key === 'host' || key === 'connection' || key === 'content-length') continue
            if (typeof value === 'string') {
                headers.set(key, value)
            } else if (Array.isArray(value)) {
                for (const v of value) headers.append(key, v)
            }
        }

        const init = { method: req.method, headers, redirect: 'follow' }
        if (!['GET', 'HEAD'].includes(req.method)) {
            init.body = JSON.stringify(req.body ?? {})
            if (!headers.has('content-type')) {
                headers.set('content-type', 'application/json')
            }
        }

        const upstream = await fetch(targetUrl.toString(), init)
        const upstreamHeaders = new Headers()
        for (const [key, value] of upstream.headers.entries()) {
            if (['content-encoding', 'transfer-encoding', 'connection'].includes(key)) continue
            upstreamHeaders.set(key, value)
        }

        const contentType = upstream.headers.get('content-type') || ''
        if (contentType.includes('application/json')) {
            const body = await upstream.json()
            res.status(upstream.status).json(body)
        } else {
            const body = await upstream.arrayBuffer()
            res.status(upstream.status).set(Object.fromEntries(upstreamHeaders)).send(Buffer.from(body))
        }
    } catch (err) {
        errorLogger(err)
        res.status(502).json({ success: false, message: `visibilityCheck-Proxyfehler: ${err.message}` })
    }
}

// Alle Methoden abdecken — visibilityCheck ist GET, aber der Proxy ist generisch
router.all('*', proxyHandler)

export default router