/*
http://patorjk.com/software/taag/#p=display&f=ANSI%20Regular&t=Server

███████ ███████ ██████  ██    ██ ███████ ██████  
██      ██      ██   ██ ██    ██ ██      ██   ██ 
███████ █████   ██████  ██    ██ █████   ██████  
     ██ ██      ██   ██  ██  ██  ██      ██   ██ 
███████ ███████ ██   ██   ████   ███████ ██   ██                                           

dependencies: {
    @mattermost/client      : https://www.npmjs.com/package/@mattermost/client
    @ngrok/ngrok            : https://www.npmjs.com/package/@ngrok/ngrok
    @sentry/node            : https://www.npmjs.com/package/@sentry/node
    axios                   : https://www.npmjs.com/package/axios
    chokidar                : https://www.npmjs.com/package/chokidar
    colors                  : https://www.npmjs.com/package/colors
    compression             : https://www.npmjs.com/package/compression
    cors                    : https://www.npmjs.com/package/cors
    crypto-js               : https://www.npmjs.com/package/crypto-js
    dompurify               : https://www.npmjs.com/package/dompurify
    dotenv                  : https://www.npmjs.com/package/dotenv
    express                 : https://www.npmjs.com/package/express
    express-openid-connect  : https://www.npmjs.com/package/express-openid-connect
    he                      : https://www.npmjs.com/package/he
    helmet                  : https://www.npmjs.com/package/helmet
    httpolyglot             : https://www.npmjs.com/package/httpolyglot
    jsdom                   : https://www.npmjs.com/package/jsdom
    jsonwebtoken            : https://www.npmjs.com/package/jsonwebtoken
    js-yaml                 : https://www.npmjs.com/package/js-yaml
    nodemailer              : https://www.npmjs.com/package/nodemailer
    openai                  : https://www.npmjs.com/package/openai
    qs                      : https://www.npmjs.com/package/qs
    socket.io               : https://www.npmjs.com/package/socket.io
    swagger-ui-express      : https://www.npmjs.com/package/swagger-ui-express
    uuid                    : https://www.npmjs.com/package/uuid
}
*/

/**
 * MiroTalk P2P - Server component
 *
 * @link    GitHub: https://github.com/miroslavpejic85/mirotalk
 * @link    Official Live demo: https://p2p.mirotalk.com
 * @license For open source use: AGPLv3
 * @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
 * @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
 * @author  Miroslav Pejic - miroslav.pejic.85@gmail.com
 * @version 1.7.01
 *
 */

'use strict'; // https://www.w3schools.com/js/js_strict.asp

require('dotenv').config();

const { auth, requiresAuth } = require('express-openid-connect');
const { Server } = require('socket.io');
const httpolyglot = require('httpolyglot');
const compression = require('compression');
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const path = require('path');
const axios = require('axios');
const jwt = require('jsonwebtoken');
const app = express();
const fs = require('fs');
const checkXSS = require('./xss.js');
const ServerApi = require('./api');
const MattermostController = require('./mattermost');
const Validate = require('./validate');
const HtmlInjector = require('./htmlInjector');
const Host = require('./host');
const Logs = require('./logs');
const log = new Logs('server');

// Custom Brand and buttons
const config = safeRequire('./config');

// Email alerts and notifications
const nodemailer = require('./lib/nodemailer');

const packageJson = require('../../package.json');

// Login attempts limit
const rateLimit = require('express-rate-limit');
const maxAttempts = process.env.HOST_MAX_LOGIN_ATTEMPTS || 5;
const minBlockTime = process.env.HOST_MIN_LOGIN_BLOCK_TIME || 15; // in minutes
const loginLimiter = rateLimit({
    windowMs: minBlockTime * 60 * 1000, // 15 minutes default
    max: maxAttempts,
    message: 'Too many login attempts, please try again later.',
    keyGenerator: (req) => req.body?.username || getIP(req),
});

const port = process.env.PORT || 3000; // must be the same to client.js signalingServerPort
const host = process.env.HOST || `http://localhost:${port}`;

const authHost = new Host(); // Authenticated IP by Login

// Define paths to the SSL key and certificate files
const keyPath = path.join(__dirname, '../ssl/key.pem');
const certPath = path.join(__dirname, '../ssl/cert.pem');

// Read SSL key and certificate files securely
const options = {
    key: fs.readFileSync(keyPath, 'utf-8'),
    cert: fs.readFileSync(certPath, 'utf-8'),
};

// Server both http and https
const server = httpolyglot.createServer(options, app);

// Trust Proxy
const trustProxy = !!getEnvBoolean(process.env.TRUST_PROXY);

// Cors
const cors_origin = process.env.CORS_ORIGIN;
const cors_methods = process.env.CORS_METHODS;

let corsOrigin = '*';
let corsMethods = ['GET', 'POST'];

if (cors_origin && cors_origin !== '*') {
    try {
        corsOrigin = JSON.parse(cors_origin);
    } catch (error) {
        log.error('Error parsing CORS_ORIGIN', error.message);
    }
}

if (cors_methods && cors_methods !== '') {
    try {
        corsMethods = JSON.parse(cors_methods);
    } catch (error) {
        log.error('Error parsing CORS_METHODS', error.message);
    }
}

const corsOptions = {
    origin: corsOrigin,
    methods: corsMethods,
};

/*  
    Set maxHttpBufferSize from 1e6 (1MB) to 1e7 (10MB)
*/
const io = new Server({
    maxHttpBufferSize: 1e7,
    transports: ['websocket'],
    cors: corsOptions,
}).listen(server);

// console.log(io);

// Host protection (disabled by default)
const hostProtected = getEnvBoolean(process.env.HOST_PROTECTED);
const userAuth = getEnvBoolean(process.env.HOST_USER_AUTH);
const hostUsersString = process.env.HOST_USERS || '[{"username": "MiroTalk", "password": "P2P"}]';
const hostUsers = JSON.parse(hostUsersString);
const hostCfg = {
    protected: hostProtected,
    user_auth: userAuth,
    users: hostUsers,
    authenticated: !hostProtected,
    maxRoomParticipants: parseInt(process.env.ROOM_MAX_PARTICIPANTS) || 1000,
    showActiveRooms: getEnvBoolean(process.env.SHOW_ACTIVE_ROOMS) || false,
};

// JWT config
const jwtCfg = {
    JWT_KEY: process.env.JWT_KEY || 'mirotalk_jwt_secret',
    JWT_EXP: process.env.JWT_EXP || '1h',
};

// Room presenters
const roomPresentersString = process.env.PRESENTERS || '["MiroTalk P2P"]';
const roomPresenters = JSON.parse(roomPresentersString);

// Swagger config
const yaml = require('js-yaml');
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = yaml.load(fs.readFileSync(path.join(__dirname, '/../api/swagger.yaml'), 'utf8'));

// Api config
const { v4: uuidV4 } = require('uuid');
const apiBasePath = '/api/v1'; // api endpoint path
const api_docs = host + apiBasePath + '/docs'; // api docs
const api_key_secret = process.env.API_KEY_SECRET || 'mirotalkp2p_default_secret';
const apiDisabledString = process.env.API_DISABLED || '["token", "meetings"]';
const api_disabled = JSON.parse(apiDisabledString);

// Ngrok config
const ngrok = require('@ngrok/ngrok');
const ngrokEnabled = getEnvBoolean(process.env.NGROK_ENABLED);
const ngrokAuthToken = process.env.NGROK_AUTH_TOKEN;

// Handle WebHook
const webhook = {
    enabled: config?.webhook?.enabled || false,
    url: config?.webhook?.url || 'http://localhost:8888/webhook-endpoint',
};

// Stun (https://bloggeek.me/webrtcglossary/stun/)
// Turn (https://bloggeek.me/webrtcglossary/turn/)
const iceServers = [];
const stunServerUrl = process.env.STUN_SERVER_URL;
const turnServerUrl = process.env.TURN_SERVER_URL;
const turnServerUsername = process.env.TURN_SERVER_USERNAME;
const turnServerCredential = process.env.TURN_SERVER_CREDENTIAL;
const stunServerEnabled = getEnvBoolean(process.env.STUN_SERVER_ENABLED);
const turnServerEnabled = getEnvBoolean(process.env.TURN_SERVER_ENABLED);
// Stun is mandatory for not internal network
if (stunServerEnabled && stunServerUrl) iceServers.push({ urls: stunServerUrl });
// Turn is recommended if direct peer to peer connection is not possible
if (turnServerEnabled && turnServerUrl && turnServerUsername && turnServerCredential) {
    iceServers.push({ urls: turnServerUrl, username: turnServerUsername, credential: turnServerCredential });
}

// Test Stun and Turn connection with query params
// const testStunTurn = host + '/icetest?iceServers=' + JSON.stringify(iceServers);
const testStunTurn = host + '/icetest';

// IP Lookup
const IPLookupEnabled = getEnvBoolean(process.env.IP_LOOKUP_ENABLED);

// Survey URL
const surveyEnabled = getEnvBoolean(process.env.SURVEY_ENABLED);
const surveyURL = process.env.SURVEY_URL || 'https://www.questionpro.com/t/AUs7VZq00L';

// Redirect URL
const redirectEnabled = getEnvBoolean(process.env.REDIRECT_ENABLED);
const redirectURL = process.env.REDIRECT_URL || '/newcall';

// Sentry config
const Sentry = require('@sentry/node');
const sentryEnabled = getEnvBoolean(process.env.SENTRY_ENABLED);
const sentryDSN = process.env.SENTRY_DSN;
const sentryTracesSampleRate = parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE || '0.0');

