From e3511513bcacb4cbe1d13a13be4ae61578752c6e Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Sat, 29 Mar 2025 00:54:08 +0100 Subject: [PATCH] [mirotalksfu] - Improve config.js.template, add .env.template, update dep --- .env.template | 335 ++++++ README.md | 16 +- app/src/Logger.js | 8 +- app/src/Mattermost.js | 4 +- app/src/Room.js | 14 +- app/src/RtmpFile.js | 3 +- app/src/RtmpStreamer.js | 3 +- app/src/RtmpUrl.js | 3 +- app/src/Server.js | 280 ++--- app/src/ServerApi.js | 4 +- app/src/config.template.js | 1720 ++++++++++++++++++----------- app/src/lib/nodemailer.js | 13 +- app/src/middleware/IpWhitelist.js | 5 +- install.sh | 2 + package.json | 4 +- public/js/Brand.js | 2 +- public/js/Room.js | 4 +- public/js/RoomClient.js | 4 +- webhook/package.json | 4 +- 19 files changed, 1594 insertions(+), 834 deletions(-) create mode 100644 .env.template diff --git a/.env.template b/.env.template new file mode 100644 index 00000000..c7784fc5 --- /dev/null +++ b/.env.template @@ -0,0 +1,335 @@ +# ======================================================== +# MiroTalk SFU - Environment Configuration +# ======================================================== + +# config.js - Main configuration with: +# - All default values and documentation +# - Complex logic and validations +# - Safe to commit to version control +# +# .env - Environment overrides with: +# - Secrets and sensitive data (NEVER commit) +# - Environment-specific settings +# - Simple key=value format +# +# Why this works best: +# 1. SECURITY: Secrets stay out of codebase +# 2. FLEXIBILITY: Change settings without redeploy +# 3. SAFETY: Built-in defaults prevent crashes +# 4. DOCS: config.js explains all options +# +# Best practice: +# - Keep DEFAULT values in config.js +# - Keep SECRETS/ENV-SPECIFIC in .env +# - Add .env to .gitignore +# ============================================== + + +# ---------------------------------------------------- +# 1. Core System Configuration +# ---------------------------------------------------- + +NODE_ENV=development # Runtime environment: development|production +SFU_PUBLIC_IP= # Public IP address for WebRTC announcements +SFU_LISTEN_IP=0.0.0.0 # IP address to bind to +SFU_MIN_PORT=40000 # Minimum WebRTC port range +SFU_MAX_PORT=40100 # Maximum WebRTC port range +SFU_NUM_WORKERS= # Number of worker processes (defaults to CPU count) +SFU_SERVER=false # Enable/disable WebRTC server (true|false) + +# ---------------------------------------------------- +# 2. Server Configuration +# ---------------------------------------------------- + +SERVER_HOST_URL= # Public server URL (e.g., https://yourdomain.com) +SERVER_LISTEN_IP=0.0.0.0 # Server bind IP +SERVER_LISTEN_PORT=3010 # Port to listen on +TRUST_PROXY=false # Trust proxy headers (true in production behind reverse proxy) +SERVER_SSL_CERT=../ssl/cert.pem # Path to SSL certificate +SERVER_SSL_KEY=../ssl/key.pem # Path to SSL private key +CORS_ORIGIN=* # Allowed CORS origins (comma-separated) + +# ---------------------------------------------------- +# 3. Media Handling +# ---------------------------------------------------- + +# Recording +RECORDING_ENABLED=false # Enable recording functionality (true|false) +RECORDING_ENDPOINT= # Recording service endpoint es http://localhost:8080 + +# Rtmp streaming +RTMP_ENABLED=true # Enable RTMP streaming (true|false) +RTMP_FROM_FILE=true # Enable local file streaming +RTMP_FROM_URL=true # Enable URL streaming +RTMP_FROM_STREAM=true # Enable live stream (camera, microphone, screen, window) +RTMP_MAX_STREAMS=1 # Max simultaneous RTMP streams +RTMP_SERVER=rtmp://localhost:1935 # RTMP server URL +RTMP_APP_NAME=mirotalk # RTMP application name +RTMP_STREAM_KEY= # RTMP stream key (optional) +RTMP_SECRET=mirotalkRtmpSecret # RTMP API secret +RTMP_API_SECRET=mirotalkRtmpApiSecret # RTMP internal secret +RTMP_EXPIRATION_HOURS=4 # RTMP URL validity duration (hours) +RTMP_FFMPEG_PATH= # RTMP Custom path to FFmpeg binary (auto-detected if empty) + +# ---------------------------------------------------- +# 4. Security & Authentication +# ---------------------------------------------------- + +# Middleware +IP_WHITELIST_ENABLED=false # Restrict access by IP (true|false) +IP_WHITELIST_ALLOWED=127.0.0.1,::1 # Allowed IPs (comma-separated) + +# Token +JWT_SECRET=mirotalksfu_jwt_secret # Secret for JWT tokens +JWT_EXPIRATION=1h # JWT token expiration (e.g., 1h, 7d) + +# OIDC +OIDC_ENABLED=false # Enable OpenID Connect (true|false) +OIDC_ISSUER=https://server.example.com # OIDC provider URL +OIDC_BASE_URL= # OIDC base URL es https://yourdomain.com +OIDC_CLIENT_ID=clientID # OIDC client ID +OIDC_CLIENT_SECRET=clientSecret # OIDC client secret +OIDC_SECRET=mirotalksfu-oidc-secret # OIDC secret + +# Host protection +HOST_PROTECTED=false # Enable host protection (true|false) +HOST_USER_AUTH=false # Enable user authentication (true|false) + +# Endpoints +HOST_USERS_FROM_DB=false # Use DB for user auth (true|false) +USERS_API_SECRET=mirotalkweb_default_secret # Users API secret key +USERS_API_ENDPOINT=http://localhost:9000/api/v1/user/isAuth # User auth endpoint +USERS_ROOM_ALLOWED_ENDPOINT=http://localhost:9000/api/v1/user/isRoomAllowed # Room permission endpoint +USERS_ROOMS_ALLOWED_ENDPOINT=http://localhost:9000/api/v1/user/roomsAllowed # Allowed rooms endpoint +ROOM_EXISTS_ENDPOINT=http://localhost:9000/api/v1/room/exists # Room exists endpoint + +# Users +DEFAULT_USERNAME=username # Default admin username +DEFAULT_PASSWORD=password # Default admin password +DEFAULT_DISPLAY_NAME=username display name # Default display name +DEFAULT_ALLOWED_ROOMS=* # Default allowed rooms all or room1,room2... (comma-separated) + +# Presenters +PRESENTERS=Miroslav Pejic,miroslav.pejic.85@gmail.com, # Presenter usernames (comma-separated) +PRESENTER_JOIN_FIRST=true # First joiner becomes presenter (true|false) + +# ---------------------------------------------------- +# 5. API Configuration +# ---------------------------------------------------- + +API_SECRET=mirotalksfu_default_secret # Secret for API authentication +API_ALLOW_STATS=true # Enable stats API (true|false) + +# ---------------------------------------------------- +# 6. Third-Party Integrations +# ---------------------------------------------------- + +# ChatGPT Integration +CHATGPT_ENABLED=false # Enable ChatGPT integration (true|false) +CHATGPT_BASE_PATH=https://api.openai.com/v1/ # OpenAI base path +CHATGPT_API_KEY= # OpenAI API key +CHATGPT_MODEL=gpt-3.5-turbo # Model to use (gpt-3.5-turbo, gpt-4, etc.) +CHATGPT_MAX_TOKENS=1000 # Max response tokens +CHATGPT_TEMPERATURE=0 # Creativity level (0-1) + +# Video AI (HeyGen) Integration +VIDEOAI_ENABLED=false # Enable video AI avatars (true|false) +VIDEOAI_API_KEY= # HeyGen API key +VIDEOAI_SYSTEM_LIMIT= # AI system prompt + +# Email Alerts +EMAIL_ALERTS_ENABLED=false # Enable email notifications (true|false) +EMAIL_HOST=smtp.gmail.com # SMTP server host +EMAIL_PORT=587 # SMTP port +EMAIL_USERNAME=your_username # SMTP username +EMAIL_PASSWORD=your_password # SMTP password +EMAIL_SEND_TO=sfu.mirotalk@gmail.com # Notification recipient + +# Slack Integration +SLACK_ENABLED=false # Enable Slack integration (true|false) +SLACK_SIGNING_SECRET= # Slack app signing secret + +# Mattermost Integration +MATTERMOST_ENABLED=false # Enable Mattermost (true|false) +MATTERMOST_SERVER_URL=YourMattermostServerUrl # Mattermost server URL +MATTERMOST_USERNAME=YourMattermostUsername # Mattermost username +MATTERMOST_PASSWORD=YourMattermostPassword # Mattermost password +MATTERMOST_TOKEN=YourMattermostToken # Mattermost slash command token +MATTERMOST_COMMAND_NAME=/sfu # Mattermost command name +MATTERMOST_DEFAULT_MESSAGE=Here is your meeting room: # Mattermost default message + +# Discord Integration +DISCORD_ENABLED=false # Enable Discord bot (true|false) +DISCORD_TOKEN= # Discord bot token +DISCORD_COMMAND_NAME=/sfu # Discord command name +DISCORD_DEFAULT_MESSAGE=Here is your SFU meeting room: # Discord default message +DISCORD_BASE_URL=https://sfu.mirotalk.com/join/ # Discord Base URL for meeting rooms + +# Ngrok Tunnel +NGROK_ENABLED=false # Enable Ngrok tunneling (true|false) +NGROK_AUTH_TOKEN= # Ngrok authentication token + +# Sentry Error Tracking +SENTRY_ENABLED=false # Enable Sentry error tracking (true|false) +SENTRY_DSN= # Sentry DSN URL +SENTRY_TRACES_SAMPLE_RATE=0.2 # Error sampling rate (0-1) + +# Webhook Notifications +WEBHOOK_ENABLED=false # Enable webhook notifications (true|false) +WEBHOOK_URL=https://your-site.com/webhook-endpoint # Webhook endpoint URL + +# IP Geolocation +IP_LOOKUP_ENABLED=false # Enable IP lookup functionality (true|false) + +# ---------------------------------------------------- +# 7. UI Customization +# ---------------------------------------------------- + +# App +UI_LANGUAGE=en # Interface language (en, es, fr, etc.) +APP_NAME=MiroTalk SFU # Application name +APP_TITLE= # Custom HTML title (leave empty for default) +APP_DESCRIPTION= # Application description +JOIN_DESCRIPTION= # Join screen description +JOIN_BUTTON_LABEL=JOIN ROOM # Join button text +JOIN_LAST_LABEL=Your recent room: # Recent room label text + +# Site +SITE_TITLE= # Website title +SITE_ICON_PATH=../images/logo.svg # Favicon path +APPLE_TOUCH_ICON_PATH=../images/logo.svg # Apple touch icon path +NEW_ROOM_TITLE= # New room title +NEW_ROOM_DESC= # New room description + +# Meta +META_DESCRIPTION= # HTML meta description +META_KEYWORDS= # HTML meta keywords + +# OG +OG_TYPE=app-webrtc # OpenGraph type +OG_SITE_NAME=MiroTalk SFU # OG site name +OG_TITLE= # OG title +OG_DESCRIPTION= # OG description +OG_IMAGE_URL=https://sfu.mirotalk.com/images/mirotalksfu.png # OG image +OG_URL=https://sfu.mirotalk.com # OG URL + +# HTML +SHOW_FEATURES=true # Show features section (true|false) +SHOW_TEAMS=true # Show teams section (true|false) +SHOW_TRY_EASIER=true # Show "try easier" section (true|false) +SHOW_POWERED_BY=true # Show powered by (true|false) +SHOW_SPONSORS=true # Show sponsors (true|false) +SHOW_ADVERTISERS=true # Show advertisers (true|false) +SHOW_FOOTER=true # Show footer (true|false) + +# About +ABOUT_IMAGE_URL=../images/mirotalk-logo.gif # About section image +SUPPORT_URL=https://codecanyon.net/user/miroslavpejic85 # Support link URL +SUPPORT_TEXT=Support # Support button text +AUTHOR_LABEL=Author # Author label text +AUTHOR_NAME=Miroslav Pejic # Author name +LINKEDIN_URL=https://www.linkedin.com/in/miroslav-pejic-976a07101/ # LinkedIn profile +CONTACT_EMAIL=miroslav.pejic.85@gmail.com # Contact email +EMAIL_SUBJECT=MiroTalk SFU info # Email subject +COPYRIGHT_TEXT=MiroTalk SFU, all rights reserved # Copyright text + +# ---------------------------------------------------- +# 8. UI Button Configuration +# ---------------------------------------------------- + +# Main Control Buttons +SHOW_SHARE_BUTTON=true # Show share button (true|false) +SHOW_HIDE_ME=true # Show hide me button (true|false) +SHOW_AUDIO_BUTTON=true # Show audio button (true|false) +SHOW_VIDEO_BUTTON=true # Show video button (true|false) +SHOW_SCREEN_BUTTON=true # Show screen share button (true|false) +SHOW_SWAP_CAMERA=true # Show camera swap button (true|false) +SHOW_CHAT_BUTTON=true # Show chat button (true|false) +SHOW_POLL_BUTTON=true # Show poll button (true|false) +SHOW_EDITOR_BUTTON=true # Show editor button (true|false) +SHOW_RAISE_HAND=true # Show raise hand button (true|false) +SHOW_TRANSCRIPTION=true # Show transcription button (true|false) +SHOW_WHITEBOARD=true # Show whiteboard button (true|false) +SHOW_DOCUMENT_PIP=true # Show document PiP button (true|false) +SHOW_SNAPSHOT=true # Show snapshot button (true|false) +SHOW_EMOJI=true # Show emoji button (true|false) +SHOW_SETTINGS=true # Show settings button (true|false) +SHOW_ABOUT=true # Show about button (true|false) +SHOW_EXIT_BUTTON=true # Show exit button (true|false) + +# Settings Panel +ENABLE_FILE_SHARING=true # Enable file sharing (true|false) +SHOW_LOCK_ROOM=true # Show lock room button (true|false) +SHOW_UNLOCK_ROOM=true # Show unlock room button (true|false) +SHOW_BROADCASTING=true # Show broadcasting button (true|false) +SHOW_LOBBY=true # Show lobby button (true|false) +SHOW_EMAIL_INVITE=true # Show email invitation button (true|false) +SHOW_MIC_OPTIONS=true # Show mic options button (true|false) +SHOW_RTMP_TAB=true # Show RTMP tab (true|false) +SHOW_MODERATOR_TAB=true # Show moderator tab (true|false) +SHOW_RECORDING_TAB=true # Show recording tab (true|false) +HOST_ONLY_RECORDING=true # Only host can record (true|false) +ENABLE_PUSH_TO_TALK=true # Enable push-to-talk (true|false) +SHOW_KEYBOARD_SHORTCUTS=true # Show keyboard shortcuts (true|false) +SHOW_VIRTUAL_BACKGROUND=true # Show virtual background (true|false) + +# Video Controls +ENABLE_PIP=true # Enable picture-in-picture (true|false) +SHOW_MIRROR_BUTTON=true # Show mirror button (true|false) +SHOW_FULLSCREEN=true # Show fullscreen button (true|false) +SHOW_SNAPSHOT_BUTTON=true # Show snapshot button (true|false) +SHOW_MUTE_AUDIO=true # Show mute audio button (true|false) +SHOW_PRIVACY_TOGGLE=true # Show privacy toggle (true|false) +SHOW_VOLUME_CONTROL=true # Show volume control (true|false) +SHOW_FOCUS_BUTTON=true # Show focus button (true|false) +SHOW_SEND_MESSAGE=true # Show send message button (true|false) +SHOW_SEND_FILE=true # Show send file button (true|false) +SHOW_SEND_VIDEO=true # Show send video button (true|false) +SHOW_MUTE_VIDEO=true # Show mute video button (true|false) +SHOW_GEO_LOCATION=true # Show geoLocation button (true|false) +SHOW_BAN_BUTTON=true # Show ban button (true|false) +SHOW_EJECT_BUTTON=true # Show eject button (true|false) + +# Chat Controls +SHOW_CHAT_PIN=true # Show chat pin button (true|false) +SHOW_CHAT_MAXIMIZE=true # Show chat maximize button (true|false) +SHOW_CHAT_SAVE=true # Show chat save button (true|false) +SHOW_CHAT_EMOJI=true # Show chat emoji button (true|false) +SHOW_CHAT_MARKDOWN=true # Show chat markdown button (true|false) +SHOW_CHAT_SPEECH=true # Show chat speech button (true|false) +ENABLE_CHAT_GPT=true # Enable ChatGPT in chat (true|false) + +# Poll Controls +SHOW_POLL_PIN=true # Show poll pin button (true|false) +SHOW_POLL_MAXIMIZE=true # Show poll maximize button (true|false) +SHOW_POLL_SAVE=true # Show poll save button (true|false) + +# Participants Controls +SHOW_SAVE_INFO=true # Show save info button (true|false) +SHOW_SEND_FILE_ALL=true # Show send file to all button (true|false) +SHOW_EJECT_ALL=true # Show eject all button (true|false) + +# Whiteboard Controls +SHOW_WB_LOCK=true # Show whiteboard lock button (true|false) + +# ---------------------------------------------------- +# 9. Feature Flags +# ---------------------------------------------------- + +SURVEY_ENABLED=false # Enable post-call survey (true|false) +SURVEY_URL= # Survey URL + +# Redirect +REDIRECT_ENABLED=false # Enable post-call redirect (true|false) +REDIRECT_URL= # Redirect URL + +# Stats +STATS_ENABLED=true # Enable usage statistics (true|false) +STATS_SRC=https://stats.mirotalk.com/script.js # Stats script URL +STATS_ID=41d26670-f275-45bb-af82-3ce91fe57756 # Stats tracking ID + +# ---------------------------------------------------- +# 10. Mediasoup Configuration +# ---------------------------------------------------- + +MEDIASOUP_LOG_LEVEL=error # Mediasoup log level (debug, warn, error) \ No newline at end of file diff --git a/README.md b/README.md index 7ed1881d..d2d3a0c6 100644 --- a/README.md +++ b/README.md @@ -204,12 +204,14 @@ $ git clone https://github.com/miroslavpejic85/mirotalksfu.git $ cd mirotalksfu # Copy app/src/config.template.js in app/src/config.js and edit it if needed $ cp app/src/config.template.js app/src/config.js +# Copy .env.template to .env and edit it if needed +$ cp .env.template .env # Install dependencies - be patient, the first time will take a few minutes, in the meantime have a good coffee ;) $ npm install # Start the server $ npm start # If you want to start the server on a different port than the default use an env var -$ PORT=3011 npm start +$ SERVER_LISTEN_PORT=3011 npm start ``` - Open [https://localhost:3010](https://localhost:3010) or `:3011` if the default port has been changed in your browser. @@ -239,10 +241,22 @@ $ git clone https://github.com/miroslavpejic85/mirotalksfu.git $ cd mirotalksfu # Copy app/src/config.template.js in app/src/config.js IMPORTANT (edit it according to your needs) $ cp app/src/config.template.js app/src/config.js +# Copy .env.template to .env and edit it if needed +$ cp .env.template .env # Copy docker-compose.template.yml in docker-compose.yml and edit it if needed $ cp docker-compose.template.yml docker-compose.yml # (Optional) Get official image from Docker Hub $ docker-compose pull +``` + +Make sure to load the `.env` file in your `docker-compose.yml` by adding: + +```yaml +env_file: + - .env +``` + +```bash # Create and start containers $ docker-compose up # -d # To stop and remove resources diff --git a/app/src/Logger.js b/app/src/Logger.js index d97460de..d90ffe5f 100644 --- a/app/src/Logger.js +++ b/app/src/Logger.js @@ -6,21 +6,21 @@ const colors = require('colors'); const config = require('./config'); -config.console.colors ? colors.enable() : colors.disable(); +config.system?.console?.colors ? colors.enable() : colors.disable(); const options = { depth: null, - colors: config.console.colors, + colors: config.system?.console?.colors || false, }; module.exports = class Logger { constructor(appName = 'miroTalkSfu') { this.appName = colors.yellow(appName); - this.debugOn = config.console.debug; + this.debugOn = config.system?.console?.debug || true; this.timeStart = Date.now(); this.timeEnd = null; this.timeElapsedMs = null; this.tzOptions = { - timeZone: process.env.TZ || config.console.timeZone || 'UTC', + timeZone: process.env.TZ || config.system?.console?.timeZone || 'UTC', hour12: false, }; } diff --git a/app/src/Mattermost.js b/app/src/Mattermost.js index d463b27c..29ba1589 100644 --- a/app/src/Mattermost.js +++ b/app/src/Mattermost.js @@ -20,12 +20,12 @@ class Mattermost { password, commands = '/sfu', texts = '/sfu', - } = config.mattermost || {}; + } = config.integrations.mattermost || {}; if (!enabled) return; // Check if Mattermost integration is enabled this.app = app; - this.allowed = config.api.allowed && config.api.allowed.mattermost; + this.allowed = config.api?.allowed?.mattermost || false; this.token = token; this.serverUrl = serverUrl; this.username = username; diff --git a/app/src/Room.js b/app/src/Room.js index 462e4a4f..724413e5 100644 --- a/app/src/Room.js +++ b/app/src/Room.js @@ -34,8 +34,8 @@ module.exports = class Room { this._hostOnlyRecording = false; // ########################## this.recording = { - recSyncServerRecording: config?.server?.recording?.enabled || false, - recSyncServerEndpoint: config?.server?.recording?.endpoint || '', + recSyncServerRecording: config?.media?.recording?.enabled || false, + recSyncServerEndpoint: config?.media?.recording?.endpoint || '', }; // ########################## this._moderator = { @@ -49,9 +49,9 @@ module.exports = class Room { chat_cant_chatgpt: false, media_cant_sharing: false, }; - this.survey = config.survey; - this.redirect = config.redirect; - this.videoAIEnabled = config?.videoAI?.enabled || false; + this.survey = config?.features?.survey; + this.redirect = config?.features?.redirect; + this.videoAIEnabled = config?.integrations?.videoAI?.enabled || false; this.peers = new Map(); this.bannedPeers = []; this.webRtcTransport = config.mediasoup.webRtcTransport; @@ -62,12 +62,12 @@ module.exports = class Room { // RTMP configuration this.rtmpFileStreamer = null; this.rtmpUrlStreamer = null; - this.rtmp = config.server.rtmp || false; + this.rtmp = config?.media?.rtmp || false; // Polls this.polls = []; - this.isHostProtected = config.host.protected; + this.isHostProtected = config?.security?.host?.protected || false; // Share Media this.shareMediaData = {}; diff --git a/app/src/RtmpFile.js b/app/src/RtmpFile.js index d206c7ad..8c9b662e 100644 --- a/app/src/RtmpFile.js +++ b/app/src/RtmpFile.js @@ -1,8 +1,7 @@ 'use strict'; const config = require('./config'); -const ffmpegPath = - config.server.rtmp && config.server.rtmp.ffmpegPath ? config.server.rtmp.ffmpegPath : '/usr/bin/ffmpeg'; +const ffmpegPath = config.media?.rtmp?.ffmpegPath || '/usr/bin/ffmpeg'; const ffmpeg = require('fluent-ffmpeg'); ffmpeg.setFfmpegPath(ffmpegPath); diff --git a/app/src/RtmpStreamer.js b/app/src/RtmpStreamer.js index 944dad4f..39501ccb 100644 --- a/app/src/RtmpStreamer.js +++ b/app/src/RtmpStreamer.js @@ -3,8 +3,7 @@ const config = require('./config'); const { PassThrough } = require('stream'); const ffmpeg = require('fluent-ffmpeg'); -const ffmpegPath = - config.server.rtmp && config.server.rtmp.ffmpegPath ? config.server.rtmp.ffmpegPath : '/usr/bin/ffmpeg'; +const ffmpegPath = config.media?.rtmp?.ffmpegPath || '/usr/bin/ffmpeg'; ffmpeg.setFfmpegPath(ffmpegPath); const Logger = require('./Logger'); diff --git a/app/src/RtmpUrl.js b/app/src/RtmpUrl.js index 9e57994f..cf8c6f9e 100644 --- a/app/src/RtmpUrl.js +++ b/app/src/RtmpUrl.js @@ -1,8 +1,7 @@ 'use strict'; const config = require('./config'); -const ffmpegPath = - config.server.rtmp && config.server.rtmp.ffmpegPath ? config.server.rtmp.ffmpegPath : '/usr/bin/ffmpeg'; +const ffmpegPath = config.media?.rtmp?.ffmpegPath || '/usr/bin/ffmpeg'; const ffmpeg = require('fluent-ffmpeg'); ffmpeg.setFfmpegPath(ffmpegPath); diff --git a/app/src/Server.js b/app/src/Server.js index 1ae88206..e3bc6c6b 100644 --- a/app/src/Server.js +++ b/app/src/Server.js @@ -64,7 +64,7 @@ dev dependencies: { * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.7.99 + * @version 1.8.00 * */ @@ -107,8 +107,8 @@ const packageJson = require('../../package.json'); const { v4: uuidv4 } = require('uuid'); const crypto = require('crypto-js'); const RtmpStreamer = require('./RtmpStreamer.js'); // Import the RtmpStreamer class -const rtmpCfg = config.server.rtmp; -const rtmpDir = rtmpCfg && rtmpCfg.dir ? rtmpCfg.dir : 'rtmp'; +const rtmpCfg = config?.media?.rtmp; +const rtmpDir = rtmpCfg?.dir || 'rtmp'; // File and Url Rtmp streams count let rtmpFileStreamsCount = 0; @@ -120,14 +120,14 @@ const nodemailer = require('./lib/nodemailer'); // Slack API const CryptoJS = require('crypto-js'); const qS = require('qs'); -const slackEnabled = config.slack.enabled; -const slackSigningSecret = config.slack.signingSecret; +const slackEnabled = config?.integrations?.slack?.enabled || false; +const slackSigningSecret = config?.integrations?.slack?.signingSecret || ''; const app = express(); const options = { - cert: fs.readFileSync(path.join(__dirname, config.server.ssl.cert), 'utf-8'), - key: fs.readFileSync(path.join(__dirname, config.server.ssl.key), 'utf-8'), + cert: fs.readFileSync(path.join(__dirname, config?.server?.ssl.cert || '../ssl/cert.pem'), 'utf-8'), + key: fs.readFileSync(path.join(__dirname, config?.server?.ssl.key || '../ssl/key.pem'), 'utf-8'), }; const corsOptions = { @@ -143,38 +143,38 @@ const io = socketIo(server, { cors: corsOptions, }); -const host = config.server.hostUrl || `http://localhost:${config.server.listen.port}`; -const trustProxy = !!config.server.trustProxy; +const host = config?.server?.hostUrl || `http://localhost:${config?.server?.listen?.port || 3010}`; +const trustProxy = Boolean(config?.server?.trustProxy); const jwtCfg = { - JWT_KEY: (config.jwt && config.jwt.key) || 'mirotalksfu_jwt_secret', - JWT_EXP: (config.jwt && config.jwt.exp) || '1h', + JWT_KEY: config?.security?.jwt?.key || 'mirotalksfu_jwt_secret', + JWT_EXP: config?.security?.jwt?.exp || '1h', }; const hostCfg = { - protected: config.host.protected, - user_auth: config.host.user_auth, - users: config.host.users, - users_from_db: config.host.users_from_db, - users_api_room_allowed: config.host.users_api_room_allowed, - users_api_rooms_allowed: config.host.users_api_rooms_allowed, - users_api_endpoint: config.host.users_api_endpoint, - users_api_secret_key: config.host.users_api_secret_key, - api_room_exists: config.host.api_room_exists, - users: config.host.users, - authenticated: !config.host.protected, + protected: config?.security?.host?.protected, + authenticated: !config?.security?.host?.protected, + user_auth: config?.security?.host?.user_auth, + users: config?.security?.host?.users, + users_from_db: config?.security?.host?.users_from_db, + users_api_room_allowed: config?.security?.host?.users_api_room_allowed, + users_api_rooms_allowed: config?.security?.host?.users_api_rooms_allowed, + users_api_endpoint: config?.security?.host?.users_api_endpoint, + users_api_secret_key: config?.security?.host?.users_api_secret_key, + api_room_exists: config?.security?.host?.api_room_exists, + presenters: config?.security?.host?.presenters, }; const restApi = { basePath: '/api/v1', // api endpoint path docs: host + '/api/v1/docs', // api docs - allowed: config.api?.allowed, + allowed: config.api?.allowed || {}, }; // Sentry monitoring -const sentryEnabled = config.sentry.enabled; -const sentryDSN = config.sentry.DSN; -const sentryTracesSampleRate = config.sentry.tracesSampleRate; +const sentryEnabled = config.integrations?.sentry?.enabled || false; +const sentryDSN = config.integrations.sentry.DSN; +const sentryTracesSampleRate = config.integrations.sentry.tracesSampleRate; if (sentryEnabled) { Sentry.init({ dsn: sentryDSN, @@ -202,7 +202,7 @@ const webhook = { }; // Discord Bot -const { enabled, commands, token } = config.discord || {}; +const { enabled, commands, token } = config?.integrations?.discord || {}; if (enabled && commands.length > 0 && token) { const discordBot = new Discord(token, commands); @@ -218,12 +218,12 @@ const defaultStats = { // OpenAI/ChatGPT let chatGPT; -if (config.chatGPT.enabled) { - if (config.chatGPT.apiKey) { +if (config?.integrations?.chatGPT?.enabled) { + if (config?.integrations?.chatGPT?.apiKey) { const { OpenAI } = require('openai'); const configuration = { - basePath: config.chatGPT.basePath, - apiKey: config.chatGPT.apiKey, + basePath: config?.integrations?.chatGPT?.basePath, + apiKey: config?.integrations?.chatGPT?.apiKey, }; chatGPT = new OpenAI(configuration); } else { @@ -232,18 +232,18 @@ if (config.chatGPT.enabled) { } // OpenID Connect -const OIDC = config.oidc ? config.oidc : { enabled: false }; +const OIDC = config?.security?.oidc || { enabled: false }; // directory const dir = { - public: path.join(__dirname, '../../', 'public'), - rec: path.join(__dirname, '../', config?.server?.recording?.dir ? config.server.recording.dir + '/' : 'rec/'), - rtmp: path.join(__dirname, '../', config?.rtmp?.dir ? config.rtmp.dir + '/' : 'rtmp/'), + public: path.join(__dirname, '../../public'), + rec: path.join(__dirname, '../', config?.media?.recording?.dir || 'rec', '/'), + rtmp: path.join(__dirname, '../', config?.media?.rtmp?.dir || 'rtmp', '/'), }; // Rec directory create and set max file size -const recMaxFileSize = config?.server?.recording?.maxFileSize || 1 * 1024 * 1024 * 1024; // 1GB default -const serverRecordingEnabled = config?.server?.recording?.enabled; +const recMaxFileSize = config?.media?.recording?.maxFileSize || 1 * 1024 * 1024 * 1024; // 1GB default +const serverRecordingEnabled = config?.media?.recording?.enabled || false; if (serverRecordingEnabled) { log.debug('Server Recording enabled creating dir', dir.rtmp); if (!fs.existsSync(dir.rec)) { @@ -306,7 +306,7 @@ let nextMediasoupWorkerIdx = 0; // Autodetect announcedAddress with multiple fallback services if (!announcedAddress && IP === '0.0.0.0') { const detectPublicIp = async () => { - const services = config.services?.ip || [ + const services = config.system?.services?.ip || [ 'http://api.ipify.org', 'http://ipinfo.io/ip', 'http://ifconfig.me/ip', @@ -554,12 +554,12 @@ function startServer() { // UI buttons configuration app.get('/config', (req, res) => { - res.status(200).json({ message: config.ui ? config.ui.buttons : false }); + res.status(200).json({ message: config?.ui?.buttons || false }); }); // Brand configuration app.get('/brand', (req, res) => { - res.status(200).json({ message: config.ui ? config.ui.brand : false }); + res.status(200).json({ message: config?.ui?.brand || false }); }); // main page @@ -780,7 +780,7 @@ function startServer() { // Get stats endpoint app.get('/stats', (req, res) => { - const stats = config.stats ? config.stats : defaultStats; + const stats = config?.features?.stats || defaultStats; // log.debug('Send stats', stats); res.send(stats); }); @@ -833,12 +833,9 @@ function startServer() { authorizedIps: authHost.getAuthorizedIPs(), }); - const isPresenter = - config.presenters && config.presenters.join_first - ? true - : config.presenters && - config.presenters.list && - config.presenters.list.includes(username).toString(); + const isPresenter = Boolean( + hostCfg?.presenters?.join_first || hostCfg?.presenters?.list?.includes(username), + ); const token = encodeToken({ username: username, password: password, presenter: isPresenter }); const allowedRooms = await getUserAllowedRooms(username, password); @@ -848,8 +845,7 @@ function startServer() { if (isPeerValid) { log.debug('PEER LOGIN OK', { ip: ip, authorized: true }); - const isPresenter = - config.presenters && config.presenters.list && config.presenters.list.includes(username).toString(); + const isPresenter = hostCfg?.presenters?.list?.includes(username) || false; const token = encodeToken({ username: username, password: password, presenter: isPresenter }); const allowedRooms = await getUserAllowedRooms(username, password); return res.status(200).json({ message: token, allowedRooms: allowedRooms }); @@ -982,7 +978,9 @@ function startServer() { return res.status(400).send('RTMP server is not enabled or missing the config'); } - const domainName = config.ngrok.enabled ? 'localhost' : req.headers.host.split(':')[0]; + const domainName = config?.integrations?.ngrok?.enabled + ? 'localhost' + : req.headers.host?.split(':')[0] || 'localhost'; const rtmpServer = rtmpCfg.server != '' ? rtmpCfg.server : false; const rtmpServerAppName = rtmpCfg.appName != '' ? rtmpCfg.appName : 'live'; @@ -1283,7 +1281,7 @@ function startServer() { // #################################################### function getServerConfig(tunnel = false) { - return { + const safeConfig = { // Network & Connectivity network: { server_listen: host, @@ -1301,68 +1299,74 @@ function startServer() { // Security & Authentication security: { - cors_options: corsOptions, + cors: corsOptions, jwtCfg: jwtCfg, - hostProtected: hostCfg.protected || hostCfg.user_auth ? hostCfg : false, - ip_lookup_enabled: config.IPLookup?.enabled ? config.IPLookup : false, - oidc: OIDC.enabled ? OIDC : false, + host: hostCfg?.protected || hostCfg?.user_auth ? hostCfg : { presenters: hostCfg.presenters }, + ip_lookup: config.integrations?.IPLookup?.enabled ? config.integrations.IPLookup : false, + oidc: OIDC?.enabled ? OIDC : false, + middleware: { + IpWhitelist: config?.security?.middleware?.IpWhitelist?.enabled + ? config.security.middleware.IpWhitelist + : false, + //... + }, }, // API & Services api: { rest_api: restApi, - webhook: webhook, - presenters: config.presenters, + webhook: webhook.enabled ? webhook : false, }, // Media Configuration media: { mediasoup: { - listenInfos: config.mediasoup.webRtcTransport.listenInfos, - worker_bin: mediasoup.workerBin, - server_version: mediasoup.version, - client_version: mediasoupClient.version, + listenInfos: config.mediasoup?.webRtcTransport?.listenInfos, + worker_bin: mediasoup?.workerBin, + server_version: mediasoup?.version, + client_version: mediasoupClient?.version, }, - rtmp_enabled: rtmpCfg.enabled ? rtmpCfg : false, - videoAI_enabled: config.videoAI.enabled ? config.videoAI : false, - server_recording: config?.server?.recording, + rtmp_enabled: rtmpCfg?.enabled ? rtmpCfg : false, + videoAI_enabled: config.integrations?.videoAI?.enabled ? config.integrations.videoAI : false, + server_recording: config?.media?.recording?.enabled ? config.media.recording : false, }, // Communication Integrations integrations: { - discord: config.discord?.enabled ? config.discord : false, - mattermost: config.mattermost?.enabled ? config.mattermost : false, - slack: slackEnabled ? config.slack : false, - chatGPT: config.chatGPT?.enabled ? config.chatGPT : false, - email_alerts: config.email?.alert ? config.email : false, + discord: config.integrations?.discord?.enabled ? config.integrations.discord : false, + mattermost: config.integrations?.mattermost?.enabled ? config.integrations.mattermost : false, + slack: slackEnabled ? config.integrations?.slack : false, + chatGPT: config.integrations?.chatGPT?.enabled ? config.integrations.chatGPT : false, + email_alerts: config?.integrations?.email?.alert ? config.integrations.email : false, }, // UI & Branding ui: { - brand: config.ui.brand, - buttons: config.ui.buttons, - middleware: config.middleware, + brand: config.ui?.brand, + buttons: config.ui?.buttons, }, // Monitoring & Analytics monitoring: { - sentry: sentryEnabled ? config.sentry : false, - stats: config.stats?.enabled ? config.stats : false, - system_info: config.systemInfo, + sentry: sentryEnabled ? config.integrations?.sentry : false, + stats: config.features?.stats?.enabled ? config.features.stats : false, + system_info: config.system?.info, }, // Features & Functionality features: { - survey: config.survey?.enabled ? config.survey : false, - redirect: config.redirect?.enabled ? config.redirect : false, + survey: config.features?.survey?.enabled ? config.features.survey : false, + redirect: config.features?.redirect?.enabled ? config.features.redirect : false, }, // Version Information versions: { - app: packageJson.version, + app: packageJson?.version, node: process.versions.node, }, }; + + return safeConfig; } // #################################################### @@ -1371,8 +1375,8 @@ function startServer() { async function ngrokStart() { try { - await ngrok.authtoken(config.ngrok.authToken); - const listener = await ngrok.forward({ addr: config.server.listen.port }); + await ngrok.authtoken(config?.integrations?.ngrok?.authToken); + const listener = await ngrok.forward({ addr: config?.server?.listen?.port }); const tunnelUrl = listener.url(); log.info('Server config', getServerConfig(tunnelUrl)); } catch (err) { @@ -1386,7 +1390,7 @@ function startServer() { // START SERVER // #################################################### - server.listen(config.server.listen.port, () => { + server.listen(config?.server?.listen?.port || 3010, () => { log.log( `%c @@ -1401,7 +1405,7 @@ function startServer() { 'font-family:monospace', ); - if (config.ngrok.enabled && config.ngrok.authToken !== '') { + if (config?.integrations?.ngrok?.enabled && config?.integrations?.ngrok?.authToken !== '') { return ngrokStart(); } log.info('Server config', getServerConfig()); @@ -1527,7 +1531,7 @@ function startServer() { const peer_ip = getIpSocket(socket); // Get peer Geo Location - if (config.IPLookup.enabled && peer_ip != '::1') { + if (config?.integrations?.IPLookup?.enabled && peer_ip != '::1') { dataObject.peer_geo = await getPeerGeoLocation(peer_ip); } @@ -1569,10 +1573,10 @@ function startServer() { return cb('unauthorized'); } - is_presenter = + const is_presenter = presenter === '1' || presenter === 'true' || - (config.presenters.join_first && room.getPeersCount() === 0); + (hostCfg?.presenters?.join_first && room?.getPeersCount() === 0); log.debug('[Join] - HOST PROTECTED - USER AUTH check peer', { ip: peer_ip, @@ -1637,7 +1641,7 @@ function startServer() { is_presenter: is_presenter, }; // first we check if the username match the presenters username - if (config.presenters && config.presenters.list && config.presenters.list.includes(peer_name)) { + if (hostCfg?.presenters?.list?.includes(peer_name)) { presenters[socket.room_id][socket.id] = presenter; } else { // if not match the presenters username, the first one join room is the presenter @@ -2486,7 +2490,7 @@ function startServer() { socket.on('getChatGPT', async ({ time, room, name, prompt, context }, cb) => { if (!roomExists(socket)) return; - if (!config.chatGPT.enabled) return cb({ message: 'ChatGPT seems disabled, try later!' }); + if (!config?.integrations?.chatGPT?.enabled) return cb({ message: 'ChatGPT seems disabled, try later!' }); // https://platform.openai.com/docs/api-reference/completions/create try { @@ -2494,10 +2498,10 @@ function startServer() { context.push({ role: 'user', content: prompt }); // Call OpenAI's API to generate response const completion = await chatGPT.chat.completions.create({ - model: config.chatGPT.model || 'gpt-3.5-turbo', + model: config?.integrations?.chatGPT?.model || 'gpt-3.5-turbo', messages: context, - max_tokens: config.chatGPT.max_tokens, - temperature: config.chatGPT.temperature, + max_tokens: config?.integrations?.chatGPT?.max_tokens, + temperature: config?.integrations?.chatGPT?.temperature, }); // Extract message from completion const message = completion.choices[0].message.content.trim(); @@ -2532,14 +2536,14 @@ function startServer() { // https://docs.heygen.com/reference/list-avatars-v2 socket.on('getAvatarList', async ({}, cb) => { - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { - const response = await axios.get(`${config.videoAI.basePath}/v2/avatars`, { + const response = await axios.get(`${config?.integrations?.videoAI?.basePath}/v2/avatars`, { headers: { 'Content-Type': 'application/json', - 'X-Api-Key': config.videoAI.apiKey, + 'X-Api-Key': config?.integrations?.videoAI?.apiKey, }, }); @@ -2556,14 +2560,14 @@ function startServer() { // https://docs.heygen.com/reference/list-voices-v2 socket.on('getVoiceList', async ({}, cb) => { - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { - const response = await axios.get(`${config.videoAI.basePath}/v2/voices`, { + const response = await axios.get(`${config?.integrations?.videoAI?.basePath}/v2/voices`, { headers: { 'Content-Type': 'application/json', - 'X-Api-Key': config.videoAI.apiKey, + 'X-Api-Key': config?.integrations?.videoAI?.apiKey, }, }); @@ -2582,12 +2586,12 @@ function startServer() { socket.on('streamingNew', async ({ quality, avatar_id, voice_id }, cb) => { if (!roomExists(socket)) return; - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { const voice = voice_id ? { voice_id: voice_id } : {}; const response = await axios.post( - `${config.videoAI.basePath}/v1/streaming.new`, + `${config?.integrations?.videoAI?.basePath}/v1/streaming.new`, { quality, avatar_id, @@ -2597,7 +2601,7 @@ function startServer() { headers: { accept: 'application/json', 'content-type': 'application/json', - 'x-api-key': config.videoAI.apiKey, + 'x-api-key': config?.integrations?.videoAI?.apiKey, }, }, ); @@ -2617,17 +2621,17 @@ function startServer() { socket.on('streamingStart', async ({ session_id, sdp }, cb) => { if (!roomExists(socket)) return; - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { const response = await axios.post( - `${config.videoAI.basePath}/v1/streaming.start`, + `${config?.integrations?.videoAI?.basePath}/v1/streaming.start`, { session_id, sdp }, { headers: { 'Content-Type': 'application/json', - 'X-Api-Key': config.videoAI.apiKey, + 'X-Api-Key': config?.integrations?.videoAI?.apiKey, }, }, ); @@ -2647,17 +2651,17 @@ function startServer() { socket.on('streamingICE', async ({ session_id, candidate }, cb) => { if (!roomExists(socket)) return; - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { const response = await axios.post( - `${config.videoAI.basePath}/v1/streaming.ice`, + `${config?.integrations?.videoAI?.basePath}/v1/streaming.ice`, { session_id, candidate }, { headers: { 'Content-Type': 'application/json', - 'X-Api-Key': config.videoAI.apiKey, + 'X-Api-Key': config?.integrations?.videoAI?.apiKey, }, }, ); @@ -2677,12 +2681,12 @@ function startServer() { socket.on('streamingTask', async ({ session_id, text }, cb) => { if (!roomExists(socket)) return; - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { const response = await axios.post( - `${config.videoAI.basePath}/v1/streaming.task`, + `${config?.integrations?.videoAI?.basePath}/v1/streaming.task`, { session_id, text, @@ -2690,7 +2694,7 @@ function startServer() { { headers: { 'Content-Type': 'application/json', - 'X-Api-Key': config.videoAI.apiKey, + 'X-Api-Key': config?.integrations?.videoAI?.apiKey, }, }, ); @@ -2710,19 +2714,19 @@ function startServer() { socket.on('streamingInterrupt', async ({ session_id, text }, cb) => { if (!roomExists(socket)) return; - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); try { const response = await axios.post( - `${config.videoAI.basePath}/v1/streaming.interrupt`, + `${config?.integrations?.videoAI?.basePath}/v1/streaming.interrupt`, { session_id, }, { headers: { 'Content-Type': 'application/json', - 'X-Api-Key': config.videoAI.apiKey, + 'X-Api-Key': config?.integrations?.videoAI?.apiKey, }, }, ); @@ -2741,10 +2745,11 @@ function startServer() { socket.on('talkToOpenAI', async ({ text, context }, cb) => { if (!roomExists(socket)) return; - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); + try { - const systemLimit = config.videoAI.systemLimit; + const systemLimit = config?.integrations?.videoAI?.systemLimit; const arr = { messages: [...context, { role: 'system', content: systemLimit }, { role: 'user', content: text }], model: 'gpt-3.5-turbo', @@ -2769,18 +2774,19 @@ function startServer() { socket.on('streamingStop', async ({ session_id }, cb) => { if (!roomExists(socket)) return; - if (!config.videoAI.enabled || !config.videoAI.apiKey) + if (!config?.integrations?.videoAI?.enabled || !config?.integrations?.videoAI?.apiKey) return cb({ error: 'Video AI seems disabled, try later!' }); + try { const response = await axios.post( - `${config.videoAI.basePath}/v1/streaming.stop`, + `${config?.integrations?.videoAI?.basePath}/v1/streaming.stop`, { session_id, }, { headers: { 'Content-Type': 'application/json', - 'X-Api-Key': config.videoAI.apiKey, + 'X-Api-Key': config?.integrations?.videoAI?.apiKey, }, }, ); @@ -2820,7 +2826,12 @@ function startServer() { if (!isPresenter) return cb(false); const room = getRoom(socket); - const host = config.ngrok.enabled ? 'localhost' : socket.handshake.headers.host.split(':')[0]; + + const DEFAULT_HOST = 'localhost'; + const host = config?.ngrok?.enabled + ? DEFAULT_HOST + : socket?.handshake?.headers?.host?.split(':')[0] || DEFAULT_HOST; + const rtmp = await room.startRTMP(socket.id, room, host, 1935, `../${rtmpDir}/${file}`); if (rtmp !== false) rtmpFileStreamsCount++; @@ -2864,7 +2875,12 @@ function startServer() { if (!isPresenter) return cb(false); const room = getRoom(socket); - const host = config.ngrok.enabled ? 'localhost' : socket.handshake.headers.host.split(':')[0]; + + const DEFAULT_HOST = 'localhost'; + const host = config?.integrations?.ngrok?.enabled + ? DEFAULT_HOST + : socket?.handshake?.headers?.host?.split(':')[0] || DEFAULT_HOST; + const rtmp = await room.startRTMPfromURL(socket.id, room, host, 1935, inputVideoURL); if (rtmp !== false) rtmpUrlStreamsCount++; @@ -3266,11 +3282,7 @@ function startServer() { function isPeerPresenter(room_id, peer_id, peer_name, peer_uuid) { try { - if ( - config.presenters && - config.presenters.join_first && - (!presenters[room_id] || !presenters[room_id][peer_id]) - ) { + if (hostCfg?.presenters?.join_first && (!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) { @@ -3286,13 +3298,13 @@ function startServer() { } const isPresenter = - (config.presenters && - config.presenters.join_first && - 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) || - (config.presenters && config.presenters.list && config.presenters.list.includes(peer_name)); + // First condition: join_first validation + (hostCfg?.presenters?.join_first && + presenters[room_id]?.[peer_id]?.peer_name === peer_name && + presenters[room_id]?.[peer_id]?.peer_uuid === peer_uuid && + Object.keys(presenters[room_id]?.[peer_id] || {}).length > 1) || + // Fallback condition: list check + hostCfg?.presenters?.list?.includes(peer_name); log.debug('isPeerPresenter', { room_id: room_id, @@ -3491,7 +3503,7 @@ function startServer() { // Get allowed rooms for user from config.js file if (hostCfg.protected && !hostCfg.users_from_db) { - const isOIDCEnabled = config.oidc && config.oidc.enabled; + const isOIDCEnabled = config?.security?.oidc?.enabled; const user = hostCfg.users.find((user) => user.displayname === username || user.username === username); @@ -3512,7 +3524,7 @@ function startServer() { if (!username || !room) return false; - const isOIDCEnabled = config.oidc && config.oidc.enabled; + const isOIDCEnabled = config?.security?.oidc?.enabled; if (hostCfg.protected || hostCfg.user_auth) { // Check if allowed room for user from DB... @@ -3539,7 +3551,7 @@ function startServer() { } } - const isInPresenterLists = config.presenters.list.includes(username); + const isInPresenterLists = hostCfg?.presenters?.list?.includes(username); if (isInPresenterLists) { log.debug('isRoomAllowedForUser - user in presenters list room allowed', room); @@ -3571,7 +3583,7 @@ function startServer() { } async function getPeerGeoLocation(ip) { - const endpoint = config.IPLookup.getEndpoint(ip); + const endpoint = config?.integrations?.IPLookup?.getEndpoint(ip); log.debug('Get peer geo', { ip: ip, endpoint: endpoint }); return axios .get(endpoint) diff --git a/app/src/ServerApi.js b/app/src/ServerApi.js index 18e74c4d..9f600ca5 100644 --- a/app/src/ServerApi.js +++ b/app/src/ServerApi.js @@ -6,8 +6,8 @@ const CryptoJS = require('crypto-js'); const config = require('./config'); const { v4: uuidV4 } = require('uuid'); -const JWT_KEY = (config.jwt && config.jwt.key) || 'mirotalksfu_jwt_secret'; -const JWT_EXP = (config.jwt && config.jwt.exp) || '1h'; +const JWT_KEY = config.security?.jwt?.key || 'mirotalksfu_jwt_secret'; +const JWT_EXP = config.security?.jwt?.exp || '1h'; module.exports = class ServerApi { constructor(host = null, authorization = null) { diff --git a/app/src/config.template.js b/app/src/config.template.js index ef7797a1..1848fc45 100644 --- a/app/src/config.template.js +++ b/app/src/config.template.js @@ -1,636 +1,1116 @@ 'use strict'; +/** + * ============================================== + * MiroTalk SFU - Configuration File + * ============================================== + * + * This file contains all configurable settings for the MiroTalk SFU application. + * Environment variables can override most settings (see each section for details). + * + * Structure: + * 1. Core System Configuration + * 2. Server Settings + * 3. Media Handling + * 4. Security & Authentication + * 5. API Configuration + * 6. Third-Party Integrations + * 7. UI/UX Customization + * 8. Feature Flags + * 9. Mediasoup (WebRTC) Settings + */ + const dotenv = require('dotenv').config(); const packageJson = require('../../package.json'); - const os = require('os'); const fs = require('fs'); +const splitChar = ','; + +// ============================================== +// 1. Environment Detection & System Configuration +// ============================================== const PLATFORM = os.platform(); const IS_DOCKER = fs.existsSync('/.dockerenv'); +const ENVIRONMENT = process.env.NODE_ENV || 'development'; +const PUBLIC_IP = process.env.SFU_PUBLIC_IP || ''; +const LISTEN_IP = process.env.SFU_LISTEN_IP || '0.0.0.0'; +const IPv4 = getIPv4(); -// ################################################################################################### -const ENVIRONMENT = process.env.NODE_ENV || 'development'; // production -const PUBLIC_IP = process.env.SFU_PUBLIC_IP || ''; // SFU Public IP -const LISTEN_IP = process.env.SFU_LISTEN_IP || '0.0.0.0'; // SFU listen IP -const IPv4 = getIPv4(); // Determines the appropriate IPv4 address based on ENVIRONMENT -// ################################################################################################### +// ============================================== +// 2. WebRTC Port Configuration +// ============================================== -/* - Set the port range for WebRTC communication. This range is used for the dynamic allocation of UDP ports for media streams. - - Each participant requires 2 ports: one for audio and one for video. - - The default configuration supports up to 50 participants (50 * 2 ports = 100 ports). - - To support more participants, simply increase the port range. - Note: - - When running in Docker, use 'network mode: host' for improved performance. - - Alternatively, enable 'webRtcServerActive: true' mode for better scalability. - - Make sure these port ranges are not blocked by the firewall, if they are, add the necessary rules -*/ const RTC_MIN_PORT = parseInt(process.env.SFU_MIN_PORT) || 40000; const RTC_MAX_PORT = parseInt(process.env.SFU_MAX_PORT) || 40100; - -/* - One worker can handle approximately 100 concurrent participants. - The number of workers cannot exceed the number of available CPU cores. -*/ const NUM_CPUS = os.cpus().length; const NUM_WORKERS = Math.min(process.env.SFU_NUM_WORKERS || NUM_CPUS, NUM_CPUS); -// RTMP using FMMPEG for streaming... -const FFMPEG_PATH = process.env.FFMPEG_PATH || getFFmpegPath(PLATFORM); +// ============================================== +// 3. FFmpeg Path Configuration +// ============================================== + +const RTMP_FFMPEG_PATH = process.env.RTMP_FFMPEG_PATH || getFFmpegPath(PLATFORM); + +// ============================================== +// Main Configuration Export +// ============================================== module.exports = { - services: { - ip: ['http://api.ipify.org', 'http://ipinfo.io/ip', 'http://ifconfig.me/ip'], - }, - systemInfo: { - os: { - type: os.type(), - release: os.release(), - arch: os.arch(), + // ============================================== + // 1. Core System Configuration + // ============================================== + + system: { + /** + * System Information + * ------------------ + * - Hardware/OS details collected automatically + * - Used for diagnostics and optimization + */ + info: { + os: { + type: os.type(), + release: os.release(), + arch: os.arch(), + }, + cpu: { + cores: NUM_CPUS, + model: os.cpus()[0].model, + }, + memory: { + total: (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB', + }, + isDocker: IS_DOCKER, }, - cpu: { - cores: os.cpus().length, - model: os.cpus()[0].model, + + /** + * Console Configuration + * --------------------- + * - timeZone: IANA timezone (e.g., 'Europe/Rome') + * - debug: Enable debug logging in non-production + * - colors: Colorized console output + */ + console: { + timeZone: 'UTC', + debug: ENVIRONMENT !== 'production', + colors: true, }, - memory: { - total: (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB', + + /** + * External Services Configuration + * ------------------------------- + * - ip: Services to detect public IP address + */ + services: { + ip: ['http://api.ipify.org', 'http://ipinfo.io/ip', 'http://ifconfig.me/ip'], }, - isDocker: IS_DOCKER, - }, - console: { - /* - timeZone: Time Zone corresponding to timezone identifiers from the IANA Time Zone Database es 'Europe/Rome' default UTC - */ - timeZone: 'UTC', - debug: true, - colors: true, }, + + // ============================================== + // 2. Server Configuration + // ============================================== + server: { - hostUrl: '', // default to http://localhost:port + /** + * Host Configuration + * ------------------ + * - hostUrl: Public URL (e.g., 'https://yourdomain.com') + * - listen: IP and port to bind to + */ + hostUrl: process.env.SERVER_HOST_URL || 'https://localhost:3010', listen: { - // app listen on - ip: '0.0.0.0', - port: process.env.PORT || 3010, + ip: process.env.SERVER_LISTEN_IP || '0.0.0.0', + port: process.env.SERVER_LISTEN_PORT || 3010, }, - trustProxy: false, // Enables trust for proxy headers (e.g., X-Forwarded-For) based on the trustProxy setting + + /** + * Security Settings + * ----------------- + * - trustProxy: Trust X-Forwarded-* headers + * - ssl: SSL certificate paths + * - cors: Cross-origin resource sharing + */ + trustProxy: process.env.TRUST_PROXY === 'true', ssl: { - // ssl/README.md - cert: process.env.SSL_CERT || '../ssl/cert.pem', - key: process.env.SSL_KEY || '../ssl/key.pem', + cert: process.env.SERVER_SSL_CERT || '../ssl/cert.pem', + key: process.env.SERVER_SSL_KEY || '../ssl/key.pem', }, cors: { - /* - origin: Allow specified origin es ['https://example.com', 'https://subdomain.example.com', 'http://localhost:3010'] or all origins if not specified - methods: Allow only GET and POST methods - */ - origin: '*', + origin: process.env.CORS_ORIGIN || '*', methods: ['GET', 'POST'], }, + }, + + // ============================================== + // 3. Media Handling Configuration + // ============================================== + + media: { + /** + * Recording Configuration + * ======================= + * Server side recording functionality. + * + * Core Settings: + * ------------------------ + * - enabled : Enable recording functionality + * - endpoint : Leave empty ('') to store recordings locally OR + * - Set to a valid URL (e.g., 'http://localhost:8080/') to: + * - Push recordings to a remote server + * - Store in cloud storage services + * - Send to processing pipelines + * - dir : Storage directory for recordings + * - maxFileSize : Maximum recording size (1GB default) + * + * Docker Note: + * ------------ + * - When running in Docker, ensure the recording directory exists and is properly mounted: + * 1. Create the directory (e.g., 'app/rec') + * 2. Configure as volume in docker-compose.yml + * 3. Set appropriate permissions + * 4. Restart container after changes + */ recording: { - /* - The recording will be saved to the directory designated within your Server app/ - Note: if you use Docker: Create the "app/rec" directory, configure it as a volume in docker-compose.yml, - ensure proper permissions, and start the Docker container. - */ - enabled: false, - endpoint: '', // Change the URL if you want to save the recording to a different server or cloud service (http://localhost:8080), otherwise leave it as is (empty). + enabled: process.env.RECORDING_ENABLED === 'true', + endpoint: process.env.RECORDING_ENDPOINT || '', dir: 'rec', - maxFileSize: 1 * 1024 * 1024 * 1024, // 1 GB + maxFileSize: 1 * 1024 * 1024 * 1024, // 1GB }, + + /** + * RTMP Configuration + * ======================== + * Real-Time Messaging Protocol for audio/video/data streaming + * + * Core Settings: + * ------------------------ + * - enabled: Enable RTMP streaming + * - fromFile: Local file streaming [true/false] (default: true) + * - fromUrl: URL streaming [true/false] (default: true) + * - fromStream: Live stream input [true/false] (default: true) + * - maxStreams: Max simultaneous streams (default: 1) + * - server: RTMP server URL + * - appName: Application name for RTMP + * - streamKey: Optional authentication key + * - secret: Must match node-media-server config.js + * - apiSecret: WebRTC → RTMP API secret + * - expirationHours: Stream URL expiry in hours (default: 4) + * - dir: Directory for video files + * - ffmpegPatch: FFmpeg binary path + * + * RTMP Server Management: + * ------------------------ + * - Start: npm run nms-start + * - Stop: npm run nms-stop + * - Logs: npm run nms-logs + * + * Important: + * ------------------------ + * - RTMP server must be running (npm run nms-start) + * - Requires open port 1935 + * - FFmpeg must be installed + * + * Documentation: + * ------------------------ + * - Docs: https://docs.mirotalk.com/mirotalk-sfu/rtmp/ + */ rtmp: { - /* - Real-Time Messaging Protocol (RTMP) is a communication protocol for streaming audio, video, and data over the Internet. (beta) - - Configuration: - - enabled: Enable or disable the RTMP streaming feature. Set to 'true' to enable, 'false' to disable. - - fromFile: Enable or disable the RTMP streaming from File. Set to 'true' to enable, 'false' to disable. - - fromUrl: Enable or disable the RTMP streaming from Url. Set to 'true' to enable, 'false' to disable. - - fromStream: Enable or disable the RTMP Streamer. Set to 'true' to enable, 'false' to disable. - - maxStreams: Specifies the maximum number of simultaneous streams permitted for File, URL, and Stream. The default value is 1. - - server: The URL of the RTMP server. Leave empty to use the built-in MiroTalk RTMP server (rtmp://localhost:1935). Change the URL to connect to a different RTMP server. - - appName: The application name for the RTMP stream. Default is 'mirotalk'. - - streamKey: The stream key for the RTMP stream. Leave empty if not required. - - secret: The secret key for RTMP streaming. Must match the secret in rtmpServers/node-media-server/src/config.js. Leave empty if no authentication is needed. - - apiSecret: The API secret for streaming WebRTC to RTMP through the MiroTalk API. - - expirationHours: The number of hours before the RTMP URL expires. Default is 4 hours. - - dir: Directory where your video files are stored to be streamed via RTMP. - - ffmpegPath: Path of the ffmpeg installation on the system (which ffmpeg) - - platform: 'darwin', 'linux', 'win32', etc. - - Important: Before proceeding, make sure your RTMP server is up and running. - For more information, refer to the documentation here: https://docs.mirotalk.com/mirotalk-sfu/rtmp/. - You can start the server by running the following command: - - Start: npm run nms-start - Start the RTMP server. - - Stop: npm run npm-stop - Stop the RTMP server. - - Logs: npm run npm-logs - View the logs of the RTMP server. - */ - enabled: false, - fromFile: true, - fromUrl: true, - fromStream: true, - maxStreams: 1, - server: 'rtmp://localhost:1935', - appName: 'mirotalk', - streamKey: '', - secret: 'mirotalkRtmpSecret', - apiSecret: 'mirotalkRtmpApiSecret', - expirationHours: 4, + enabled: process.env.RTMP_ENABLED === 'true', + fromFile: process.env.RTMP_FROM_FILE !== 'false', + fromUrl: process.env.RTMP_FROM_URL !== 'false', + fromStream: process.env.RTMP_FROM_STREAM !== 'false', + maxStreams: parseInt(process.env.RTMP_MAX_STREAMS) || 1, + server: process.env.RTMP_SERVER || 'rtmp://localhost:1935', + appName: process.env.RTMP_APP_NAME || 'mirotalk', + streamKey: process.env.RTMP_STREAM_KEY || '', + secret: process.env.RTMP_SECRET || 'mirotalkRtmpSecret', + apiSecret: process.env.RTMP_API_SECRET || 'mirotalkRtmpApiSecret', + expirationHours: parseInt(process.env.RTMP_EXPIRATION_HOURS) || 4, dir: 'rtmp', - ffmpegPath: FFMPEG_PATH, + ffmpegPath: RTMP_FFMPEG_PATH, platform: PLATFORM, }, }, - middleware: { - /* - Middleware: - - IP Whitelist: Access to the instance is restricted to only the specified IP addresses in the allowed list. This feature is disabled by default. - - ... - */ - IpWhitelist: { - enabled: false, - allowed: ['127.0.0.1', '::1'], + + // ============================================== + // 4. Security & Authentication + // ============================================== + + security: { + /** + * IP Whitelisting + * ------------------------ + * - enabled: Restrict access to specified IPs + * - allowedIPs: Array of permitted IP addresses + */ + middleware: { + IpWhitelist: { + enabled: process.env.IP_WHITELIST_ENABLED === 'true', + allowedIPs: process.env.IP_WHITELIST_ALLOWED + ? process.env.IP_WHITELIST_ALLOWED.split(splitChar) + .map((ip) => ip.trim()) + .filter((ip) => ip !== '') + : ['127.0.0.1', '::1'], + }, + }, + + /** + * JWT Configuration + * ------------------------ + * - key: Secret for JWT signing + * - exp: Token expiration time + */ + jwt: { + key: process.env.JWT_SECRET || 'mirotalksfu_jwt_secret', + exp: process.env.JWT_EXPIRATION || '1h', + }, + + /** + * OpenID Connect (OIDC) Authentication Configuration + * ================================================= + * Configures authentication using OpenID Connect (OIDC), allowing integration with + * identity providers like Auth0, Okta, Keycloak, etc. + * + * Structure: + * - enabled : Master switch for OIDC authentication + * - baseURLDynamic : Whether to dynamically resolve base URL + * - peer_name : Controls which user attributes to enforce/request + * - config : Core OIDC provider settings + * + * Core Settings: + * - issuerBaseURL : Provider's discovery endpoint (e.g., https://your-tenant.auth0.com) + * - baseURL : Your application's base URL + * - clientID : Client identifier issued by provider + * - clientSecret : Client secret issued by provider + * - secret : Application session secret + * - authRequired : Whether all routes require authentication + * - auth0Logout : Whether to use provider's logout endpoint + * - authorizationParams: OAuth/OIDC flow parameters including: + * - response_type : OAuth response type ('code' for Authorization Code Flow) + * - scope : Requested claims (openid, profile, email) + * - routes : Endpoint path configuration for: + * - callback : OAuth callback handler path + * - login : Custom login path (false to disable) + * - logout : Custom logout path + * + */ + oidc: { + enabled: process.env.OIDC_ENABLED === 'true', + baseURLDynamic: false, // Set true if your app has dynamic base URLs + + // User identity requirements + peer_name: { + force: true, // Require identity provider authentication + email: true, // Request email claim + name: false, // Don't require full name + }, + + // Provider configuration + config: { + // Required provider settings + issuerBaseURL: process.env.OIDC_ISSUER || 'https://server.example.com', + baseURL: process.env.OIDC_BASE_URL || `http://localhost:${process.env.PORT || 3010}`, + clientID: process.env.OIDC_CLIENT_ID || 'clientID', + clientSecret: process.env.OIDC_CLIENT_SECRET || 'clientSecret', + + // Session configuration + secret: process.env.OIDC_SECRET || 'mirotalksfu-oidc-secret', + authRequired: false, // Whether all routes require authentication + auth0Logout: true, // Use provider's logout endpoint + + // OAuth/OIDC flow parameters + authorizationParams: { + response_type: 'code', // Use authorization code flow + scope: 'openid profile email', // Request standard claims + }, + + // Route customization + routes: { + callback: '/auth/callback', // OAuth callback path + login: false, // Disable default login route + logout: '/logout', // Custom logout path + }, + }, + }, + + /** + * Host Protection Configuration + * ============================ + * Controls access to host-level functionality and room management. + * Supports multiple authentication methods including local users and API-based validation. + * + * Core Protection Settings: + * ------------------------- + * - protected : Enable/disable host protection globally + * - user_auth : Require user authentication for host access + * - users_from_db : Fetch authorized users from database/API instead of local config (eg., MiroTalk WEB) + * + * API Integration: + * ---------------- + * - users_api_secret_key : Secret key for API authentication + * - users_api_endpoint : Endpoint to validate user credentials + * - users_api_room_allowed : Endpoint to check if user can access specific room + * - users_api_rooms_allowed : Endpoint to get list of allowed rooms for user + * - api_room_exists : Endpoint to verify if room exists + * + * Local User Configuration: + * ------------------------- + * - users : Array of authorized users (used when users_from_db=false) + * - username : Login username + * - password : Login password + * - displayname : User's display name + * - allowed_rooms : List of rooms user can access ('*' for all rooms) + * + * Presenter Management: + * -------------------- + * - list : Array of usernames who can be presenters + * - join_first : First joiner becomes presenter [true/false] default true + * + * Documentation: + * -------------- + * - https://docs.mirotalk.com/mirotalk-sfu/host-protection/ + */ + host: { + protected: process.env.HOST_PROTECTED === 'true', + user_auth: process.env.HOST_USER_AUTH === 'true', + + users_from_db: process.env.HOST_USERS_FROM_DB === 'true', + users_api_secret_key: process.env.USERS_API_SECRET || 'mirotalkweb_default_secret', + users_api_endpoint: process.env.USERS_API_ENDPOINT || 'http://localhost:9000/api/v1/user/isAuth', // 'https://webrtc.mirotalk.com/api/v1/user/isAuth' + users_api_room_allowed: + process.env.USERS_ROOM_ALLOWED_ENDPOINT || 'http://localhost:9000/api/v1/user/isRoomAllowed', // 'https://webrtc.mirotalk.com/api/v1/user/isRoomAllowed' + users_api_rooms_allowed: + process.env.USERS_ROOMS_ALLOWED_ENDPOINT || 'http://localhost:9000/api/v1/user/roomsAllowed', // 'https://webrtc.mirotalk.com/api/v1/user/roomsAllowed' + api_room_exists: process.env.ROOM_EXISTS_ENDPOINT || 'http://localhost:9000/api/v1/room/exists', // 'https://webrtc.mirotalk.com//api/v1/room/exists' + + users: [ + { + username: process.env.DEFAULT_USERNAME || 'username', + password: process.env.DEFAULT_PASSWORD || 'password', + displayname: process.env.DEFAULT_DISPLAY_NAME || 'username display name', + allowed_rooms: process.env.DEFAULT_ALLOWED_ROOMS + ? process.env.DEFAULT_ALLOWED_ROOMS.split(splitChar) + .map((room) => room.trim()) + .filter((room) => room !== '') + : ['*'], + }, + // Additional users can be added here + ], + + presenters: { + list: process.env.PRESENTERS + ? process.env.PRESENTERS.split(splitChar) + .map((presenter) => presenter.trim()) + .filter((presenter) => presenter !== '') + : ['Miroslav Pejic', 'miroslav.pejic.85@gmail.com'], + join_first: process.env.PRESENTER_JOIN_FIRST !== 'false', + }, }, }, + + // ============================================== + // 5. API Configuration + // ============================================== + + /** + * API Security & Endpoint Configuration + * ==================================== + * Controls access to the SFU's API endpoints and integration settings. + * + * Security Settings: + * ----------------- + * - keySecret : Authentication secret for API requests + * (Always override default in production) + * + * Endpoint Control: + * ----------------- + * - stats : Enable/disable system statistics endpoint [true/false] (default: true) + * - meetings : Enable/disable meetings list endpoint [true/false] (default: true) + * - meeting : Enable/disable single meeting operations [true/false] (default: true) + * - join : Enable/disable meeting join endpoint [true/false] (default: true) + * - token : Enable/disable token generation endpoint [true/false] (default: false) + * - slack : Enable/disable Slack webhook integration [true/false] (default: true) + * - mattermost : Enable/disable Mattermost webhook integration [true/false] (default: true) + * + * API Documentation: + * ------------------ + * - Complete API reference: https://docs.mirotalk.com/mirotalk-sfu/api/ + * - Webhook setup: See integration guides for Slack/Mattermost + */ api: { - // Default secret key for app/api - keySecret: 'mirotalksfu_default_secret', - // Define which endpoints are allowed + keySecret: process.env.API_SECRET || 'mirotalksfu_default_secret', allowed: { - stats: true, + stats: process.env.API_ALLOW_STATS !== 'false', meetings: false, meeting: true, join: true, token: false, slack: true, mattermost: true, - //... }, }, - jwt: { - /* - JWT https://jwt.io/ - Securely manages credentials for host configurations and user authentication, enhancing security and streamlining processes. + + // ============================================== + // 6. Third-Party Integrations + // ============================================== + + integrations: { + /** + * ChatGPT Integration Configuration + * ================================ + * OpenAI API integration for AI-powered chat functionality + * + * Setup Instructions: + * ------------------ + * 1. Go to https://platform.openai.com/ + * 2. Create your OpenAI account + * 3. Generate your API key at https://platform.openai.com/account/api-keys + * + * Core Settings: + * ------------- + * - enabled : Enable/disable ChatGPT integration [true/false] (default: false) + * - basePath : OpenAI API endpoint (default: 'https://api.openai.com/v1/') + * - apiKey : OpenAI API secret key (ALWAYS store in .env) + * - model : GPT model version (default: 'gpt-3.5-turbo') + * + * Advanced Settings: + * ----------------- + * - max_tokens: Maximum response length (default: 1000 tokens) + * - temperature: Creativity control (0=strict, 1=creative) (default: 0) + * + * Usage Example: + * ------------- + * 1. Supported Models: + * - gpt-3.5-turbo (recommended) + * - gpt-4 + * - gpt-4-turbo + * + * 2. Temperature Guide: + * - 0.0: Factual responses + * - 0.7: Balanced + * - 1.0: Maximum creativity */ - key: 'mirotalksfu_jwt_secret', - exp: '1h', - }, - oidc: { - /* - OIDC stands for OpenID Connect, which is an authentication protocol built on top of OAuth 2.0. - It provides a simple identity layer on the OAuth 2.0 protocol, allowing clients to verify the identity of the end-user - based on the authentication performed by an authorization server. - How to configure your own Provider: - 1. Sign up for an account at https://auth0.com. - 2. Navigate to https://manage.auth0.com/ to create a new application tailored to your specific requirements. - For those seeking an open-source solution, check out: https://github.com/panva/node-oidc-provider - */ - enabled: false, - baseURLDynamic: false, - peer_name: { - force: true, // Enforce using profile data for peer_name - email: true, // Use email as peer_name - name: false, // Don't use full name (family_name + given_name) + chatGPT: { + enabled: process.env.CHATGPT_ENABLED === 'true', + basePath: process.env.CHATGPT_BASE_PATH || 'https://api.openai.com/v1/', + apiKey: process.env.CHATGPT_API_KEY || '', + model: process.env.CHATGPT_MODEL || 'gpt-3.5-turbo', + max_tokens: parseInt(process.env.CHATGPT_MAX_TOKENS) || 1000, + temperature: parseInt(process.env.CHATGPT_TEMPERATURE) || 0, }, - config: { - issuerBaseURL: 'https://server.example.com', - baseURL: `http://localhost:${process.env.PORT ? process.env.PORT : 3010}`, // https://sfu.mirotalk.com - clientID: 'clientID', - clientSecret: 'clientSecret', - secret: 'mirotalksfu-oidc-secret', - authorizationParams: { - response_type: 'code', - scope: 'openid profile email', - }, - authRequired: false, // Set to true if authentication is required for all routes - auth0Logout: 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. - }, - }, - }, - host: { - /* - Host Protection (default: false) - To enhance host security, enable host protection - user auth and provide valid - usernames and passwords in the users array or active users_from_db using users_api_endpoint for check. - When oidc.enabled is utilized alongside host protection, the authenticated user will be recognized as valid. - */ - protected: false, - user_auth: false, - users_from_db: false, // if true ensure that api.token is also set to true. - users_api_endpoint: 'http://localhost:9000/api/v1/user/isAuth', - users_api_room_allowed: 'http://localhost:9000/api/v1/user/isRoomAllowed', - users_api_rooms_allowed: 'http://localhost:9000/api/v1/user/roomsAllowed', - api_room_exists: 'http://localhost:9000/api/v1/room/exists', - //users_api_endpoint: 'https://webrtc.mirotalk.com/api/v1/user/isAuth', - //users_api_room_allowed: 'https://webrtc.mirotalk.com/api/v1/user/isRoomAllowed', - //users_api_rooms_allowed: 'https://webrtc.mirotalk.com/api/v1/user/roomsAllowed', - //api_room_exists: 'https://webrtc.mirotalk.com//api/v1/room/exists', - users_api_secret_key: 'mirotalkweb_default_secret', - users: [ - { - username: 'username', - password: 'password', - displayname: 'username displayname', - allowed_rooms: ['*'], - }, - { - username: 'username2', - password: 'password2', - displayname: 'username2 displayname', - allowed_rooms: ['room1', 'room2'], - }, - { - username: 'username3', - password: 'password3', - displayname: 'username3 displayname', - }, - //... - ], - }, - presenters: { - list: [ - /* - By default, the presenter is identified as the first participant to join the room, distinguished by their username and UUID. - Additional layers can be added to specify valid presenters and co-presenters by setting designated usernames. - */ - 'Miroslav Pejic', - 'miroslav.pejic.85@gmail.com', - ], - join_first: true, // Set to true for traditional behavior, false to prioritize presenters - }, - chatGPT: { - /* - ChatGPT - 1. Goto https://platform.openai.com/ - 2. Create your account - 3. Generate your APIKey https://platform.openai.com/account/api-keys - */ - enabled: false, - basePath: 'https://api.openai.com/v1/', - apiKey: '', - model: 'gpt-3.5-turbo', - max_tokens: 1000, - temperature: 0, - }, - videoAI: { - /* - HeyGen Video AI - 1. Goto https://app.heygen.com - 2. Create your account - 3. Generate your APIKey https://app.heygen.com/settings?nav=API + + /** + * HeyGen Video AI Configuration + * ============================ + * AI-powered avatar streaming integration + * + * Setup Instructions: + * ------------------ + * 1. Go to https://app.heygen.com + * 2. Create your HeyGen account + * 3. Generate your API key at https://app.heygen.com/settings?nav=API + * + * Core Settings: + * ------------- + * - enabled : Enable/disable Video AI [true/false] (default: false) + * - basePath : HeyGen API endpoint (default: 'https://api.heygen.com') + * - apiKey : From HeyGen account (ALWAYS store in .env) + * + * AI Behavior: + * ----------- + * - systemLimit: Personality/behavior instructions for the AI avatar + * (default: Streaming avatar instructions for MiroTalk SFU) */ - enabled: false, - basePath: 'https://api.heygen.com', - apiKey: '', - systemLimit: - 'You are a streaming avatar from MiroTalk SFU, an industry-leading product that specialize in videos communications.', - }, - email: { - /* - Configure email settings for notifications or alerts - Refer to the documentation for Gmail configuration: https://support.google.com/mail/answer/185833?hl=en - */ - alert: false, - host: 'smtp.gmail.com', - port: 587, - username: 'your_username', - password: 'your_password', - sendTo: 'sfu.mirotalk@gmail.com', - }, - ngrok: { - /* - Ngrok - 1. Goto https://ngrok.com - 2. Get started for free - 3. Copy YourNgrokAuthToken: https://dashboard.ngrok.com/get-started/your-authtoken - */ - enabled: false, - authToken: '', - }, - sentry: { - /* - Sentry - 1. Goto https://sentry.io/ - 2. Create account - 3. On dashboard goto Settings/Projects/YourProjectName/Client Keys (DSN) - */ - enabled: false, - DSN: '', - tracesSampleRate: 0.5, - }, - webhook: { - /* - Enable or disable webhook functionality. - Set `enabled` to `true` to activate webhook sending of socket events (join, exitRoom, disconnect) - */ - enabled: false, - url: 'https://your-site.com/webhook-endpoint', - }, - mattermost: { - /* - Mattermost: https://mattermost.com - 1. Navigate to Main Menu > Integrations > Slash Commands in Mattermost. - 2. Click on Add Slash Command and configure the following settings: - - Title: Enter a descriptive title (e.g., `SFU Command`). - - Command Trigger Word: Set the trigger word to `sfu`. - - Callback URLs: Enter the URL for your Express server (e.g., `https://yourserver.com/mattermost`). - - Request Method: Select POST. - - Enable Autocomplete: Check the box for Autocomplete. - - Autocomplete Description: Provide a brief description (e.g., `Get MiroTalk SFU meeting room`). - 3. Save the slash command and copy the generated token (YourMattermostToken). - */ - enabled: false, - serverUrl: 'YourMattermostServerUrl', - username: 'YourMattermostUsername', - password: 'YourMattermostPassword', - token: 'YourMattermostToken', - commands: [ - { - name: '/sfu', - message: 'Here is your meeting room:', + videoAI: { + enabled: process.env.VIDEOAI_ENABLED !== 'false', + basePath: 'https://api.heygen.com', + apiKey: process.env.VIDEOAI_API_KEY || '', + systemLimit: process.env.VIDEOAI_SYSTEM_LIMIT || 'You are a streaming avatar from MiroTalk SFU...', + }, + + /** + * Email Notification Configuration + * =============================== + * SMTP settings for system alerts and notifications + * + * Core Settings: + * ------------- + * - alert : Enable/disable email alerts [true/false] (default: false) + * - host : SMTP server address (default: 'smtp.gmail.com') + * - port : SMTP port (default: 587 for TLS) + * - username : SMTP auth username + * - password : SMTP auth password (store ONLY in .env) + * - sendTo : Recipient email for alerts + * + * Common Providers: + * ---------------- + * Gmail: + * - host: smtp.gmail.com + * - port: 587 + * + * Office365: + * - host: smtp.office365.com + * - port: 587 + * + * SendGrid: + * - host: smtp.sendgrid.net + * - port: 587 + */ + email: { + alert: process.env.EMAIL_ALERTS_ENABLED === 'true', + host: process.env.EMAIL_HOST || 'smtp.gmail.com', + port: parseInt(process.env.EMAIL_PORT) || 587, + username: process.env.EMAIL_USERNAME || 'your_username', + password: process.env.EMAIL_PASSWORD || 'your_password', + sendTo: process.env.EMAIL_SEND_TO || 'sfu.mirotalk@gmail.com', + }, + + /** + * Slack Integration Configuration + * ============================== + * Settings for Slack slash commands and interactivity + * + * Setup Instructions: + * ------------------ + * 1. Create a Slack app at https://api.slack.com/apps + * 2. Under "Basic Information" → "App Credentials": + * - Copy the Signing Secret + * 3. Enable "Interactivity & Shortcuts" and "Slash Commands" + * 4. Set Request URL to: https://your-domain.com/slack/commands + * + * Core Settings: + * ------------- + * - enabled : Enable/disable Slack integration [true/false] (default: false) + * - signingSecret : From Slack app credentials (store ONLY in .env) + * + */ + slack: { + enabled: process.env.SLACK_ENABLED === 'true', + signingSecret: process.env.SLACK_SIGNING_SECRET || '', + }, + + /** + * Mattermost Integration Configuration + * =================================== + * Settings for Mattermost slash commands and bot integration + * + * Setup Instructions: + * ------------------ + * 1. Go to Mattermost System Console → Integrations → Bot Accounts + * 2. Create a new bot account and copy: + * - Server URL (e.g., 'https://chat.yourdomain.com') + * - Access Token + * 3. For slash commands: + * - Navigate to Integrations → Slash Commands + * - Set Command: '/sfu' + * - Set Request URL: 'https://your-sfu-server.com/mattermost/commands' + * + * Core Settings: + * ------------- + * - enabled : Enable/disable integration [true/false] (default: false) + * - serverUrl : Mattermost server URL (include protocol) + * - token : Bot account access token (most secure option) + * - OR + * - username : Legacy auth username (less secure) + * - password : Legacy auth password (deprecated) + * + * Command Configuration: + * --------------------- + * - commands : Slash command definitions: + * - name : Command trigger (e.g., '/sfu') + * - message : Default response template + * + */ + mattermost: { + enabled: process.env.MATTERMOST_ENABLED === 'true', + serverUrl: process.env.MATTERMOST_SERVER_URL || '', + username: process.env.MATTERMOST_USERNAME || '', + password: process.env.MATTERMOST_PASSWORD || '', + token: process.env.MATTERMOST_TOKEN || '', + commands: [ + { + name: process.env.MATTERMOST_COMMAND_NAME || '/sfu', + message: process.env.MATTERMOST_DEFAULT_MESSAGE || 'Here is your meeting room:', + }, + ], + texts: [ + { + name: process.env.MATTERMOST_COMMAND_NAME || '/sfu', + message: process.env.MATTERMOST_DEFAULT_MESSAGE || 'Here is your meeting room:', + }, + ], + }, + + /** + * Discord Integration Configuration + * ================================ + * Settings for Discord bot and slash commands integration + * + * Setup Instructions: + * ------------------ + * 1. Create a Discord application at https://discord.com/developers/applications + * 2. Navigate to "Bot" section and: + * - Click "Add Bot" + * - Copy the bot token (DISCORD_TOKEN) + * 3. Under "OAuth2 → URL Generator": + * - Select "bot" and "applications.commands" scopes + * - Select required permissions (see below) + * 4. Invite bot to your server using generated URL + * + * Core Settings: + * ------------- + * - enabled : Enable/disable Discord bot [true/false] (default: false) + * - token : Bot token from Discord Developer Portal (store in .env) + * + * Command Configuration: + * --------------------- + * - commands : Slash command definitions: + * - name : Command trigger (e.g., '/sfu') + * - message : Response template + * - baseUrl : Meeting room base URL + * + */ + discord: { + enabled: process.env.DISCORD_ENABLED === 'true', + token: process.env.DISCORD_TOKEN || '', + commands: [ + { + name: process.env.DISCORD_COMMAND_NAME || '/sfu', + message: process.env.DISCORD_DEFAULT_MESSAGE || 'Here is your SFU meeting room:', + baseUrl: process.env.DISCORD_BASE_URL || 'https://sfu.mirotalk.com/join/', + }, + ], + }, + + /** + * Ngrok Tunnel Configuration + * ========================= + * Secure tunneling for local development and testing + * + * Setup Instructions: + * ------------------ + * 1. Sign up at https://dashboard.ngrok.com/signup + * 2. Get your auth token from: + * https://dashboard.ngrok.com/get-started/your-authtoken + * 3. For reserved domains/subdomains: + * - Upgrade to paid plan if needed + * - Reserve at https://dashboard.ngrok.com/cloud-edge/domains + * + * Core Settings: + * ------------- + * - enabled : Enable/disable Ngrok tunneling [true/false] (default: false) + * - authToken : Your Ngrok authentication token (from dashboard) + */ + ngrok: { + enabled: process.env.NGROK_ENABLED === 'true', + authToken: process.env.NGROK_AUTH_TOKEN || '', + }, + + /** + * Sentry Error Tracking Configuration + * ================================== + * Real-time error monitoring and performance tracking + * + * Setup Instructions: + * ------------------ + * 1. Create a project at https://sentry.io/signup/ + * 2. Get your DSN from: + * Project Settings → Client Keys (DSN) + * 3. Configure alert rules and integrations as needed + * + * Core Settings: + * ------------- + * enabled : Enable/disable Sentry [true/false] (default: false) + * DSN : Data Source Name (from Sentry dashboard) + * tracesSampleRate : Percentage of transactions to capture (0.0-1.0) + * + * Performance Tuning: + * ------------------ + * - Production : 0.1-0.2 (10-20% of transactions) + * - Staging : 0.5-1.0 + * - Development : 0.0 (disable performance tracking) + * + */ + sentry: { + enabled: process.env.SENTRY_ENABLED === 'true', + DSN: process.env.SENTRY_DSN || '', + tracesSampleRate: Math.min(Math.max(parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE) || 0.5, 0), 1), + }, + + /** + * Webhook Configuration Settings + * ============================= + * Controls the webhook notification system for sending event data to external services. + * + * Core Settings: + * --------------------- + * - enabled: Turns webhook notifications on/off + * - url: The endpoint URL where webhook payloads will be sent in JSON format + * + * Implementation Guide: + * -------------------- + * - For complete implementation examples, refer to: + * - Project demo: /mirotalksfu/webhook/ folder + */ + webhook: { + enabled: process.env.WEBHOOK_ENABLED === 'true', + url: process.env.WEBHOOK_URL || 'https://your-site.com/webhook-endpoint', + }, + + /** + * IP Geolocation Service Configuration + * =================================== + * Enables lookup of geographical information based on IP addresses using the GeoJS.io API. + * + * Core Settings: + * --------------------- + * - enabled: Enable/disable the IP lookup functionality [true/false] default false + * + * Service Details: + * -------------- + * - Uses GeoJS.io free API service (https://www.geojs.io/) + * - Returns JSON data containing: + * - Country, region, city + * - Latitude/longitude + * - Timezone and organization + * - Rate limits: 60 requests/minute (free tier) + */ + IPLookup: { + enabled: process.env.IP_LOOKUP_ENABLED === 'true', + getEndpoint(ip) { + return `https://get.geojs.io/v1/ip/geo/${ip}.json`; }, - //.... - ], - texts: [ - { - name: '/sfu', - message: 'Here is your meeting room:', - }, - //.... - ], - }, - slack: { - /* - Slack - 1. Goto https://api.slack.com/apps/ - 2. Create your app - 3. On Settings - Basic Information - App Credentials, chose your Signing Secret - 4. Create a Slash Commands and put as Request URL: https://your.domain.name/slack - */ - enabled: false, - signingSecret: '', - }, - discord: { - /* - Discord - 1. Go to the Discord Developer Portal: https://discord.com/developers/. - 2. Create a new application and name it whatever you like. - 3. Under the Bot section, click Add Bot and confirm. - 4. Copy your bot token (this will be used later). - 5. Under OAuth2 -> URL Generator, select bot scope, and under Bot Permissions, select the permissions you need (e.g., Send Messages and Read Messages). - 6. Copy the generated invite URL, open it in a browser, and invite the bot to your Discord server. - 7. Add the Bot in the Server channel permissions - 8. Type /sfu (commands.name) in the channel, the response will return a URL for the meeting - */ - enabled: false, - token: '', - commands: [ - { - name: '/sfu', - message: 'Here is your SFU meeting room:', - baseUrl: 'https://sfu.mirotalk.com/join/', - }, - ], - }, - IPLookup: { - /* - GeoJS - https://www.geojs.io/docs/v1/endpoints/geo/ - */ - enabled: false, - getEndpoint(ip) { - return `https://get.geojs.io/v1/ip/geo/${ip}.json`; }, }, - survey: { - /* - QuestionPro - 1. GoTo https://www.questionpro.com/ - 2. Create your account - 3. Create your custom survey - */ - enabled: false, - url: '', - }, - redirect: { - /* - Redirect URL on leave room - Upon leaving the room, users who either opt out of providing feedback or if the survey is disabled - will be redirected to a specified URL. If enabled false the default '/newroom' URL will be used. - */ - enabled: false, - url: '', - }, + + // ============================================== + // 7. UI/UX Customization + // ============================================== + ui: { - /* - Customize your MiroTalk instance - Branding and customizations require a license: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 - */ + /** + * Branding & Appearance + * --------------------- + * - app: Application-specific branding + * - site: Website metadata and SEO + * - meta: HTML meta tags + * - og: OpenGraph social media tags + */ brand: { app: { - language: 'en', // https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes - name: 'MiroTalk SFU', - title: 'MiroTalk SFU
Free browser based Real-time video calls.
Simple, Secure, Fast.', + language: process.env.UI_LANGUAGE || 'en', + name: process.env.APP_NAME || 'MiroTalk SFU', + title: + process.env.APP_TITLE || + 'MiroTalk SFU
Free browser based Real-time video calls.
Simple, Secure, Fast.', description: - 'Start your next video call with a single click. No download, plug-in, or login is required. Just get straight to talking, messaging, and sharing your screen.', - joinDescription: 'Pick a room name.
How about this one?', - joinButtonLabel: 'JOIN ROOM', - joinLastLabel: 'Your recent room:', + process.env.APP_DESCRIPTION || + 'Start your next video call with a single click. No download, plug-in, or login is required.', + joinDescription: process.env.JOIN_DESCRIPTION || 'Pick a room name.
How about this one?', + joinButtonLabel: process.env.JOIN_BUTTON_LABEL || 'JOIN ROOM', + joinLastLabel: process.env.JOIN_LAST_LABEL || 'Your recent room:', }, site: { - title: 'MiroTalk SFU, Free Video Calls, Messaging and Screen Sharing', - icon: '../images/logo.svg', - appleTouchIcon: '../images/logo.svg', - newRoomTitle: 'Pick name.
Share URL.
Start conference.', + title: process.env.SITE_TITLE || 'MiroTalk SFU, Free Video Calls, Messaging and Screen Sharing', + icon: process.env.SITE_ICON_PATH || '../images/logo.svg', + appleTouchIcon: process.env.APPLE_TOUCH_ICON_PATH || '../images/logo.svg', + newRoomTitle: process.env.NEW_ROOM_TITLE || 'Pick name.
Share URL.
Start conference.', newRoomDescription: - "Each room has its disposable URL. Just pick a room name and share your custom URL. It's that easy.", + process.env.NEW_ROOM_DESC || 'Each room has its disposable URL. Just pick a name and share.', }, meta: { description: - 'MiroTalk SFU powered by WebRTC and mediasoup, Real-time Simple Secure Fast video calls, messaging and screen sharing capabilities in the browser.', - keywords: - 'webrtc, miro, mediasoup, mediasoup-client, self hosted, voip, sip, real-time communications, chat, messaging, meet, webrtc stun, webrtc turn, webrtc p2p, webrtc sfu, video meeting, video chat, video conference, multi video chat, multi video conference, peer to peer, p2p, sfu, rtc, alternative to, zoom, microsoft teams, google meet, jitsi, meeting', + process.env.META_DESCRIPTION || + 'MiroTalk SFU powered by WebRTC and mediasoup for real-time video communications.', + keywords: process.env.META_KEYWORDS || 'webrtc, video calls, conference, screen sharing, mirotalk, sfu', }, og: { - type: 'app-webrtc', - siteName: 'MiroTalk SFU', - title: 'Click the link to make a call.', - description: 'MiroTalk SFU calling provides real-time video calls, messaging and screen sharing.', - image: 'https://sfu.mirotalk.com/images/mirotalksfu.png', - url: 'https://sfu.mirotalk.com', + type: process.env.OG_TYPE || 'app-webrtc', + siteName: process.env.OG_SITE_NAME || 'MiroTalk SFU', + title: process.env.OG_TITLE || 'Click the link to make a call.', + description: + process.env.OG_DESCRIPTION || 'MiroTalk SFU provides real-time video calls and screen sharing.', + image: process.env.OG_IMAGE_URL || 'https://sfu.mirotalk.com/images/mirotalksfu.png', + url: process.env.OG_URL || 'https://sfu.mirotalk.com', }, html: { - features: true, - teams: true, - tryEasier: true, - poweredBy: true, - sponsors: true, - advertisers: true, - footer: true, + features: process.env.SHOW_FEATURES !== 'false', + teams: process.env.SHOW_TEAMS !== 'false', + tryEasier: process.env.SHOW_TRY_EASIER !== 'false', + poweredBy: process.env.SHOW_POWERED_BY !== 'false', + sponsors: process.env.SHOW_SPONSORS !== 'false', + advertisers: process.env.SHOW_ADVERTISERS !== 'false', + footer: process.env.SHOW_FOOTER !== 'false', }, about: { - imageUrl: '../images/mirotalk-logo.gif', + imageUrl: process.env.ABOUT_IMAGE_URL || '../images/mirotalk-logo.gif', title: `WebRTC SFU v${packageJson.version}`, html: ` - -


