/**
* @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;
}