Source: http-server.js

/**
 * @module http-server
 * @description Express.js HTTP server configuration and setup
 *
 * This module creates and configures the main Express application with:
 * - CORS configuration for allowed domains
 * - Authentication middleware setup
 * - API route mounting for different modules
 * - Static file serving
 * - Error handling middleware
 * - Compression and security headers
 *
 * The server handles REST API requests for the Commtool Members system
 * including organization management, user authentication, and data operations.
 */

// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from './types.js'
 */
import express from 'express';
import cors from 'cors';
import compression from 'compression';
import {checkRoot, setOrgaRoot} from './utils/organizationUtils.js'
import {readLogger, errorLoggerRead} from './utils/requestLogger.js'
import { loadOrganizationDomains, createCorsOriginChecker } from '@commtool/shared-auth';



// Keep merged Secrets, if they are needed elsewhere
export let mergedSecrets



export async function createApp() {
    console.log('[http-server] createApp CALLED');
    const app = express();

    app.set('trust proxy', 1); // trust first proxy

    app.use(express.urlencoded({ extended: true, limit: '200mb' }));
    app.use(express.json({ limit: '200mb' }));

    // Load organization domains from Vault for CORS
    const appBaseDomain = process.env.APP_BASE || 'commtool.org';
    const domainOrgMap = await loadOrganizationDomains(appBaseDomain);
    
    // Legacy domains to support
    const legacyDomains = ['adbmv.de', 'kpe.de'];
    
    var corsOptions = {
        origin: createCorsOriginChecker({
            domainOrgMap,
            additionalDomains: [appBaseDomain],
            builtInDomains: legacyDomains
        }),
        methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
        allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin', 'Cache-Control', 'X-Organization', 'credentials'],
        exposedHeaders: ['X-Organization'],
        credentials: true,
    };
    
    // @ts-ignore
    app.use(cors(corsOptions));
    app.use(/** @type {any} */ (compression()));
    
    // Dynamic import to ensure it loads after secrets are available
    const {configureAuth, makeAuthCheck, createAuthRouter, authStatusEndpoint, registerRedirectUri} = await import('@commtool/shared-auth');
    
    // Configure authentication AFTER CORS is set up
    const authConfig = await configureAuth(app, {
        secretsPath: process.env.LOGIN_SECRET_PATH, 
        appSecretsPath: process.env.GIT_SECRET_PATH
    });
    mergedSecrets = authConfig.appVaultSecrets;
    console.log(mergedSecrets)
    
    // Register redirect URI for OIDC login
    const appUrl = process.env.APP_URL || mergedSecrets.appUrl;
    if (appUrl) {
        try {
            await registerRedirectUri(appUrl, authConfig);
            console.log(`[http-server] ✅ Redirect URI registered for: ${appUrl}`);
        } catch (error) {
            console.warn(`[http-server] ⚠️  Failed to register redirect URI:`, error.message);
        }
    }
    const {default: member} = await import('./Router/extraApi.js');
    const {default: event} = await import('./RouterEvents/eventApi.js');
    const {default: location} = await import('./RouterLocation/locationApi.js');
    const {filesPublic} = await import('./Router/files.js');
    const {default: docu} = await import('./combineAPIDocs.js');
    const {default: configFiles} = await import('./Router/apiConfigFile.js');
 
    const authCheck = makeAuthCheck({
        user: ['db-user', 'db-admin'],  // User braucht eine dieser Org-Rollen
        bot: [],                        // Bot braucht keine Org-Rollen, nur app-bott gruppe
        employee: []                     // Employee braucht keine Org-Rollen
    })
   
    // Define routes AFTER auth middleware is set up
    // authCheckBot runs first to handle Bearer tokens, then authCheck for regular auth
    
    /**
     * @route /api/kpe20/*
     * @group KPE20 API
     * @description legacy KPE20 organization and member management API endpoints
     * Requires authentication with 'bot' or 'user' role
     */
    // @ts-ignore
    app.use("/api/kpe20/",  authCheck, member);

    /**
     * @route /api/member/*
     * @group Member API
     * @description new member organization and member management API endpoints
     * Requires authentication with 'bot' or 'user' role
     */
    // @ts-ignore
    app.use("/api/member/",  authCheck, member);
    /**
     * @route /api/event/*
     * @group Event API
     * @description Event management and scheduling API endpoints
     * Requires authentication with 'bot' or 'user' role
     */
    // @ts-ignore
    app.use("/api/event/",  authCheck, event);

    /**
     * @route /api/location/*
     * @group Location API
     * @description Location and venue management API endpoints
     * Requires authentication with 'bot' or 'user' role
     */
    // @ts-ignore
    app.use("/api/location/", authCheck, location);
    
    /**
     * @route /api/public/*
     * @group Public Files API
     * @description Public file serving endpoints (no authentication required)
     */
    // @ts-ignore
    app.use("/api/public", filesPublic);

    /**
     * @route /api/docu/*
     * @group Documentation API
     * @description API documentation and OpenAPI specification endpoints
     */
    // @ts-ignore
    app.use("/api/docu", docu);

    /**
     * @route /api/configfiles/*
     * @group Configuration Files API
     * @description Configuration file management endpoints (upload, download, list, delete)
     * Requires admin authentication
     */
    // @ts-ignore
    app.use("/api/configfiles", configFiles);
    /**
     * @route GET /api/auth/status
     * @group Authentication API
     * @description Get current authentication status (no authentication required)
     * @returns {object} Authentication status information
     */
    // @ts-ignore
    app.get("/api/auth/status", authStatusEndpoint);


    /**
     * @route GET /api/auth/user
     * @group Authentication API
     * @description Get current user information
     * Requires authentication with 'bot' or 'user' role
     * @returns {object} User information and permissions
     */
    // @ts-ignore
    app.get("/api/auth/user", authCheck, authStatusEndpoint);


    // get the config of the current organisation
    /**
     * @route GET /api/orga/config/:app
     * @group Organization API
     * @description Get the organization configuration for the current session
     * This retrieves the organizational configuration based on the session root
     * @param {string} app - Application identifier (must match configured apps)
     * 
     * @example
     * GET /api/orga/config/member
     * Response: {"success": true, "result": {...organization config...}}
     * @returns {Object} Response object
     * @property {boolean} .success - Whether the config was successfully retrieved
     * @property {object} .result - Organization configuration data
     */
    // @ts-ignore
    app.get(`/api/orga/config/:app`,authCheck,checkRoot,readLogger,async (req, res, next)=>
    {
        try
        {   
            const authReq = /** @type {ExpressRequestAuthorized} */ (/** @type {unknown} */ (req));
            const {user,config}=await setOrgaRoot(authReq.session.root,authReq)
            // Always wait for session to save before responding
            

            if(config)
                return  res.json({success:true,result:config})
            else
                return res.status(400).json({success:false,message:'no valid orga root found'})     
        
            if(user !== false)
                res.status(200).json({success:true,result:config})
            else
                res.status(400).json({success:false,message:'user not authorized'})
        }
        catch(e)
        {
            errorLoggerRead(e)
        }
    })
    /**
     * @route /auth/*
     * @group Authentication Routes
     * @description User authentication routes (login, logout, callback)
     */
    // @ts-ignore
    app.use("/auth", createAuthRouter({
        redirectAfterLogout: '/',
        appSecrets: mergedSecrets
    }));
    /**
     * @route GET /
     * @group Root Route
     * @description Welcome message at the root endpoint
     * @returns {object} Welcome message
     * can be used to check if the API is reachable (Health Check)
     */
    // @ts-ignore
    app.get("/", (req, res) => {
        res.json({ message: "Welcome Commtool Members Api" });
    });

    /**
     * @route GET /api/branding
     * @group Branding API
     * @description Get branding configuration for the current organization
     * @param {string} orgUID - Organization identifier
     * @returns {object} Branding configuration including favicon URL and app title
     */
    // @ts-ignore
    app.get("/api/branding", async (req, res) => {
        try {
            const { orgUID } = req.query;
            
            if (!orgUID) {
                return res.status(400).json({ error: 'orgUID parameter is required' });
            }

            // TODO: Load organization branding config from database/S3
            // const orgBranding = await loadOrgConfig(orgUID);
            // const { faviconUrl, appTitle } = orgBranding;
            
            const faviconUrl = null; // Placeholder
            const appTitle = null; // Placeholder

            if (!faviconUrl && !appTitle) {
                return res.status(404).json({ error: 'No branding configured for this organization' });
            }

            // Set cache headers for better performance
            res.set({
                'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
                'Content-Type': 'application/json'
            });

            res.json({
                faviconUrl: faviconUrl || '/default-favicon.svg',
                appTitle: appTitle || 'App'
            });

        } catch (error) {
            console.error('Error loading branding:', error);
            res.status(500).json({ error: 'Internal server error' });
        }
    });

    app.use((err, req, res, next) => {
        console.error(err.stack);
        res.status(500).send('Something broke!');
    });

    // Start crons (if they don't depend on the app being listened to yet)
    // crons();
    // forEveryOrga(triggerQueue);

    console.log('[http-server] createApp RETURNING configured app');
    return app;
}