// Slack API
const CryptoJS = require('crypto-js');
const qS = require('qs');
const slackEnabled = getEnvBoolean(process.env.SLACK_ENABLED);
const slackSigningSecret = process.env.SLACK_SIGNING_SECRET;

// Setup sentry client
if (sentryEnabled && typeof sentryDSN === 'string' && sentryDSN.trim()) {
    log.info('Sentry monitoring started...');

    Sentry.init({
        dsn: sentryDSN,
        tracesSampleRate: sentryTracesSampleRate,
    });

    const logLevels = process.env.SENTRY_LOG_LEVELS
        ? process.env.SENTRY_LOG_LEVELS.split(',').map((level) => level.trim())
        : ['error'];

    const originalConsole = {};
    logLevels.forEach((level) => {
        originalConsole[level] = console[level];
        console[level] = function (...args) {
            switch (level) {
                case 'warn':
                    Sentry.captureMessage(args.join(' '), 'warning');
                    break;
                case 'error':
                    args[0] instanceof Error
                        ? Sentry.captureException(args[0])
                        : Sentry.captureException(new Error(args.join(' ')));
                    break;
            }
            originalConsole[level].apply(console, args);
        };
    });

    // log.error('Sentry error', { foo: 'bar' });
    // log.warn('Sentry warning');
}

// OpenAI/ChatGPT
let chatGPT;
const configChatGPT = {
    enabled: getEnvBoolean(process.env.CHATGPT_ENABLED),
    basePath: process.env.CHATGPT_BASE_PATH,
    apiKey: process.env.CHATGPT_APIKEY,
    model: process.env.CHATGPT_MODEL,
    max_tokens: parseInt(process.env.CHATGPT_MAX_TOKENS),
    temperature: parseInt(process.env.CHATGPT_TEMPERATURE),
};
if (configChatGPT.enabled) {
    if (configChatGPT.apiKey) {
        const { OpenAI } = require('openai');
        const configuration = {
            basePath: configChatGPT.basePath,
            apiKey: configChatGPT.apiKey,
        };
        chatGPT = new OpenAI(configuration);
    } else {
        log.warning('ChatGPT seems enabled, but you missing the apiKey!');
    }
}

// IP Whitelist
const ipWhitelist = {
    enabled: getEnvBoolean(process.env.IP_WHITELIST_ENABLED),
    allowed: process.env.IP_WHITELIST_ALLOWED ? JSON.parse(process.env.IP_WHITELIST_ALLOWED) : [],
};

// OIDC - Open ID Connect
const OIDC = {
    enabled: process.env.OIDC_ENABLED ? getEnvBoolean(process.env.OIDC_ENABLED) : false,
    allowRoomCreationForAuthUsers: process.env.OIDC_ALLOW_ROOMS_CREATION_FOR_AUTH_USERS
        ? getEnvBoolean(process.env.OIDC_ALLOW_ROOMS_CREATION_FOR_AUTH_USERS)
        : false,
    baseUrlDynamic: process.env.OIDC_BASE_URL_DYNAMIC ? getEnvBoolean(process.env.OIDC_BASE_URL_DYNAMIC) : false,
    config: {
        issuerBaseURL: process.env.OIDC_ISSUER_BASE_URL,
        clientID: process.env.OIDC_CLIENT_ID,
        clientSecret: process.env.OIDC_CLIENT_SECRET,
        baseURL: process.env.OIDC_BASE_URL,
        secret: process.env.SESSION_SECRET,
        authorizationParams: {
            response_type: 'code',
            scope: 'openid profile email',
        },
        authRequired: process.env.OIDC_AUTH_REQUIRED ? getEnvBoolean(process.env.OIDC_AUTH_REQUIRED) : false, // Set to true if authentication is required for all routes
        auth0Logout: process.env.OIDC_AUTH_LOGOUT ? getEnvBoolean(process.env.OIDC_AUTH_LOGOUT) : true, // Set to true to enable logout with Auth0
        routes: {
            callback: '/auth/callback', // Indicating the endpoint where your application will handle the callback from the authentication provider after a user has been authenticated.
            login: false, // Dedicated route in your application for user login.
            logout: '/logout', // Indicating the endpoint where your application will handle user logout requests.
        },
    },
};

// Custom middleware function for OIDC authentication
function OIDCAuth(req, res, next) {
    if (OIDC.enabled) {
        function handleHostProtected(req) {
            if (!hostCfg.protected) return;

            const ip = authHost.getIP(req);
            hostCfg.authenticated = true;
            authHost.setAuthorizedIP(ip, true);
            // Check...
            log.debug('OIDC ------> Host protected', {
                authenticated: hostCfg.authenticated,
                authorizedIPs: authHost.getAuthorizedIPs(),
            });
        }

        if (req.oidc.isAuthenticated()) {
            log.debug('OIDC ------> User already Authenticated');
            handleHostProtected(req);
            return next();
        }

        // Apply requiresAuth() middleware conditionally
        requiresAuth()(req, res, function () {
            log.debug('OIDC ------> requiresAuth');
            // Check if user is authenticated
            if (req.oidc.isAuthenticated()) {
                log.debug('[OIDC] ------> User isAuthenticated');
                handleHostProtected(req);
                next();
            } else {
                // User is not authenticated
                res.status(401).send('Unauthorized');
            }
        });
    } else {
        next();
    }
}

// Mattermost config
const mattermostCfg = {
    enabled: getEnvBoolean(process.env.MATTERMOST_ENABLED),
    server_url: process.env.MATTERMOST_SERVER_URL,
    username: process.env.MATTERMOST_USERNAME,
    password: process.env.MATTERMOST_PASSWORD,
    token: process.env.MATTERMOST_TOKEN,
    roomTokenExpire: process.env.MATTERMOST_ROOM_TOKEN_EXPIRE,
    encryptionKey: process.env.JWT_KEY,
    security: hostCfg.protected || OIDC.enabled,
    api_disabled: api_disabled,
};

// stats configuration
const statsData = {
    enabled: process.env.STATS_ENABLED ? getEnvBoolean(process.env.STATS_ENABLED) : true,
    src: process.env.STATS_SCR || 'https://stats.mirotalk.com/script.js',
    id: process.env.STATS_ID || 'c7615aa7-ceec-464a-baba-54cb605d7261',
};

// directory
const dir = {
    public: path.join(__dirname, '../../', 'public'),
};
// html views
const views = {
    about: path.join(__dirname, '../../', 'public/views/about.html'),
    client: path.join(__dirname, '../../', 'public/views/client.html'),
    landing: path.join(__dirname, '../../', 'public/views/landing.html'),
    login: path.join(__dirname, '../../', 'public/views/login.html'),
    newCall: path.join(__dirname, '../../', 'public/views/newcall.html'),
    notFound: path.join(__dirname, '../../', 'public/views/404.html'),
    privacy: path.join(__dirname, '../../', 'public/views/privacy.html'),
    activeRooms: path.join(__dirname, '../../', 'public/views/activeRooms.html'),
    stunTurn: path.join(__dirname, '../../', 'public/views/testStunTurn.html'),
};

// Branding configuration
const brandHtmlInjection = config?.brand?.htmlInjection ?? true;

// File to cache and inject custom HTML data like OG tags and any other elements.
const filesPath = [views.landing, views.newCall, views.client, views.login, views.activeRooms];
const htmlInjector = new HtmlInjector(filesPath, config?.brand || null);

const channels = {}; // collect channels
const sockets = {}; // collect sockets
const peers = {}; // collect peers info grp by channels
const presenters = {}; // collect presenters grp by channels

app.set('trust proxy', trustProxy); // Enables trust for proxy headers (e.g., X-Forwarded-For) based on the trustProxy setting
app.use(helmet.noSniff()); // Enable content type sniffing prevention

// Use all static files from the public folder
const staticOptions = {
    setHeaders: (res, filePath) => {
        if (filePath.endsWith('.js')) {
            res.setHeader('Content-Type', 'application/javascript');
        }
        // Add other headers if needed...
    },
};

// Serve static files from root (/)
app.use(express.static(dir.public, staticOptions));

// Also serve the same files under /mattermost
app.use('/mattermost', express.static(dir.public, staticOptions));