- Author: - - Miroslav Pejic - -

- Email: - - miroslav.pejic.85@gmail.com - -

-
- © 2025 MiroTalk SFU, all rights reserved -
+ +
+
+ ${process.env.AUTHOR_LABEL || 'Author'}: + + ${process.env.AUTHOR_NAME || 'Miroslav Pejic'} + +
+ ${process.env.EMAIL_LABEL || 'Email'}: + + ${process.env.CONTACT_EMAIL || 'miroslav.pejic.85@gmail.com'} + +
+ + © ${new Date().getFullYear()} ${process.env.COPYRIGHT_TEXT || 'MiroTalk SFU, all rights reserved'} + +
`, }, - //... }, - /* - Toggle the visibility of specific HTML elements within the room - */ + + /** + * UI Button Configuration + * --------------------- + * Organized by component/functionality area + */ buttons: { + // Main control buttons visible in the UI main: { - shareButton: true, // presenter - hideMeButton: true, - startAudioButton: true, - startVideoButton: true, - startScreenButton: true, - swapCameraButton: true, - chatButton: true, - pollButton: true, - editorButton: true, - raiseHandButton: true, - transcriptionButton: true, - whiteboardButton: true, - documentPiPButton: true, - snapshotRoomButton: true, - emojiRoomButton: true, - settingsButton: true, - aboutButton: true, - exitButton: true, + shareButton: process.env.SHOW_SHARE_BUTTON !== 'false', + hideMeButton: process.env.SHOW_HIDE_ME !== 'false', + startAudioButton: process.env.SHOW_AUDIO_BUTTON !== 'false', + startVideoButton: process.env.SHOW_VIDEO_BUTTON !== 'false', + startScreenButton: process.env.SHOW_SCREEN_BUTTON !== 'false', + swapCameraButton: process.env.SHOW_SWAP_CAMERA !== 'false', + chatButton: process.env.SHOW_CHAT_BUTTON !== 'false', + pollButton: process.env.SHOW_POLL_BUTTON !== 'false', + editorButton: process.env.SHOW_EDITOR_BUTTON !== 'false', + raiseHandButton: process.env.SHOW_RAISE_HAND !== 'false', + transcriptionButton: process.env.SHOW_TRANSCRIPTION !== 'false', + whiteboardButton: process.env.SHOW_WHITEBOARD !== 'false', + documentPiPButton: process.env.SHOW_DOCUMENT_PIP !== 'false', + snapshotRoomButton: process.env.SHOW_SNAPSHOT !== 'false', + emojiRoomButton: process.env.SHOW_EMOJI !== 'false', + settingsButton: process.env.SHOW_SETTINGS !== 'false', + aboutButton: process.env.SHOW_ABOUT !== 'false', + exitButton: process.env.SHOW_EXIT_BUTTON !== 'false', }, + + // Settings panel buttons and options settings: { - fileSharing: true, - lockRoomButton: true, // presenter - unlockRoomButton: true, // presenter - broadcastingButton: true, // presenter - lobbyButton: true, // presenter - sendEmailInvitation: true, // presenter - micOptionsButton: true, // presenter - tabRTMPStreamingBtn: true, // presenter - tabModerator: true, // presenter - tabRecording: true, - host_only_recording: true, // presenter - pushToTalk: true, - keyboardShortcuts: true, - virtualBackground: true, + fileSharing: process.env.ENABLE_FILE_SHARING !== 'false', + lockRoomButton: process.env.SHOW_LOCK_ROOM !== 'false', + unlockRoomButton: process.env.SHOW_UNLOCK_ROOM !== 'false', + broadcastingButton: process.env.SHOW_BROADCASTING !== 'false', + lobbyButton: process.env.SHOW_LOBBY !== 'false', + sendEmailInvitation: process.env.SHOW_EMAIL_INVITE !== 'false', + micOptionsButton: process.env.SHOW_MIC_OPTIONS !== 'false', + tabRTMPStreamingBtn: process.env.SHOW_RTMP_TAB !== 'false', + tabModerator: process.env.SHOW_MODERATOR_TAB !== 'false', + tabRecording: process.env.SHOW_RECORDING_TAB !== 'false', + host_only_recording: process.env.HOST_ONLY_RECORDING !== 'false', + pushToTalk: process.env.ENABLE_PUSH_TO_TALK !== 'false', + keyboardShortcuts: process.env.SHOW_KEYBOARD_SHORTCUTS !== 'false', + virtualBackground: process.env.SHOW_VIRTUAL_BACKGROUND !== 'false', }, + + // Video controls for producer (local user) producerVideo: { - videoPictureInPicture: true, - videoMirrorButton: true, - fullScreenButton: true, - snapShotButton: true, - muteAudioButton: true, - videoPrivacyButton: true, - audioVolumeInput: true, + videoPictureInPicture: process.env.ENABLE_PIP !== 'false', + videoMirrorButton: process.env.SHOW_MIRROR_BUTTON !== 'false', + fullScreenButton: process.env.SHOW_FULLSCREEN !== 'false', + snapShotButton: process.env.SHOW_SNAPSHOT_BUTTON !== 'false', + muteAudioButton: process.env.SHOW_MUTE_AUDIO !== 'false', + videoPrivacyButton: process.env.SHOW_PRIVACY_TOGGLE !== 'false', + audioVolumeInput: process.env.SHOW_VOLUME_CONTROL !== 'false', }, + + // Video controls for consumer (remote users) consumerVideo: { - videoPictureInPicture: true, - videoMirrorButton: true, - fullScreenButton: true, - snapShotButton: true, - focusVideoButton: true, - sendMessageButton: true, - sendFileButton: true, - sendVideoButton: true, - muteVideoButton: true, - muteAudioButton: true, - audioVolumeInput: true, - geolocationButton: true, // Presenter - banButton: true, // presenter - ejectButton: true, // presenter + videoPictureInPicture: process.env.ENABLE_PIP !== 'false', + videoMirrorButton: process.env.SHOW_MIRROR_BUTTON !== 'false', + fullScreenButton: process.env.SHOW_FULLSCREEN !== 'false', + snapShotButton: process.env.SHOW_SNAPSHOT_BUTTON !== 'false', + focusVideoButton: process.env.SHOW_FOCUS_BUTTON !== 'false', + sendMessageButton: process.env.SHOW_SEND_MESSAGE !== 'false', + sendFileButton: process.env.SHOW_SEND_FILE !== 'false', + sendVideoButton: process.env.SHOW_SEND_VIDEO !== 'false', + muteVideoButton: process.env.SHOW_MUTE_VIDEO !== 'false', + muteAudioButton: process.env.SHOW_MUTE_AUDIO !== 'false', + audioVolumeInput: process.env.SHOW_VOLUME_CONTROL !== 'false', + geolocationButton: process.env.SHOW_GEO_LOCATION !== 'false', + banButton: process.env.SHOW_BAN_BUTTON !== 'false', + ejectButton: process.env.SHOW_EJECT_BUTTON !== 'false', }, + + // Controls when video is off videoOff: { - sendMessageButton: true, - sendFileButton: true, - sendVideoButton: true, - muteAudioButton: true, - audioVolumeInput: true, - geolocationButton: true, // Presenter - banButton: true, // presenter - ejectButton: true, // presenter + sendMessageButton: process.env.SHOW_SEND_MESSAGE !== 'false', + sendFileButton: process.env.SHOW_SEND_FILE !== 'false', + sendVideoButton: process.env.SHOW_SEND_VIDEO !== 'false', + muteAudioButton: process.env.SHOW_MUTE_AUDIO !== 'false', + audioVolumeInput: process.env.SHOW_VOLUME_CONTROL !== 'false', + geolocationButton: process.env.SHOW_GEO_LOCATION !== 'false', + banButton: process.env.SHOW_BAN_BUTTON !== 'false', + ejectButton: process.env.SHOW_EJECT_BUTTON !== 'false', }, + + // Chat interface controls chat: { - chatPinButton: true, - chatMaxButton: true, - chatSaveButton: true, - chatEmojiButton: true, - chatMarkdownButton: true, - chatSpeechStartButton: true, - chatGPT: true, + chatPinButton: process.env.SHOW_CHAT_PIN !== 'false', + chatMaxButton: process.env.SHOW_CHAT_MAXIMIZE !== 'false', + chatSaveButton: process.env.SHOW_CHAT_SAVE !== 'false', + chatEmojiButton: process.env.SHOW_CHAT_EMOJI !== 'false', + chatMarkdownButton: process.env.SHOW_CHAT_MARKDOWN !== 'false', + chatSpeechStartButton: process.env.SHOW_CHAT_SPEECH !== 'false', + chatGPT: process.env.ENABLE_CHAT_GPT !== 'false', }, + + // Poll interface controls poll: { - pollPinButton: true, - pollMaxButton: true, - pollSaveButton: true, + pollPinButton: process.env.SHOW_POLL_PIN !== 'false', + pollMaxButton: process.env.SHOW_POLL_MAXIMIZE !== 'false', + pollSaveButton: process.env.SHOW_POLL_SAVE !== 'false', }, + + // Participants list controls participantsList: { - saveInfoButton: true, // presenter - sendFileAllButton: true, // presenter - ejectAllButton: true, // presenter - sendFileButton: true, // presenter & guests - geoLocationButton: true, // presenter - banButton: true, // presenter - ejectButton: true, // presenter + saveInfoButton: process.env.SHOW_SAVE_INFO !== 'false', + sendFileAllButton: process.env.SHOW_SEND_FILE_ALL !== 'false', + ejectAllButton: process.env.SHOW_EJECT_ALL !== 'false', + sendFileButton: process.env.SHOW_SEND_FILE !== 'false', + geoLocationButton: process.env.SHOW_GEO_LOCATION !== 'false', + banButton: process.env.SHOW_BAN_BUTTON !== 'false', + ejectButton: process.env.SHOW_EJECT_BUTTON !== 'false', }, + + // Whiteboard controls whiteboard: { - whiteboardLockButton: true, // presenter + whiteboardLockButton: process.env.SHOW_WB_LOCK !== 'false', }, - //... }, }, - stats: { - /* - Umami: https://github.com/umami-software/umami - We use our Self-hosted Umami to track aggregated usage statistics in order to improve our service. - */ - enabled: true, - src: 'https://stats.mirotalk.com/script.js', - id: '41d26670-f275-45bb-af82-3ce91fe57756', + + // ============================================== + // 8. Feature Flags + // ============================================== + + features: { + /** + * Survey Configuration (QuestionPro) + * ================================= + * Settings for user feedback and survey integration + * + * Setup Instructions: + * ------------------ + * 1. Sign up at https://www.questionpro.com/ + * 2. Create survey: + * - Use template or custom questions + * - Configure survey logic and branching + * 3. Get survey URL: + * - Publish survey + * - Copy "Collect Responses" link + */ + survey: { + enabled: process.env.SURVEY_ENABLED === 'true', + url: process.env.SURVEY_URL || '', + }, + + /** + * Post-Call Redirect + * --------------------- + * - enabled: Redirect after call ends + * - url: Redirect destination URL + */ + redirect: { + enabled: process.env.REDIRECT_ENABLED === 'true', + url: process.env.REDIRECT_URL || '', + }, + + /** + * Usage Statistics Configuration (Umami) + * ===================================== + * Privacy-focused analytics tracking for service improvement + * + * Setup Instructions: + * ------------------ + * 1. Self-host Umami or use cloud version: + * - GitHub: https://github.com/umami-software/umami + * - Official Docs: https://umami.is/docs + * 2. Create website entry in Umami dashboard + * 3. Obtain tracking script URL and website ID + * + * Privacy & Security: + * ------------------ + * - No cookies used (GDPR compliant) + * - No persistent user tracking + * - All data aggregated and anonymized + * - Self-hosted option keeps data in your infrastructure + * + * Core Settings: + * ------------- + * - enabled : Enable/disable analytics [true/false] (default: true) + * - src : Umami tracking script URL + * - id : Your website ID from Umami + */ + stats: { + enabled: process.env.STATS_ENABLED !== 'false', + src: process.env.STATS_SRC || 'https://stats.mirotalk.com/script.js', + id: process.env.STATS_ID || '41d26670-f275-45bb-af82-3ce91fe57756', + }, }, + + // ============================================== + // 9. Mediasoup (WebRTC) Configuration + // ============================================== + + /** + * Mediasoup Integration Resources + * ============================== + * Core WebRTC components powering MiroTalk SFU + * + * Essential Links: + * --------------- + * 🌐 Website : https://mediasoup.org + * + * 📚 Documentation: + * - Client API : https://mediasoup.org/documentation/v3/mediasoup-client/api/ + * - Server API : https://mediasoup.org/documentation/v3/mediasoup/api/ + * - Protocols : https://mediasoup.org/documentation/v3/mediasoup/rtp-parameters-and-capabilities/ + * + * 🔧 Key Components: + * - Router : Manages RTP streams + * - Transport : Network connection handler + * - Producer : Media sender + * - Consumer : Media receiver + */ mediasoup: { - // Worker settings + /** + * Worker Configuration + * -------------------- + * - numWorkers: Number of mediasoup workers + * - worker: Worker-specific settings + */ numWorkers: NUM_WORKERS, worker: { rtcMinPort: RTC_MIN_PORT, rtcMaxPort: RTC_MAX_PORT, - disableLiburing: false, // https://github.com/axboe/liburing - logLevel: 'error', + disableLiburing: false, + logLevel: process.env.MEDIASOUP_LOG_LEVEL || 'error', logTags: ['info', 'ice', 'dtls', 'rtp', 'srtp', 'rtcp', 'rtx', 'bwe', 'score', 'simulcast', 'svc', 'sctp'], }, - // Router settings + + /** + * Router Configuration + * -------------------- + * - Media codecs and capabilities + */ router: { audioLevelObserverEnabled: true, activeSpeakerObserverEnabled: false, @@ -654,7 +1134,7 @@ module.exports = { mimeType: 'video/VP9', clockRate: 90000, parameters: { - 'profile-id': 0, // Default profile for wider compatibility + 'profile-id': 0, 'x-google-start-bitrate': 1000, }, }, @@ -663,7 +1143,7 @@ module.exports = { mimeType: 'video/VP9', clockRate: 90000, parameters: { - 'profile-id': 2, // High profile for modern devices + 'profile-id': 2, 'x-google-start-bitrate': 1000, }, }, @@ -673,7 +1153,7 @@ module.exports = { clockRate: 90000, parameters: { 'packetization-mode': 1, - 'profile-level-id': '42e01f', // Baseline profile for compatibility + 'profile-level-id': '42e01f', 'level-asymmetry-allowed': 1, 'x-google-start-bitrate': 1000, }, @@ -684,19 +1164,23 @@ module.exports = { clockRate: 90000, parameters: { 'packetization-mode': 1, - 'profile-level-id': '4d0032', // High profile for modern devices + 'profile-level-id': '4d0032', 'level-asymmetry-allowed': 1, 'x-google-start-bitrate': 1000, }, }, ], }, - // WebRtcServerOptions - webRtcServerActive: false, + + /** + * WebRTC Server Configuration + * --------------------------- + * - Active: Enable/disable WebRTC server + * - Options: Listening interfaces and ports + */ + webRtcServerActive: process.env.SFU_SERVER || false, webRtcServerOptions: { listenInfos: [ - // { protocol: 'udp', ip: LISTEN_IP, announcedAddress: IPv4, port: RTC_MIN_PORT }, - // { protocol: 'tcp', ip: LISTEN_IP, announcedAddress: IPv4, port: RTC_MIN_PORT }, { protocol: 'udp', ip: LISTEN_IP, @@ -711,11 +1195,14 @@ module.exports = { }, ], }, - // WebRtcTransportOptions + + /** + * WebRTC Transport Configuration + * ------------------------------ + * - Network and bandwidth settings + */ webRtcTransport: { listenInfos: [ - // { protocol: 'udp', ip: IPv4, portRange: { min: RTC_MIN_PORT, max: RTC_MAX_PORT } }, - // { protocol: 'tcp', ip: IPv4, portRange: { min: RTC_MIN_PORT, max: RTC_MAX_PORT } }, { protocol: 'udp', ip: LISTEN_IP, @@ -737,207 +1224,120 @@ module.exports = { }, }; +// ============================================== +// Helper Functions +// ============================================== + /** - * Determines the appropriate IPv4 address based on environment and configuration - * Priority order: - * 1. Explicitly configured PUBLIC_IP (if set) - * 2. Environment-specific detection - * - * @returns {string} The selected IPv4 address + * Get IPv4 Address + * ---------------- + * - Prioritizes PUBLIC_IP if set + * - Falls back to local IP detection */ function getIPv4() { - // Highest priority: use explicitly configured IP if available if (PUBLIC_IP) return PUBLIC_IP; switch (ENVIRONMENT) { case 'development': return IS_DOCKER ? '127.0.0.1' : getLocalIPv4(); - case 'production': - /* - * Production Environment Notes: - * ---------------------------------- - * 1. Recommended: Explicitly set your public IPv4 address - * - For cloud providers (AWS/Azure/GCP): - * - AWS: Use Elastic IP associated with your EC2 instance - * - GCP: Use static external IP assigned to your VM - * - Azure: Use public IP address resource - * - * 2. Auto-detection Fallback: - * - Will attempt to detect public IP if not manually configured - * - Not recommended for production as it may cause: - * - DNS resolution delays during startup - * - Inconsistent behavior if detection services are unavailable - * - * 3. For containerized production: - * - Set via environment variable - * - Use cloud provider metadata service when available - * (e.g., AWS EC2 metadata service) - */ return PUBLIC_IP; - default: - // Fallback for unknown environments - use local IP detection return getLocalIPv4(); } } /** - * Retrieves the most suitable local IPv4 address by: - * 1. Checking platform-specific priority interfaces first (Ethernet/Wi-Fi) - * 2. Falling back to scanning all non-virtual interfaces - * 3. Excluding APIPA (169.254.x.x) and internal/virtual addresses - * - * @returns {string} Valid IPv4 address or '0.0.0.0' if none found + * Detect Local IPv4 Address + * ------------------------- + * - Handles different OS network interfaces + * - Filters out virtual/docker interfaces */ function getLocalIPv4() { const ifaces = os.networkInterfaces(); const platform = os.platform(); - // ===== 1. Platform-Specific Configuration ===== - /** - * Interface priority list (ordered by most preferred first). - * Windows: Physical Ethernet/Wi-Fi before virtual adapters - * macOS: Built-in en0 (Ethernet/Wi-Fi) before secondary interfaces - * Linux: Standard eth0/wlan0 before containers/virtual NICs - */ const PRIORITY_CONFIG = { - win32: [ - { name: 'Ethernet', type: 'wired' }, // Primary wired - { name: 'Wi-Fi', type: 'wireless' }, // Primary wireless - { name: 'Local Area Connection', type: 'wired' }, // Legacy wired - ], - darwin: [ - { name: 'en0', type: 'wired/wireless' }, // macOS primary - { name: 'en1', type: 'secondary' }, // macOS secondary - ], - linux: [ - { name: 'eth0', type: 'wired' }, // Linux primary Ethernet - { name: 'wlan0', type: 'wireless' }, // Linux primary wireless - ], + win32: [{ name: 'Ethernet' }, { name: 'Wi-Fi' }, { name: 'Local Area Connection' }], + darwin: [{ name: 'en0' }, { name: 'en1' }], + linux: [{ name: 'eth0' }, { name: 'wlan0' }], }; - /** - * Virtual interfaces to exclude (case-insensitive partial matches): - * - Common: Docker, VPNs, loopback - * - Windows: Hyper-V, VMware, Bluetooth - * - macOS: AWDL (Apple Wireless Direct Link), virtualization - * - Linux: Kubernetes, libvirt bridges - */ const VIRTUAL_INTERFACES = { - all: ['docker', 'veth', 'tun', 'lo'], // Cross-platform virtual NICs + all: ['docker', 'veth', 'tun', 'lo'], win32: ['Virtual', 'vEthernet', 'Teredo', 'Bluetooth'], darwin: ['awdl', 'bridge', 'utun'], linux: ['virbr', 'kube', 'cni'], }; - // ===== 2. Priority Interface Check ===== const platformPriorities = PRIORITY_CONFIG[platform] || []; const virtualExcludes = [...VIRTUAL_INTERFACES.all, ...(VIRTUAL_INTERFACES[platform] || [])]; + // Check priority interfaces first for (const { name: ifName } of platformPriorities) { - // Windows: Match interface names containing priority string (e.g., "Ethernet 2") - // Unix: Match exact interface names (eth0, wlan0) const matchingIfaces = platform === 'win32' ? Object.keys(ifaces).filter((k) => k.includes(ifName)) : [ifName]; - for (const interfaceName of matchingIfaces) { const addr = findValidAddress(ifaces[interfaceName]); - if (addr) { - return addr; - } + if (addr) return addr; } } - // ===== 3. Fallback: Full Interface Scan ===== + // Fallback to scanning all non-virtual interfaces const fallbackAddress = scanAllInterfaces(ifaces, virtualExcludes); if (fallbackAddress) return fallbackAddress; - // ===== 4. Final Fallback ===== return '0.0.0.0'; } /** - * Scans all non-virtual interfaces for valid IPv4 addresses - * @param {Object} ifaces - Network interfaces from os.networkInterfaces() - * @param {string[]} excludes - Virtual interface prefixes to ignore - * @returns {string|null} First valid IPv4 address found + * Scan All Network Interfaces + * --------------------------- + * - Checks all interfaces excluding virtual ones */ function scanAllInterfaces(ifaces, excludes) { for (const [name, addresses] of Object.entries(ifaces)) { - // Skip interfaces with excluded prefixes (case-insensitive) if (excludes.some((ex) => name.toLowerCase().includes(ex.toLowerCase()))) { continue; } const addr = findValidAddress(addresses); - if (addr) { - console.log(`[Fallback] Using ${name}: ${addr}`); - return addr; - } + if (addr) return addr; } return null; } /** - * Validates a network address as: - * - IPv4 family - * - Non-internal (not loopback) - * - Non-APIPA (not 169.254.x.x) - * @param {Object[]} addresses - Network interface addresses - * @returns {string|undefined} Valid address or undefined + * Find Valid Network Address + * -------------------------- + * - Filters out internal and link-local addresses */ function findValidAddress(addresses) { - return addresses?.find( - (addr) => addr.family === 'IPv4' && !addr.internal && !addr.address.startsWith('169.254.'), // Exclude APIPA - )?.address; + return addresses?.find((addr) => addr.family === 'IPv4' && !addr.internal && !addr.address.startsWith('169.254.')) + ?.address; } /** - * Finds the appropriate FFmpeg executable path for the current platform - * - * @param {string} platform - The Node.js process.platform value (darwin, linux, win32) - * @returns {string} The first valid FFmpeg path found, or the default path for the platform - * - * @description - * This function handles FFmpeg path detection across different operating systems. - * It checks common installation locations and returns the first accessible path. - * If no valid path is found, it returns the first default path for the platform. + * Get FFmpeg Path + * --------------- + * - Checks common installation locations + * - Platform-specific paths */ function getFFmpegPath(platform) { - // Common FFmpeg installation paths organized by platform const paths = { - // macOS (Homebrew default locations) - darwin: [ - '/usr/local/bin/ffmpeg', // Traditional Homebrew location - '/opt/homebrew/bin/ffmpeg', // Apple Silicon Homebrew location - ], - // Linux (common package manager locations) - linux: [ - '/usr/bin/ffmpeg', // System package manager installation - '/usr/local/bin/ffmpeg', // Manual compilation default - ], - // Windows (common installation paths) - win32: [ - 'C:\\ffmpeg\\bin\\ffmpeg.exe', // Standard FFmpeg Windows installation - 'C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe', // Program Files installation - ], + darwin: ['/usr/local/bin/ffmpeg', '/opt/homebrew/bin/ffmpeg'], + linux: ['/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg'], + win32: ['C:\\ffmpeg\\bin\\ffmpeg.exe', 'C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe'], }; - // Get platform-specific paths or default to Linux paths if platform not recognized const platformPaths = paths[platform] || ['/usr/bin/ffmpeg']; - // Try to find the first existing accessible path for (const path of platformPaths) { try { - // Check if the path exists and is accessible fs.accessSync(path); return path; } catch (e) { - // Path not accessible, try next one continue; } } - // If no path was accessible, return the first default path for the platform - // This allows the calling code to handle the "not found" case with proper error messaging return platformPaths[0]; } diff --git a/app/src/lib/nodemailer.js b/app/src/lib/nodemailer.js index d0c28a96..5c285a26 100644 --- a/app/src/lib/nodemailer.js +++ b/app/src/lib/nodemailer.js @@ -9,12 +9,13 @@ const log = new Logger('NodeMailer'); // EMAIL CONFIG // #################################################### -const EMAIL_HOST = config.email ? config.email.host : false; -const EMAIL_PORT = config.email ? config.email.port : false; -const EMAIL_USERNAME = config.email ? config.email.username : false; -const EMAIL_PASSWORD = config.email ? config.email.password : false; -const EMAIL_SEND_TO = config.email ? config.email.sendTo : false; -const EMAIL_ALERT = config.email ? config.email.alert : false; +const emailConfig = config.integrations?.email || {}; +const EMAIL_ALERT = emailConfig.alert || false; +const EMAIL_HOST = emailConfig.host || false; +const EMAIL_PORT = emailConfig.port || false; +const EMAIL_USERNAME = emailConfig.username || false; +const EMAIL_PASSWORD = emailConfig.password || false; +const EMAIL_SEND_TO = emailConfig.sendTo || false; if (EMAIL_ALERT && EMAIL_HOST && EMAIL_PORT && EMAIL_USERNAME && EMAIL_PASSWORD && EMAIL_SEND_TO) { log.info('Email', { diff --git a/app/src/middleware/IpWhitelist.js b/app/src/middleware/IpWhitelist.js index 770e8b17..d32f8f89 100644 --- a/app/src/middleware/IpWhitelist.js +++ b/app/src/middleware/IpWhitelist.js @@ -4,11 +4,10 @@ const config = require('../config'); const Logger = require('../Logger'); const log = new Logger('RestrictAccessByIP'); -const IpWhitelistEnabled = config.middleware ? config.middleware.IpWhitelist.enabled : false; -const allowedIPs = config.middleware ? config.middleware.IpWhitelist.allowed : []; +const { enabled = false, allowedIPs = [] } = config?.security?.middleware?.IpWhitelist || {}; const restrictAccessByIP = (req, res, next) => { - if (!IpWhitelistEnabled) return next(); + if (!enabled) return next(); // const clientIP = req.headers['x-forwarded-for'] || req.headers['X-Forwarded-For'] || req.socket.remoteAddress || req.ip; diff --git a/install.sh b/install.sh index 6b1b20a5..96e7a24a 100755 --- a/install.sh +++ b/install.sh @@ -96,6 +96,7 @@ if [ "$answer" != "${answer#[Yy]}" ] ;then fi CONFIG=app/src/config.js +ENV=.env if ! [ -f "$CONFIG" ]; then @@ -103,6 +104,7 @@ if ! [ -f "$CONFIG" ]; then cp app/src/config.template.js $CONFIG + cp .env.template $ENV fi printf 'Use docker (y/n)? ' diff --git a/package.json b/package.json index 590eb6ea..e14cdb4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalksfu", - "version": "1.7.99", + "version": "1.8.00", "description": "WebRTC SFU browser-based video calls", "main": "Server.js", "scripts": { @@ -59,7 +59,7 @@ "dependencies": { "@mattermost/client": "10.6.0", "@ngrok/ngrok": "1.4.1", - "@sentry/node": "^9.10.0", + "@sentry/node": "^9.10.1", "axios": "^1.8.4", "chokidar": "^4.0.3", "colors": "1.4.0", diff --git a/public/js/Brand.js b/public/js/Brand.js index 2989e888..d384fde3 100644 --- a/public/js/Brand.js +++ b/public/js/Brand.js @@ -64,7 +64,7 @@ let BRAND = { }, about: { imageUrl: '../images/mirotalk-logo.gif', - title: 'WebRTC SFU v1.7.99', + title: 'WebRTC SFU v1.8.00', html: `