app.use(cors(corsOptions)); // Enable CORS with options
app.use(compression()); // Compress all HTTP responses using GZip
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: false })); // Parse URL-encoded bodies
app.use(apiBasePath + '/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); // api docs

// Restrict access to specified IP
app.use((req, res, next) => {
    if (!ipWhitelist.enabled) return next();
    const clientIP = getIP(req);
    log.debug('Check IP', clientIP);
    if (ipWhitelist.allowed.includes(clientIP)) {
        next();
    } else {
        log.info('Forbidden: Access denied from this IP address', { clientIP: clientIP });
        res.status(403).json({ error: 'Forbidden', message: 'Access denied from this IP address.' });
    }
});

app.use((req, res, next) => {
    const ipAddress = getIP(req);
    log.debug('New request:', {
        ip: ipAddress,
        method: req.method,
        path: req.originalUrl,
        body: req.body,
        //headers: req.headers,
    });
    next();
});

// Mattermost
const mattermost = new MattermostController(app, mattermostCfg, htmlInjector, views.client);

// Remove trailing slashes in url handle bad requests
app.use((err, req, res, next) => {
    if (err instanceof SyntaxError || err.status === 400 || 'body' in err) {
        log.error('Request Error', {
            header: req.headers,
            body: req.body,
            error: err.message,
        });
        return res.status(400).send({ status: 404, message: err.message }); // Bad request
    }
    if (req.path.substr(-1) === '/' && req.path.length > 1) {
        let query = req.url.slice(req.path.length);
        res.redirect(301, req.path.slice(0, -1) + query);
    } else {
        next();
    }
});

// OpenID Connect - Dynamically set baseURL based on incoming host and protocol
if (OIDC.enabled) {
    const getDynamicConfig = (host, protocol) => {
        const baseURL = `${protocol}://${host}`;
        const config = OIDC.baseUrlDynamic
            ? {
                  ...OIDC.config,
                  baseURL,
              }
            : OIDC.config;
        return config;
    };

    // Apply the authentication middleware using dynamic baseURL configuration
    app.use((req, res, next) => {
        const host = req.headers.host;
        const protocol = req.protocol === 'https' ? 'https' : 'http';
        const dynamicOIDCConfig = getDynamicConfig(host, protocol);
        try {
            auth(dynamicOIDCConfig)(req, res, next);
        } catch (err) {
            log.error('OIDC Auth Middleware Error', err);
            process.exit(1);
        }
    });
}

// Route to display user information
app.get('/profile', OIDCAuth, (req, res) => {
    if (OIDC.enabled) {
        log.debug('OIDC User profile requested', req.oidc.user);
        return res.json(req.oidc.user); // Send user information as JSON
    }
    return res.json({ profile: false });
});

// Authentication Callback Route
app.get('/auth/callback', (req, res, next) => {
    next(); // Let express-openid-connect handle this route
});

// Logout Route
app.get('/logout', (req, res) => {
    if (OIDC.enabled) {
        //
        if (hostCfg.protected) {
            const ip = authHost.getIP(req);
            if (authHost.isAuthorizedIP(ip)) {
                authHost.deleteIP(ip);
            }
            hostCfg.authenticated = false;
            //
            log.debug('[OIDC] ------> Logout', {
                authenticated: hostCfg.authenticated,
                authorizedIPs: authHost.getAuthorizedIPs(),
            });
        }
        req.logout(); // Logout user
    }
    res.redirect('/'); // Redirect to the home page after logout
});

// main page
app.get('/', OIDCAuth, (req, res) => {
    if (!OIDC.enabled && hostCfg.protected) {
        hostCfg.authenticated = false;
        res.redirect('/login');
    } else {
        return htmlInjector.injectHtml(views.landing, res);
    }
});

// set new room name and join
app.get('/newcall', OIDCAuth, (req, res) => {
    if (!OIDC.enabled && hostCfg.protected) {
        hostCfg.authenticated = false;
        res.redirect('/login');
    } else {
        htmlInjector.injectHtml(views.newCall, res);
    }
});

// Get Active rooms
app.get('/activeRooms', OIDCAuth, (req, res) => {
    htmlInjector.injectHtml(views.activeRooms, res);
});

// Get stats endpoint
app.get('/stats', (req, res) => {
    //log.debug('Send stats', statsData);
    res.send(statsData);
});

// mirotalk about
app.get(['/about'], (req, res) => {
    res.sendFile(views.about);
});

// privacy policy
app.get(['/privacy'], (req, res) => {
    res.sendFile(views.privacy);
});

// test Stun and Turn connections
app.get(['/icetest'], (req, res) => {
    if (Object.keys(req.query).length > 0) {
        log.debug('Request Query', req.query);
    }
    res.sendFile(views.stunTurn);
});

// Check if room active (exists)
app.post('/isRoomActive', (req, res) => {
    const { roomId } = checkXSS(req.body);

    if (roomId && (hostCfg.protected || hostCfg.user_auth || OIDC.enabled)) {
        const roomActive = Object.prototype.hasOwnProperty.call(peers, roomId);
        if (roomActive) log.debug('isRoomActive', { roomId, roomActive });
        res.status(200).json({ message: roomActive });
    } else {
        res.status(400).json({ message: 'Unauthorized' });
    }
});

// Check if Widget room active (exists)
app.post('/isWidgetRoomActive', (req, res) => {
    const { roomId } = checkXSS(req.body);
    const roomWidgetActive =
        roomId && roomId === config?.brand?.widget?.roomId && Object.prototype.hasOwnProperty.call(peers, roomId);
    log.debug('isWidgetRoomActive', { roomId, roomWidgetActive });
    res.status(200).json({ message: roomWidgetActive });
});

// Handle Direct join room with params
app.get('/join/', async (req, res) => {
    if (Object.keys(req.query).length > 0) {
        log.debug('Request Query', req.query);
        /* 
            http://localhost:3000/join?room=test&name=mirotalk&audio=1&video=1&screen=0&chat=1&notify=0&hide=0
            https://p2p.mirotalk.com/join?room=test&name=mirotalk&audio=1&video=1&screen=0&chat=1&notify=0&hide=0
            https://mirotalk.up.railway.app/join?room=test&name=mirotalk&audio=1&video=1&screen=0&chat=1&notify=0&hide=0
        */
        const { room, name, audio, video, screen, chat, notify, hide, token } = checkXSS(req.query);

        if (!room) {
            log.warn('/join/params room empty', room);
            return res.status(401).json({ message: 'Direct Room Join: Missing mandatory room parameter!' });
        }

        if (!Validate.isValidRoomName(room)) {
            return res.status(400).json({
                message: 'Invalid Room name!\nPath traversal pattern detected!',
            });
        }

        const allowRoomAccess = isAllowedRoomAccess('/join/params', req, hostCfg, peers, room);

        if (!allowRoomAccess && !token) {
            return res.status(401).json({ message: 'Direct Room Join Unauthorized' });
        }

        let peerUsername,
            peerPassword = '';
        let isPeerValid = false;
        let isPeerPresenter = false;

        if (token) {
            try {
                // Check if valid JWT token
                const validToken = await isValidToken(token);

                // Not valid token
                if (!validToken) {
                    return res.status(401).json({ message: 'Invalid Token' });
                }

                const { username, password, presenter } = checkXSS(decodeToken(token));
                // Peer credentials
                peerUsername = username;
                peerPassword = password;
                // Check if valid peer
                isPeerValid = isAuthPeer(username, password);
                // Check if presenter
                isPeerPresenter = presenter === '1' || presenter === 'true';
            } catch (err) {
                // Invalid token
                log.error('Direct Join JWT error', err.message);
                return hostCfg.protected || hostCfg.user_auth
                    ? htmlInjector.injectHtml(views.login, res)
                    : htmlInjector.injectHtml(views.landing, res);
            }
        }

        const OIDCUserAuthenticated = OIDC.enabled && req.oidc.isAuthenticated();

        // Peer valid going to auth as host
        if ((hostCfg.protected && isPeerValid && isPeerPresenter && !hostCfg.authenticated) || OIDCUserAuthenticated) {
            const ip = getIP(req);
            hostCfg.authenticated = true;
            authHost.setAuthorizedIP(ip, true);
            log.debug('Direct Join user auth as host done', {
                ip: ip,
                username: peerUsername,
                password: peerPassword,
            });
        }

        // Check if peer authenticated or valid
        if (room && (hostCfg.authenticated || isPeerValid)) {
            // only room mandatory
            return htmlInjector.injectHtml(views.client, res);
        } else {
            return htmlInjector.injectHtml(views.login, res);
        }
    }
});

// Join Room by id
app.get('/join/:roomId', function (req, res) {
    //
    const { roomId } = req.params;

    if (!roomId) {
        log.warn('/join/:roomId empty', roomId);
        return res.redirect('/');
    }

    if (!Validate.isValidRoomName(roomId)) {
        log.warn('/join/:roomId invalid', roomId);
        return res.redirect('/');
    }

    const allowRoomAccess = isAllowedRoomAccess('/join/:roomId', req, hostCfg, peers, roomId);

    if (allowRoomAccess) {
        htmlInjector.injectHtml(views.client, res);
    } else {
        !OIDC.enabled && hostCfg.protected ? res.redirect('/login') : res.redirect('/');
    }
});

// Not specified correctly the room id
app.get('/join/\\*', function (req, res) {
    res.redirect('/');
});

// Login
app.get(['/login'], (req, res) => {
    if (hostCfg.protected || hostCfg.user_auth) {
        return htmlInjector.injectHtml(views.login, res);
    }
    res.redirect('/');
});

// Logged
app.get('/logged', (req, res) => {
    if (!OIDC.enabled && hostCfg.protected) {
        const ip = getIP(req);
        if (allowedIP(ip)) {
            res.redirect('/');
        } else {
            hostCfg.authenticated = false;
            res.redirect('/login');
        }
    } else {
        res.redirect('/');
    }
});

/* AXIOS */

// handle login on host protected
app.post('/login', loginLimiter, (req, res) => {
    //
    const ip = getIP(req);
    log.debug(`Request login to host from: ${ip}`, req.body);

    const safeBody = checkXSS(req.body) || {};
    const { username, password } = safeBody;

    if (!username || !password) {
        log.warn('Login failed: missing username or password', req.body);
        return res.status(400).json({ message: 'Missing username or password' });
    }

    const isPeerValid = isAuthPeer(username, password);

    // Peer valid going to auth as host
    if (hostCfg.protected && isPeerValid && !hostCfg.authenticated) {
        hostCfg.authenticated = true;
        authHost.setAuthorizedIP(ip, true);
        log.debug('HOST LOGIN OK', {
            ip: ip,
            authorized: authHost.isAuthorizedIP(ip),
            authorizedIps: authHost.getAuthorizedIPs(),
        });
        const token = encodeToken({ username: username, password: password, presenter: true });
        return res.status(200).json({ message: token });
    }

    // Peer auth valid
    if (isPeerValid) {
        log.debug('PEER LOGIN OK', { ip: ip, authorized: true });
        const isPresenter = roomPresenters && roomPresenters.includes(username).toString();
        const token = encodeToken({ username: username, password: password, presenter: isPresenter });
        return res.status(200).json({ message: token });
    } else {
        return res.status(401).json({ message: 'unauthorized' });
    }
});

// UI buttons configuration
app.get('/buttons', (req, res) => {
    res.status(200).json({ message: config && config.buttons ? config.buttons : false });
});

// UI brand configuration
app.get('/brand', (req, res) => {
    res.status(200).json({ message: config && config.brand && brandHtmlInjection ? config.brand : false });
});

// Join roomId redirect to /join?room=roomId
app.get('/:roomId', (req, res) => {
    const { roomId } = checkXSS(req.params);

    if (!roomId) {
        log.warn('/:roomId empty', roomId);
        return res.redirect('/');
    }

    log.debug('Detected roomId --> redirect to /join?room=roomId');
    res.redirect(`/join/${roomId}`);
});

/**
    MiroTalk API v1
    For api docs we use: https://swagger.io/
*/

// request stats list
app.get(`${apiBasePath}/stats`, (req, res) => {
    // Check if endpoint allowed
    if (api_disabled.includes('stats')) {
        return res.status(403).json({
            error: 'This endpoint has been disabled. Please contact the administrator for further information.',
        });
    }
    // check if user was authorized for the api call
    const { host, authorization } = req.headers;
    const api = new ServerApi(host, authorization, api_key_secret);
    if (!api.isAuthorized()) {
        log.debug('MiroTalk get stats - Unauthorized', {
            header: req.headers,
            body: req.body,
        });
        return res.status(403).json({ error: 'Unauthorized!' });
    }
    // Get stats
    const { timestamp, totalRooms, totalPeers } = api.getStats(peers);
    res.json({
        success: true,
        timestamp,
        totalRooms,
        totalPeers,
    });
    // log.debug the output if all done
    log.debug('MiroTalk get stats - Authorized', {
        header: req.headers,
        body: req.body,
        timestamp,
        totalRooms,
        totalPeers,
    });
});

// request token endpoint
app.post(`${apiBasePath}/token`, (req, res) => {
    // Check if endpoint allowed
    if (api_disabled.includes('token')) {
        return res.status(403).json({
            error: 'This endpoint has been disabled. Please contact the administrator for further information.',
        });
    }
    // check if user was authorized for the api call
    const { host, authorization } = req.headers;
    const api = new ServerApi(host, authorization, api_key_secret);
    if (!api.isAuthorized()) {
        log.debug('MiroTalk get token - Unauthorized', {
            header: req.headers,
            body: req.body,
        });
        return res.status(403).json({ error: 'Unauthorized!' });
    }
    // Get Token
    const token = api.getToken(req.body);
    res.json({ token: token });
    // log.debug the output if all done
    log.debug('MiroTalk get token - Authorized', {
        header: req.headers,
        body: req.body,
        token: token,
    });
});

// request meetings list
app.get(`${apiBasePath}/meetings`, (req, res) => {
    // Check if endpoint allowed
    if (api_disabled.includes('meetings')) {
        return res.status(403).json({
            error: 'This endpoint has been disabled. Please contact the administrator for further information.',
        });
    }
    // check if user was authorized for the api call
    const { host, authorization } = req.headers;
    const api = new ServerApi(host, authorization, api_key_secret);
    if (!api.isAuthorized()) {
        log.debug('MiroTalk get meetings - Unauthorized', {
            header: req.headers,
            body: req.body,
        });
        return res.status(403).json({ error: 'Unauthorized!' });
    }
    // Get meetings
    const meetings = api.getMeetings(peers);
    res.json({ meetings: meetings });
    // log.debug the output if all done
    log.debug('MiroTalk get meetings - Authorized', {
        header: req.headers,
        body: req.body,
        meetings: meetings,
    });
});

// API request meeting room endpoint
app.post(`${apiBasePath}/meeting`, (req, res) => {
    // Check if endpoint allowed
    if (api_disabled.includes('meeting')) {
        return res.status(403).json({
            error: 'This endpoint has been disabled. Please contact the administrator for further information.',
        });
    }
    const { host, authorization } = req.headers;
    const api = new ServerApi(host, authorization, api_key_secret);
    if (!api.isAuthorized()) {
        log.debug('MiroTalk get meeting - Unauthorized', {
            header: req.headers,
            body: req.body,
        });
        return res.status(403).json({ error: 'Unauthorized!' });
    }
    const meetingURL = api.getMeetingURL();
    res.json({ meeting: meetingURL });
    log.debug('MiroTalk get meeting - Authorized', {
        header: req.headers,
        body: req.body,
        meeting: meetingURL,
    });
});

// API request join room endpoint
app.post(`${apiBasePath}/join`, (req, res) => {
    // Check if endpoint allowed
    if (api_disabled.includes('join')) {
        return res.status(403).json({
            error: 'This endpoint has been disabled. Please contact the administrator for further information.',
        });
    }
    const { host, authorization } = req.headers;
    const api = new ServerApi(host, authorization, api_key_secret);
    if (!api.isAuthorized()) {
        log.debug('MiroTalk get join - Unauthorized', {
            header: req.headers,
            body: req.body,
        });
        return res.status(403).json({ error: 'Unauthorized!' });
    }
    const joinURL = api.getJoinURL(req.body);
    res.json({ join: joinURL });
    log.debug('MiroTalk get join - Authorized', {
        header: req.headers,
        body: req.body,
        join: joinURL,
    });
});

/*
    MiroTalk Slack app v1
    https://api.slack.com/authentication/verifying-requests-from-slack
*/

// Slack request meeting room endpoint
app.post('/slack', (req, res) => {
    if (!slackEnabled) return res.end('`Under maintenance` - Please check back soon.');

    // Check if endpoint allowed
    if (api_disabled.includes('slack')) {
        return res.end('`This endpoint has been disabled`. Please contact the administrator for further information.');
    }

    log.debug('Slack', req.headers);

    if (!slackSigningSecret) return res.end('`Slack Signing Secret is empty!`');

    const slackSignature = req.headers['x-slack-signature'];
    const requestBody = qS.stringify(req.body, { format: 'RFC1738' });
    const timeStamp = req.headers['x-slack-request-timestamp'];
    const time = Math.floor(new Date().getTime() / 1000);

    // The request timestamp is more than five minutes from local time. It could be a replay attack, so let's ignore it.
    if (Math.abs(time - timeStamp) > 300) return res.end('`Wrong timestamp` - Ignore this request.');

    // Get Signature to compare it later
    const sigBaseString = 'v0:' + timeStamp + ':' + requestBody;
    const mySignature = 'v0=' + CryptoJS.HmacSHA256(sigBaseString, slackSigningSecret);

    // Valid Signature return a meetingURL
    if (mySignature == slackSignature) {
        const host = req.headers.host;
        const meetingURL = getMeetingURL(host);
        log.debug('Slack', { meeting: meetingURL });
        return res.end(meetingURL);
    }
    // Something wrong
    return res.end('`Wrong signature` - Verification failed!');
});

/**
 * Request meeting room endpoint
 * @returns  entrypoint / Room URL for your meeting.
 */
function getMeetingURL(host) {
    return 'http' + (host.includes('localhost') ? '' : 's') + '://' + host + '/join/' + uuidV4();
}

// request active rooms endpoint
app.get(`${apiBasePath}/activeRooms`, (req, res) => {
    // Check if endpoint allowed
    if (!hostCfg.showActiveRooms) {
        return res.status(403).json({
            error: 'This endpoint has been disabled. Please contact the administrator for further information.',
        });
    }
    // check if user was authorized for the api call
    const { host, authorization = api_key_secret } = req.headers;
    const api = new ServerApi(host, authorization, api_key_secret);

    // Get active rooms
    const activeRooms = api.getActiveRooms(peers);
    res.json({ activeRooms: activeRooms });

    // log.debug the output if all done
    log.debug('MiroTalk get active rooms - Authorized', {
        header: req.headers,
        body: req.body,
        activeRooms: activeRooms,
    });
});

// end of MiroTalk API v1

// not match any of page before, so 404 not found
app.use((req, res) => {
    res.sendFile(views.notFound);
});

// Global error handler for URIError and other errors
app.use((err, req, res, next) => {
    if (err instanceof URIError) {
        log.warn('Malformed URI detected', {
            url: req.url,
            ip: getIP(req),
            error: err.message,
        });
        return res.status(400).send({ status: 400, message: 'Invalid URL encoding' });
    }
    // Handle other errors
    log.error('Unhandled error', {
        url: req.url,
        error: err.message,
        stack: err.stack,
    });
    res.status(500).send({ status: 500, message: 'Internal server error' });
});

/**
 * Get Server config
 * @param {string} tunnel
 * @returns server config
 */
function getServerConfig(tunnel = false) {
    return {
        // General Server Information
        server: host,
        server_tunnel: tunnel,
        trust_proxy: trustProxy,
        api_docs: api_docs,

        // Core Configurations
        jwtCfg: jwtCfg,
        cors: corsOptions,
        iceServers: iceServers,
        test_ice_servers: testStunTurn,
        email: nodemailer.emailCfg.alert ? nodemailer.emailCfg : false,

        // Security, Authorization, and User Management
        oidc: OIDC.enabled ? OIDC : false,
        host_protected: hostCfg.protected || hostCfg.user_auth ? hostCfg : false,
        presenters: roomPresenters,
        ip_whitelist: ipWhitelist.enabled ? ipWhitelist : false,
        api_key_secret: api_key_secret,

        // Media and Connection Settings
        turn_enabled: turnServerEnabled,
        ip_lookup_enabled: IPLookupEnabled,

        // Integrations
        chatGPT_enabled: configChatGPT.enabled ? configChatGPT : false,
        slack_enabled: slackEnabled,
        mattermost_enabled: mattermostCfg.enabled ? mattermostCfg : false,
        webhook: webhook.enabled ? webhook : false,

        // Monitoring and Logging
        sentry_enabled: sentryEnabled,
        stats: statsData.enabled ? statsData : false,

        // Ngrok Configuration
        ngrok: ngrokEnabled
            ? {
                  enabled: ngrokEnabled,
                  token: ngrokAuthToken,
              }
            : false,

        // URLs for Redirection and Survey
        survey: surveyEnabled ? surveyURL : false,
        redirect: redirectEnabled ? redirectURL : false,

        // Widget Configuration
        widget: config?.brand?.widget?.enabled ? config.brand.widget : false,

        // Versions and environment information
        environment: process.env.NODE_ENV || 'development',
        app_version: packageJson.version,
        node_version: process.versions.node,
    };
}

/**
 * Expose server to external with https tunnel using ngrok
 * https://ngrok.com
 */
async function ngrokStart() {
    try {
        await ngrok.authtoken(ngrokAuthToken);
        const listener = await ngrok.forward({ addr: port });
        const tunnelUrl = listener.url();
        log.info('Server config', getServerConfig(tunnelUrl));
    } catch (err) {
        log.warn('[Error] ngrokStart', err);
        await ngrok.kill();
        process.exit(1);
    }
}

/**
 * Start Local Server with ngrok https tunnel (optional)
 */
server.listen(port, null, () => {
    log.debug(
        `%c

	███████╗██╗ ██████╗ ███╗   ██╗      ███████╗███████╗██████╗ ██╗   ██╗███████╗██████╗ 
	██╔════╝██║██╔════╝ ████╗  ██║      ██╔════╝██╔════╝██╔══██╗██║   ██║██╔════╝██╔══██╗
	███████╗██║██║  ███╗██╔██╗ ██║█████╗███████╗█████╗  ██████╔╝██║   ██║█████╗  ██████╔╝
	╚════██║██║██║   ██║██║╚██╗██║╚════╝╚════██║██╔══╝  ██╔══██╗╚██╗ ██╔╝██╔══╝  ██╔══██╗
	███████║██║╚██████╔╝██║ ╚████║      ███████║███████╗██║  ██║ ╚████╔╝ ███████╗██║  ██║
	╚══════╝╚═╝ ╚═════╝ ╚═╝  ╚═══╝      ╚══════╝╚══════╝╚═╝  ╚═╝  ╚═══╝  ╚══════╝╚═╝  ╚═╝ started...

	`,
        'font-family:monospace'
    );

    // https tunnel
    if (ngrokEnabled) {
        ngrokStart();
    } else {
        log.info('Server config', getServerConfig());
    }
});

/**
 * On peer connected
 * Users will connect to the signaling server, after which they'll issue a "join"
 * to join a particular channel. The signaling server keeps track of all sockets
 * who are in a channel, and on join will send out 'addPeer' events to each pair
 * of users in a channel. When clients receive the 'addPeer' even they'll begin
 * setting up an RTCPeerConnection with one another. During this process they'll
 * need to relay ICECandidate information to one another, as well as SessionDescription
 * information. After all of that happens, they'll finally be able to complete
 * the peer connection and will be in streaming audio/video between eachother.
 */
io.sockets.on('connect', async (socket) => {
    log.debug('[' + socket.id + '] connection accepted', {
        host: socket.handshake.headers.host.split(':')[0],
        time: socket.handshake.time,
    });

    socket.channels = {};
    sockets[socket.id] = socket;

    const transport = socket.conn.transport.name; // in most cases, "polling"
    log.debug('[' + socket.id + '] Connection transport', transport);

    /**
     * Check upgrade transport
     */
    socket.conn.on('upgrade', () => {
        const upgradedTransport = socket.conn.transport.name; // in most cases, "websocket"
        log.debug('[' + socket.id + '] Connection upgraded transport', upgradedTransport);
    });

    /**
     * On peer disconnected
     */
    socket.on('disconnect', async (reason) => {
        removeIP(socket);
        for (let channel in socket.channels) {
            await removePeerFrom(channel, socket, reason);
        }
        log.debug('[' + socket.id + '] disconnected', { reason: reason });
        delete sockets[socket.id];
    });

    /**
     * Handle incoming data, res with a callback
     */
    socket.on('data', async (dataObj, cb) => {
        const data = checkXSS(dataObj);

        log.debug('Socket Promise', data);

        if (!Validate.isValidData(data)) return;

        //...
        const { room_id, peer_id, peer_name, method, params } = data;

        switch (method) {
            case 'checkPeerName':
                log.debug('Check if peer name exists', { peer_name: peer_name, room_id: room_id });
                for (let id in peers[room_id]) {
                    if (peer_id != id && peers[room_id][id]['peer_name'] == peer_name) {
                        log.debug('Peer name found', { peer_name: peer_name, room_id: room_id });
                        cb(true);
                        break;
                    }
                }
                break;
            case 'getChatGPT':
                // https://platform.openai.com/docs/introduction
                if (!configChatGPT.enabled) return cb({ message: 'ChatGPT seems disabled, try later!' });
                // https://platform.openai.com/docs/api-reference/completions/create
                try {
                    const { time, prompt, context } = params;
                    // Add the prompt to the context
                    context.push({ role: 'user', content: prompt });

                    // Prevent memory leak: Limit context size to last 20 messages (10 exchanges)
                    const MAX_CONTEXT_MESSAGES = 20;
                    if (context.length > MAX_CONTEXT_MESSAGES) {
                        // Keep the system message (if exists) and the most recent messages
                        const systemMessage = context[0]?.role === 'system' ? [context[0]] : [];
                        const recentMessages = context.slice(-MAX_CONTEXT_MESSAGES);
                        context.length = 0; // Clear array
                        context.push(...systemMessage, ...recentMessages);
                    }

                    // Call OpenAI's API to generate response
                    const completion = await chatGPT.chat.completions.create({
                        model: configChatGPT.model || 'gpt-3.5-turbo',
                        messages: context,
                        max_tokens: configChatGPT.max_tokens || 1000,
                        temperature: configChatGPT.temperature || 0,
                    });
                    // Extract message from completion
                    const message = completion.choices[0].message.content.trim();
                    // Add response to context
                    context.push({ role: 'assistant', content: message });
                    // Log conversation details
                    log.debug('ChatGPT', {
                        time: time,
                        room: room_id,
                        name: peer_name,
                        context: context,
                        contextLength: context.length,
                    });
                    // Callback response to client
                    cb({ message: message, context: context });
                } catch (error) {
                    if (error.name === 'APIError') {
                        log.error('ChatGPT', {
                            name: error.name,
                            status: error.status,
                            message: error.message,
                            code: error.code,
                            type: error.type,
                        });
                        cb({ message: error.message });
                    } else {
                        // Non-API error
                        log.error('ChatGPT', error);
                        cb({ message: error.message });
                    }
                }
                break;
            //....
            default:
                cb(false);
                break;
        }
        cb(false);
    });

    /**
     * On peer join
     */
    socket.on('join', async (cfg) => {
        // Get peer IPv4 (::1 Its the loopback address in ipv6, equal to 127.0.0.1 in ipv4)
        const peer_ip = getSocketIP(socket);

        // Get peer Geo Location
        if (IPLookupEnabled && peer_ip != '::1') {
            cfg.peer_geo = await getPeerGeoLocation(peer_ip);
        }

        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        // log.debug('Join room', config);
        log.debug('[' + socket.id + '] join ', config);

        const {
            channel,
            channel_password,
            peer_uuid,
            peer_name,
            peer_avatar,
            peer_token,
            peer_video,
            peer_audio,
            peer_video_status,
            peer_audio_status,
            peer_screen_status,
            peer_hand_status,
            peer_rec_status,
            peer_privacy_status,
            peer_info,
        } = config;

        if (!Validate.isValidRoomName(channel)) {
            log.warn('[' + socket.id + '] - Invalid room name', channel);
            return socket.emit('unauthorized');
        }

        if (channel in socket.channels) {
            return log.debug('[' + socket.id + '] [Warning] already joined', channel);
        }
        // no channel aka room in channels init
        if (!(channel in channels)) channels[channel] = {};

        // no channel aka room in peers init
        if (!(channel in peers)) peers[channel] = {};

        // no presenter aka host in presenters init
        if (!(channel in presenters)) presenters[channel] = {};

        let is_presenter = true;

        // User Auth required, we check if peer valid
        if (hostCfg.user_auth || peer_token) {
            // Check JWT
            if (peer_token) {
                try {
                    const validToken = await isValidToken(peer_token);

                    if (!validToken) {
                        // redirect peer to login page
                        return socket.emit('unauthorized');
                    }

                    const { username, password, presenter } = checkXSS(decodeToken(peer_token));

                    const isPeerValid = isAuthPeer(username, password);

                    if (!isPeerValid) {
                        // redirect peer to login page
                        return socket.emit('unauthorized');
                    }

                    // Presenter if token 'presenter' is '1'/'true' or first to join room
                    is_presenter =
                        presenter === '1' || presenter === 'true' || Object.keys(presenters[channel]).length === 0;

                    log.debug('[' + socket.id + '] JOIN ROOM - USER AUTH check peer', {
                        ip: peer_ip,
                        peer_username: username,
                        peer_password: password,
                        peer_valid: isPeerValid,
                        peer_presenter: is_presenter,
                    });
                } catch (err) {
                    // redirect peer to login page
                    log.error('[' + socket.id + '] [Warning] Join Room JWT error', err.message);
                    return socket.emit('unauthorized');
                }
            } else {
                // redirect peer to login page
                return socket.emit('unauthorized');
            }
        }

        // room locked by the participants can't join
        if (peers[channel]['lock'] === true && peers[channel]['password'] != channel_password) {
            log.debug('[' + socket.id + '] [Warning] Room Is Locked', channel);
            return socket.emit('roomIsLocked');
        }

        // Set the presenters
        const presenter = {
            peer_ip: peer_ip,
            peer_name: peer_name,
            peer_uuid: peer_uuid,
            is_presenter: is_presenter,
        };
        // first we check if the username match the presenters username
        if (roomPresenters && roomPresenters.includes(peer_name)) {
            presenters[channel][socket.id] = presenter;
        } else {
            // if not match the presenters username, the first one join room is the presenter
            if (Object.keys(presenters[channel]).length === 0) {
                presenters[channel][socket.id] = presenter;
            }
        }

        // Check if peer is presenter, if token check the presenter key
        const isPresenter = peer_token ? is_presenter : isPeerPresenter(channel, socket.id, peer_name, peer_uuid);

        // Some peer info data
        const { osName, osVersion, browserName, browserVersion, extras } = peer_info;

        // collect peers info grp by channels
        peers[channel][socket.id] = {
            peer_name: peer_name,
            peer_avatar: peer_avatar,
            peer_presenter: isPresenter,
            peer_video: peer_video,
            peer_audio: peer_audio,
            peer_video_status: peer_video_status,
            peer_audio_status: peer_audio_status,
            peer_screen_status: peer_screen_status,
            peer_hand_status: peer_hand_status,
            peer_rec_status: peer_rec_status,
            peer_privacy_status: peer_privacy_status,
            os: osName ? `${osName} ${osVersion}` : '',
            browser: browserName ? `${browserName} ${browserVersion}` : '',
            extras: extras,
        };

        const activeRooms = getActiveRooms();

        log.debug('[Join] - active rooms and peers count', activeRooms);

        log.debug('[Join] - connected presenters grp by roomId', presenters);

        log.debug('[Join] - connected peers grp by roomId', peers);

        await addPeerTo(channel);

        channels[channel][socket.id] = socket;
        socket.channels[channel] = channel;

        const peerCounts = Object.keys(peers[channel]).length;

        // Send some server info to joined peer
        await sendToPeer(socket.id, sockets, 'serverInfo', {
            peers_count: peerCounts,
            host_protected: hostCfg.protected,
            user_auth: hostCfg.user_auth,
            is_presenter: isPresenter,
            survey: {
                active: surveyEnabled,
                url: surveyURL,
            },
            redirect: {
                active: redirectEnabled,
                url: redirectURL,
            },
            maxRoomParticipants: hostCfg.maxRoomParticipants,
            //...
        });

        // SCENARIO: Notify when the first user join room and is awaiting assistance...
        if (peerCounts === 1) {
            nodemailer.sendEmailAlert('join', {
                room_id: channel,
                peer_name: peer_name,
                domain: socket.handshake.headers.host.split(':')[0],
                os: osName ? `${osName} ${osVersion}` : '',
                browser: browserName ? `${browserName} ${browserVersion}` : '',
            }); // .env EMAIL_ALERT=true
        }

        // Handle WebHook
        if (webhook.enabled) {
            // Trigger a POST request when a user joins
            config.timestamp = log.getDateTime(false);
            axios
                .post(webhook.url, { event: 'join', data: config }, { timeout: 5000 }) // 5 second timeout
                .then((response) => log.debug('Join event tracked:', response.data))
                .catch((error) => log.error('Error tracking join event:', error.message));
        }
    });

    /**
     * Relay ICE to peers
     */
    socket.on('relayICE', async (config) => {
        if (!Validate.isValidData(config)) return;
        const { peer_id, ice_candidate } = config;

        // log.debug('[' + socket.id + '] relay ICE-candidate to [' + peer_id + '] ', {
        //     address: config.ice_candidate,
        // });

        await sendToPeer(peer_id, sockets, 'iceCandidate', {
            peer_id: socket.id,
            ice_candidate: ice_candidate,
        });
    });

    /**
     * Relay SDP to peers
     */
    socket.on('relaySDP', async (config) => {
        if (!Validate.isValidData(config)) return;
        const { peer_id, session_description } = config;

        log.debug('[' + socket.id + '] relay SessionDescription to [' + peer_id + '] ', {
            type: session_description.type,
        });

        await sendToPeer(peer_id, sockets, 'sessionDescription', {
            peer_id: socket.id,
            session_description: session_description,
        });
    });

    /**
     * Handle Room action
     */
    socket.on('roomAction', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        //log.debug('[' + socket.id + '] Room action:', config);
        const { room_id, peer_id, peer_name, peer_uuid, password, action } = config;

        // Check if peer is presenter
        const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);

        let room_is_locked = false;
        //
        try {
            switch (action) {
                case 'lock':
                    if (!isPresenter) return;
                    peers[room_id]['lock'] = true;
                    peers[room_id]['password'] = password;
                    await sendToRoom(room_id, socket.id, 'roomAction', {
                        peer_name: peer_name,
                        action: action,
                    });
                    room_is_locked = true;
                    break;
                case 'unlock':
                    if (!isPresenter) return;
                    delete peers[room_id]['lock'];
                    delete peers[room_id]['password'];
                    await sendToRoom(room_id, socket.id, 'roomAction', {
                        peer_name: peer_name,
                        action: action,
                    });
                    break;
                case 'checkPassword':
                    const data = {
                        peer_name: peer_name,
                        action: action,
                        password: password == peers[room_id]['password'] ? 'OK' : 'KO',
                    };
                    await sendToPeer(socket.id, sockets, 'roomAction', data);
                    break;
                default:
                    break;
            }
        } catch (err) {
            log.error('Room action', toJson(err));
        }
        log.debug('[' + socket.id + '] Room ' + room_id, { locked: room_is_locked, password: password });
    });

    /**
     * Relay NAME to peers
     */
    socket.on('peerName', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        // log.debug('Peer name', config);
        const { room_id, peer_name_old, peer_name_new, peer_avatar } = config;

        let peer_id_to_update = null;

        for (let peer_id in peers[room_id]) {
            if (peers[room_id][peer_id]['peer_name'] == peer_name_old && peer_id == socket.id) {
                peers[room_id][peer_id]['peer_name'] = peer_name_new;
                // presenter
                if (presenters && presenters[room_id] && presenters[room_id][peer_id]) {
                    presenters[room_id][peer_id]['peer_name'] = peer_name_new;
                }
                peer_id_to_update = peer_id;
                log.debug('[' + socket.id + '] Peer name changed', {
                    peer_name_old: peer_name_old,
                    peer_name_new: peer_name_new,
                });
            }
        }

        if (peer_id_to_update) {
            const data = {
                peer_id: peer_id_to_update,
                peer_name: peer_name_new,
                peer_avatar: peer_avatar,
            };
            log.debug('[' + socket.id + '] emit peerName to [room_id: ' + room_id + ']', data);

            await sendToRoom(room_id, socket.id, 'peerName', data);
        }
    });

    /**
     * Handle messages
     */
    socket.on('message', async (message) => {
        const data = checkXSS(message);

        log.debug('Got message', data);

        if (!Validate.isValidData(data)) return;

        await sendToRoom(data.room_id, socket.id, 'message', data);
    });

    /**
     * Relay commands to peers or specific peer in the same room
     * @param {Object} cfg - The configuration object containing command details.
     * @param {string} cfg.action - The action to be performed (e.g., 'geoLocation').
     * @param {boolean} cfg.send_to_all - Whether to send the command to all peers in the room.
     * @param {Object} cfg.data - The data associated with the command.
     */
    socket.on('cmd', async (cfg) => {
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        const { action, send_to_all, data } = config;

        const { room_id, peer_id, peer_name, peer_uuid, to_peer_id } = data;

        log.debug('cmd', config);

        // Only the presenter can do this actions
        const presenterActions = ['geoLocation'];
        if (presenterActions.some((v) => action === v)) {
            // Check if peer is presenter
            const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
            // if not presenter do nothing
            if (!isPresenter) return;
        }

        if (send_to_all) {
            log.debug('[' + socket.id + '] emit cmd to [room_id: ' + room_id + ']', config);

            await sendToRoom(room_id, socket.id, 'cmd', config);
        } else {
            log.debug('[' + socket.id + '] emit cmd to [' + to_peer_id + '] from room_id [' + room_id + ']');

            await sendToPeer(to_peer_id, sockets, 'cmd', config);
        }
    });

    /**
     * Relay Audio Video Hand ... Status to peers
     */
    socket.on('peerStatus', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        // log.debug('Peer status', config);
        const { room_id, peer_name, peer_id, element, status, extras } = config;

        const data = {
            peer_id: peer_id,
            peer_name: peer_name,
            element: element,
            status: status,
            extras: extras,
        };

        try {
            for (let peer_id in peers[room_id]) {
                if (peers[room_id][peer_id]['peer_name'] == peer_name && peer_id == socket.id) {
                    switch (element) {
                        case 'video':
                            peers[room_id][peer_id]['peer_video_status'] = status;
                            break;
                        case 'audio':
                            peers[room_id][peer_id]['peer_audio_status'] = status;
                            break;
                        case 'screen':
                            peers[room_id][peer_id]['peer_screen_status'] = status;
                            if (extras) peers[room_id][peer_id]['extras'] = extras;
                            break;
                        case 'hand':
                            peers[room_id][peer_id]['peer_hand_status'] = status;
                            break;
                        case 'rec':
                            peers[room_id][peer_id]['peer_rec_status'] = status;
                            break;
                        case 'privacy':
                            peers[room_id][peer_id]['peer_privacy_status'] = status;
                            break;
                        default:
                            break;
                    }
                }
            }

            log.debug('[' + socket.id + '] emit peerStatus to [room_id: ' + room_id + ']', data);

            await sendToRoom(room_id, socket.id, 'peerStatus', data);
        } catch (err) {
            log.error('Peer Status', toJson(err));
        }
    });

    /**
     * Relay actions to peers or specific peer in the same room
     */
    socket.on('peerAction', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        // log.debug('Peer action', config);
        const {
            room_id,
            peer_id,
            peer_uuid,
            peer_name,
            peer_avatar,
            peer_use_video,
            peer_action,
            extras,
            send_to_all,
        } = config;

        // Only the presenter can do this actions
        const presenterActions = ['muteAudio', 'hideVideo', 'ejectAll'];
        if (presenterActions.some((v) => peer_action === v)) {
            // Check if peer is presenter
            const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
            // if not presenter do nothing
            if (!isPresenter) return;
        }

        const data = {
            peer_id: peer_id,
            peer_name: peer_name,
            peer_avatar: peer_avatar,
            peer_action: peer_action,
            peer_use_video: peer_use_video,
            extras: extras,
        };

        if (send_to_all) {
            log.debug('[' + socket.id + '] emit peerAction to [room_id: ' + room_id + ']', data);

            await sendToRoom(room_id, socket.id, 'peerAction', data);
        } else {
            log.debug('[' + socket.id + '] emit peerAction to [' + peer_id + '] from room_id [' + room_id + ']');

            await sendToPeer(peer_id, sockets, 'peerAction', data);
        }
    });

    /**
     * Start caption
     */
    socket.on('caption', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        await sendToRoom(cfg.room_id, sockets, 'caption', config);
    });

    /**
     * Relay Kick out peer from room
     */
    socket.on('kickOut', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        const { room_id, peer_id, peer_uuid, peer_name } = config;

        // Check if peer is presenter
        const isPresenter = await isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);

        // Only the presenter can kickOut others
        if (isPresenter) {
            log.debug('[' + socket.id + '] kick out peer [' + peer_id + '] from room_id [' + room_id + ']');

            await sendToPeer(peer_id, sockets, 'kickOut', {
                peer_name: peer_name,
            });
        }
    });

    /**
     * Relay File info
     */
    socket.on('fileInfo', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        // log.debug('File info', config);
        const { room_id, peer_id, peer_name, peer_avatar, broadcast, file } = config;

        // check if valid fileName
        if (!isValidFileName(file.fileName)) {
            log.debug('[' + socket.id + '] File name not valid', config);
            return;
        }

        function bytesToSize(bytes) {
            let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
            if (bytes == 0) return '0 Byte';
            let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
            return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
        }

        log.debug('[' + socket.id + '] Peer [' + peer_name + '] send file to room_id [' + room_id + ']', {
            peerName: peer_name,
            peerAvatar: peer_avatar,
            fileName: file.fileName,
            fileSize: bytesToSize(file.fileSize),
            fileType: file.fileType,
            broadcast: broadcast,
        });

        if (broadcast) {
            await sendToRoom(room_id, socket.id, 'fileInfo', config);
        } else {
            await sendToPeer(peer_id, sockets, 'fileInfo', config);
        }
    });

    /**
     * Abort file sharing
     */
    socket.on('fileAbort', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        const { room_id, peer_name } = config;

        log.debug('[' + socket.id + '] Peer [' + peer_name + '] send fileAbort to room_id [' + room_id + ']');
        await sendToRoom(room_id, socket.id, 'fileAbort');
    });

    socket.on('fileReceiveAbort', async (cfg) => {
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        const { room_id, peer_name } = config;
        log.debug('[' + socket.id + '] Peer [' + peer_name + '] send fileReceiveAbort to room_id [' + room_id + ']');
        await sendToRoom(room_id, socket.id, 'fileReceiveAbort', config);
    });

    /**
     * Relay video player action
     */
    socket.on('videoPlayer', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        // log.debug('Video player', config);
        const { room_id, peer_id, peer_name, video_action, video_src } = config;

        // Check if valid video src url
        if (video_action == 'open' && !isValidHttpURL(video_src)) {
            log.debug('[' + socket.id + '] Video src not valid', config);
            return;
        }

        const data = {
            peer_id: socket.id,
            peer_name: peer_name,
            video_action: video_action,
            video_src: video_src,
        };

        if (peer_id) {
            log.debug('[' + socket.id + '] emit videoPlayer to [' + peer_id + '] from room_id [' + room_id + ']', data);

            await sendToPeer(peer_id, sockets, 'videoPlayer', data);
        } else {
            log.debug('[' + socket.id + '] emit videoPlayer to [room_id: ' + room_id + ']', data);

            await sendToRoom(room_id, socket.id, 'videoPlayer', data);
        }
    });

    /**
     * Whiteboard actions for all user in the same room
     */
    socket.on('wbCanvasToJson', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        // log.debug('Whiteboard send canvas', config);
        const { room_id } = config;
        await sendToRoom(room_id, socket.id, 'wbCanvasToJson', config);
    });

    socket.on('whiteboardAction', async (cfg) => {
        // Prevent XSS injection
        const config = checkXSS(cfg);

        if (!Validate.isValidData(config)) return;

        log.debug('Whiteboard', config);
        const { room_id } = config;
        await sendToRoom(room_id, socket.id, 'whiteboardAction', config);
    });

    /**
     * Add peers to channel
     * @param {string} channel room id
     */
    async function addPeerTo(channel) {
        for (let id in channels[channel]) {
            // offer false
            await channels[channel][id].emit('addPeer', {
                peer_id: socket.id,
                peers: peers[channel],
                should_create_offer: false,
                iceServers: iceServers,
            });
            // offer true
            socket.emit('addPeer', {
                peer_id: id,
                peers: peers[channel],
                should_create_offer: true,
                iceServers: iceServers,
            });
            log.debug('[' + socket.id + '] emit addPeer [' + id + ']');
        }
    }

    /**
     * Remove peers from channel
     * @param {string} channel room id
     */
    async function removePeerFrom(channel, socket, reason = 'unknown') {
        if (!(channel in socket.channels)) {
            return log.debug('[' + socket.id + '] [Warning] not in ', channel);
        }
        try {
            // Handle WebHook
            if (webhook.enabled) {
                const data = {
                    timestamp: log.getDateTime(false),
                    room_id: channel,
                    peer: socket.channels[channel],
                    reason: reason,
                };
                // Trigger a POST request when a user disconnects
                axios
                    .post(webhook.url, { event: 'disconnect', data }, { timeout: 5000 }) // 5 second timeout
                    .then((response) => log.debug('Disconnect event tracked:', response.data))
                    .catch((error) => log.error('Error tracking disconnect event:', error.message));
            }

            delete socket.channels[channel];
            delete channels[channel][socket.id];
            delete peers[channel][socket.id]; // delete peer data from the room

            switch (Object.keys(peers[channel]).length) {
                case 0: // last peer disconnected from the room without room lock & password set
                    delete peers[channel];
                    delete presenters[channel];
                    delete channels[channel]; // Clean up channels to prevent memory leak
                    break;
                case 2: // last peer disconnected from the room having room lock & password set
                    if (peers[channel]['lock'] && peers[channel]['password']) {
                        delete peers[channel]; // clean lock and password value from the room
                        delete presenters[channel]; // clean the presenter from the channel
                        delete channels[channel]; // Clean up channels to prevent memory leak
                    }
                    break;
                default:
                    break;
            }
        } catch (err) {
            log.error('Remove Peer', toJson(err));
        }

        const activeRooms = getActiveRooms();

        log.debug('[removePeerFrom] - active rooms and peers count', activeRooms);

        log.debug('[removePeerFrom] - connected presenters grp by roomId', presenters);

        log.debug('[removePeerFrom] - connected peers grp by roomId', peers);

        for (let id in channels[channel]) {
            await channels[channel][id].emit('removePeer', { peer_id: socket.id });
            socket.emit('removePeer', { peer_id: id });
            log.debug('[' + socket.id + '] emit removePeer [' + id + ']');
        }

        if (!OIDC.enabled && hostCfg.protected) {
            hostCfg.authenticated = false;
            removeIP(socket);
        }
    }

    /**
     * Object to Json
     * @param {object} data object
     * @returns {json} indent 4 spaces
     */
    function toJson(data) {
        return JSON.stringify(data, null, 4); // "\t"
    }

    /**
     * Send async data to all peers in the same room except yourself
     * @param {string} room_id id of the room to send data
     * @param {string} socket_id socket id of peer that send data
     * @param {string} msg message to send to the peers in the same room
     * @param {object} config data to send to the peers in the same room
     */
    async function sendToRoom(room_id, socket_id, msg, config = {}) {
        for (let peer_id in channels[room_id]) {
            // not send data to myself
            if (peer_id != socket_id) {
                await channels[room_id][peer_id].emit(msg, config);
                //console.log('Send to room', { msg: msg, config: config });
            }
        }
    }

    /**
     * Send async data to specified peer
     * @param {string} peer_id id of the peer to send data
     * @param {object} sockets all peers connections
     * @param {string} msg message to send to the peer in the same room
     * @param {object} config data to send to the peer in the same room
     */
    async function sendToPeer(peer_id, sockets, msg, config = {}) {
        if (peer_id in sockets) {
            await sockets[peer_id].emit(msg, config);
            //console.log('Send to peer', { msg: msg, config: config });
        }
    }
}); // end [sockets.on-connect]

/**
 * Get Env as boolean
 * @param {string} key
 * @param {boolean} force_true_if_undefined
 * @returns boolean
 */
function getEnvBoolean(key, force_true_if_undefined = false) {
    if (key == undefined && force_true_if_undefined) return true;
    return key == 'true' ? true : false;
}

/**
 * Check if valid filename
 * @param {string} fileName
 * @returns boolean
 */
function isValidFileName(fileName) {
    const invalidChars = /[\\\/\?\*\|:"<>]/;
    return !invalidChars.test(fileName);
}

/**
 * Check if valid URL
 * @param {string} str to check
 * @returns boolean true/false
 */
function isValidHttpURL(input) {
    try {
        const url = new URL(input);
        return url.protocol === 'http:' || url.protocol === 'https:';
    } catch (_) {
        return false;
    }
}

/**
 * get Peer geo Location using GeoJS
 * https://www.geojs.io/docs/v1/endpoints/geo/
 *
 * @param {string} ip
 * @returns json
 */
async function getPeerGeoLocation(ip) {
    const endpoint = `https://get.geojs.io/v1/ip/geo/${ip}.json`;
    log.debug('Get peer geo', { ip: ip, endpoint: endpoint });
    return axios
        .get(endpoint)
        .then((response) => response.data)
        .catch((error) => log.error(error));
}

/**
 * Check if peer is Presenter
 * @param {string} room_id
 * @param {string} peer_id
 * @param {string} peer_name
 * @param {string} peer_uuid
 * @returns boolean
 */
function isPeerPresenter(room_id, peer_id, peer_name, peer_uuid) {
    try {
        if (!presenters[room_id] || !presenters[room_id][peer_id]) {
            // Presenter not in the presenters config list, disconnected, or peer_id changed...
            for (const [existingPeerID, presenter] of Object.entries(presenters[room_id] || {})) {
                if (presenter.peer_name === peer_name) {
                    log.debug('[' + peer_id + '] Presenter found', presenters[room_id][existingPeerID]);
                    return true;
                }
            }
            return false;
        }

        const isPresenter =
            (typeof presenters[room_id] === 'object' &&
                Object.keys(presenters[room_id][peer_id]).length > 1 &&
                presenters[room_id][peer_id]['peer_name'] === peer_name &&
                presenters[room_id][peer_id]['peer_uuid'] === peer_uuid) ||
            (roomPresenters && roomPresenters.includes(peer_name));

        log.debug('[' + peer_id + '] isPeerPresenter', presenters[room_id][peer_id]);

        return isPresenter;
    } catch (err) {
        log.error('isPeerPresenter', err);
        return false;
    }
}

/**
 * Check if peer is present in the host users
 * @param {string} username
 * @param {string} password
 * @returns Boolean true/false
 */
function isAuthPeer(username, password) {
    return hostCfg.users && hostCfg.users.some((user) => user.username === username && user.password === password);
}

/**
 * Check if valid JWT token
 * @param {string} token
 * @returns boolean
 */
async function isValidToken(token) {
    return new Promise((resolve, reject) => {
        jwt.verify(token, jwtCfg.JWT_KEY, (err, decoded) => {
            if (err) {
                // Token is invalid
                resolve(false);
            } else {
                // Token is valid
                resolve(true);
            }
        });
    });
}

/**
 * Encode JWT token payload
 * @param {object} token
 * @returns
 */
function encodeToken(token) {
    if (!token) return '';

    const { username = 'username', password = 'password', presenter = false, expire } = token;

    const expireValue = expire || jwtCfg.JWT_EXP;

    // Constructing payload
    const payload = {
        username: String(username),
        password: String(password),
        presenter: String(presenter),
    };

    // Encrypt payload using AES encryption
    const payloadString = JSON.stringify(payload);
    const encryptedPayload = CryptoJS.AES.encrypt(payloadString, jwtCfg.JWT_KEY).toString();

    // Constructing JWT token
    const jwtToken = jwt.sign({ data: encryptedPayload }, jwtCfg.JWT_KEY, { expiresIn: expireValue });

    return jwtToken;
}

/**
 * Decode JWT Payload data
 * @param {object} jwtToken
 * @returns mixed
 */
function decodeToken(jwtToken) {
    if (!jwtToken) return null;

    // Verify and decode the JWT token
    const decodedToken = jwt.verify(jwtToken, jwtCfg.JWT_KEY);
    if (!decodedToken || !decodedToken.data) {
        throw new Error('Invalid token');
    }

    // Decrypt the payload using AES decryption
    const decryptedPayload = CryptoJS.AES.decrypt(decodedToken.data, jwtCfg.JWT_KEY).toString(CryptoJS.enc.Utf8);

    // Parse the decrypted payload as JSON
    const payload = JSON.parse(decryptedPayload);

    return payload;
}

/**
 * Get All connected peers count grouped by roomId
 * @return {object} array
 */
function getActiveRooms() {
    const roomPeersArray = [];
    // Iterate through each room
    for (const roomId in peers) {
        if (peers.hasOwnProperty(roomId)) {
            // Get the count of peers in the current room
            const peersCount = Object.keys(peers[roomId]).length;
            roomPeersArray.push({
                roomId: roomId,
                peersCount: peersCount,
            });
        }
    }
    return roomPeersArray;
}

/**
 * Check if Allowed Room Access
 * @param {string} logMessage
 * @param {object} req
 * @param {object} hostCfg
 * @param {object} peers
 * @param {string} roomId
 * @returns boolean true/false
 */
function isAllowedRoomAccess(logMessage, req, hostCfg, peers, roomId) {
    const OIDCUserAuthenticated = OIDC.enabled && req.oidc.isAuthenticated();
    const OIDCAllowRoomCreationForAuthUsers = OIDC.allowRoomCreationForAuthUsers;
    const hostUserAuthenticated = hostCfg.protected && hostCfg.authenticated;
    const roomExist = roomId in peers;
    const roomCount = Object.keys(peers).length;

    const allowRoomAccess =
        (!hostCfg.protected && !OIDC.enabled) || // No host protection and OIDC mode enabled (default)
        (OIDCUserAuthenticated && roomExist) || // User authenticated via OIDC and room Exist
        (hostUserAuthenticated && roomExist) || // User authenticated via Login and room Exist
        ((OIDCUserAuthenticated || hostUserAuthenticated) && roomCount === 0) || // User authenticated joins the first room
        (OIDCUserAuthenticated && OIDCAllowRoomCreationForAuthUsers) || // Allow room creation if authenticated via OIDC
        roomExist; // User Or Guest join an existing Room

    log.debug(logMessage, {
        OIDCUserAuthenticated: OIDCUserAuthenticated,
        hostUserAuthenticated: hostUserAuthenticated,
        roomExist: roomExist,
        roomCount: roomCount,
        extraInfo: {
            roomId: roomId,
            OIDCUserEnabled: OIDC.enabled,
            hostProtected: hostCfg.protected,
            hostAuthenticated: hostCfg.authenticated,
            OIDCAllowRoomCreationForAuthUsers,
        },
        allowRoomAccess: allowRoomAccess,
    });

    return allowRoomAccess;
}

/**
 * Get ip
 * @param {object} req
 * @returns string ip
 */
function getIP(req) {
    const forwarded = req.headers['x-forwarded-for'] || req.headers['X-Forwarded-For'];

    if (forwarded) {
        // Return only the first IP (client's real IP)
        return forwarded.split(',')[0].trim();
    }

    return req.socket.remoteAddress || req.ip;
}

/**
 * Get IP from socket
 * @param {object} socket
 * @returns string
 */
function getSocketIP(socket) {
    const forwarded = socket.handshake.headers['x-forwarded-for'] || socket.handshake.headers['X-Forwarded-For'];

    if (forwarded) {
        // Return only the first IP (client's real IP)
        return forwarded.split(',')[0].trim();
    }

    return socket.handshake.address;
}

/**
 * Check if auth ip
 * @param {string} ip
 * @returns boolean
 */
function allowedIP(ip) {
    const authorizedIPs = authHost.getAuthorizedIPs();
    const authorizedIP = authHost.isAuthorizedIP(ip);
    log.info('Allowed IPs', { ip: ip, authorizedIP: authorizedIP, authorizedIPs: authorizedIPs });
    return authHost != null && authorizedIP;
}

/**
 * Remove hosts auth ip on socket disconnect
 * @param {object} socket
 */
function removeIP(socket) {
    if (hostCfg.protected) {
        const ip = getSocketIP(socket);
        log.debug('[removeIP] - Host protected check ip', { ip: ip });
        if (ip && allowedIP(ip)) {
            authHost.deleteIP(ip);
            hostCfg.authenticated = false;
            log.info('[removeIP] - Remove IP from auth', {
                ip: ip,
                authorizedIps: authHost.getAuthorizedIPs(),
            });
        }
    }
}

/**
 * Load modules if exists
 * @param {string} filePath
 * @returns
 */
function safeRequire(filePath) {
    let data = null;
    try {
        data = require(filePath);
    } catch (error) {
        log.error(error);
    }
    return data;
}

/**
 * Cleanup HTML injector when the application is shutting down
 */
process.on('SIGINT', () => {
    log.debug('PROCESS', 'SIGINT');
    htmlInjector.cleanup();
    process.exit();
});

process.on('SIGTERM', () => {
    log.debug('PROCESS', 'SIGTERM');
    htmlInjector.cleanup();
    process.exit();
});
