mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 13:59:40 +00:00
feat(jitsi): Add Jitsi Meet video conferencing module
fix(live-usb): Improve UEFI boot compatibility Jitsi module: - LXC-based Jitsi Meet server - Prosody + Jicofo + JVB stack - Authentication support - Let's Encrypt SSL - Recording with Jibri Live USB UEFI fixes: - Add fallback to pre-built signed EFI - Copy grubx64.efi for compatibility - Copy grub.cfg to EFI/BOOT - Add grub-efi-amd64-signed and shim-signed to CI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d123ad49e8
commit
bb5c30efbd
4
.github/workflows/build-live-usb.yml
vendored
4
.github/workflows/build-live-usb.yml
vendored
|
|
@ -70,8 +70,8 @@ jobs:
|
|||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
debootstrap parted dosfstools e2fsprogs \
|
||||
squashfs-tools grub-efi-amd64-bin grub-pc-bin \
|
||||
xorriso mtools rsync curl wget
|
||||
squashfs-tools grub-efi-amd64-bin grub-efi-amd64-signed grub-pc-bin \
|
||||
shim-signed xorriso mtools rsync curl wget
|
||||
|
||||
- name: Build Live USB image
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -544,12 +544,28 @@ cp "${LIVE_DIR}/live/initrd.img" "${MNT}/esp/live/"
|
|||
cp "${LIVE_DIR}/live/filesystem.squashfs" "${MNT}/live/live/"
|
||||
|
||||
# Install GRUB EFI bootloader
|
||||
grub-mkimage -o "${MNT}/esp/EFI/BOOT/BOOTX64.EFI" \
|
||||
# Try grub-mkimage first for custom build
|
||||
if grub-mkimage -o "${MNT}/esp/EFI/BOOT/BOOTX64.EFI" \
|
||||
-O x86_64-efi \
|
||||
-p /boot/grub \
|
||||
part_gpt part_msdos fat ext2 normal linux boot configfile loopback \
|
||||
chain efifwsetup efi_gop efi_uga ls search search_label search_fs_uuid \
|
||||
search_fs_file gfxterm gfxterm_background gfxterm_menu test all_video loadenv exfat
|
||||
search_fs_file gfxterm gfxterm_background gfxterm_menu test all_video loadenv exfat 2>/dev/null; then
|
||||
ok "GRUB EFI image built successfully"
|
||||
else
|
||||
# Fallback: copy pre-built signed EFI from package
|
||||
warn "grub-mkimage failed, using pre-built EFI"
|
||||
if [[ -f /usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed ]]; then
|
||||
cp /usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed "${MNT}/esp/EFI/BOOT/BOOTX64.EFI"
|
||||
elif [[ -f /usr/lib/grub/x86_64-efi/grub.efi ]]; then
|
||||
cp /usr/lib/grub/x86_64-efi/grub.efi "${MNT}/esp/EFI/BOOT/BOOTX64.EFI"
|
||||
else
|
||||
err "No GRUB EFI binary found!"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Also copy as grubx64.efi for some UEFI implementations
|
||||
cp "${MNT}/esp/EFI/BOOT/BOOTX64.EFI" "${MNT}/esp/EFI/BOOT/grubx64.efi" 2>/dev/null || true
|
||||
|
||||
# Copy GRUB modules
|
||||
if [[ -d /usr/lib/grub/x86_64-efi ]]; then
|
||||
|
|
@ -557,6 +573,9 @@ if [[ -d /usr/lib/grub/x86_64-efi ]]; then
|
|||
cp -r /usr/lib/grub/x86_64-efi/*.mod "${MNT}/esp/boot/grub/x86_64-efi/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy grub.cfg to ESP as well (some UEFI look here)
|
||||
cp "${MNT}/esp/boot/grub/grub.cfg" "${MNT}/esp/EFI/BOOT/grub.cfg" 2>/dev/null || true
|
||||
|
||||
# Setup persistence partition
|
||||
if [[ $INCLUDE_PERSISTENCE -eq 1 ]]; then
|
||||
mkdir -p "${MNT}/persistence"
|
||||
|
|
|
|||
656
packages/secubox-jitsi/api/main.py
Normal file
656
packages/secubox-jitsi/api/main.py
Normal file
|
|
@ -0,0 +1,656 @@
|
|||
"""
|
||||
SecuBox Jitsi API
|
||||
LXC-based Jitsi Meet video conferencing management
|
||||
"""
|
||||
from fastapi import FastAPI, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
|
||||
# Core imports
|
||||
try:
|
||||
from secubox_core.auth import require_jwt
|
||||
from secubox_core.logger import get_logger
|
||||
except ImportError:
|
||||
def require_jwt():
|
||||
return {"sub": "admin"}
|
||||
class Logger:
|
||||
def info(self, msg): print(f"INFO: {msg}")
|
||||
def error(self, msg): print(f"ERROR: {msg}")
|
||||
def warning(self, msg): print(f"WARN: {msg}")
|
||||
def get_logger(name): return Logger()
|
||||
|
||||
app = FastAPI(
|
||||
title="secubox-jitsi",
|
||||
root_path="/api/v1/jitsi",
|
||||
)
|
||||
|
||||
log = get_logger("jitsi")
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
LXC_NAME = "secubox-jitsi"
|
||||
CONFIG_DIR = Path("/var/lib/secubox/jitsi")
|
||||
DATA_DIR = Path("/var/lib/secubox/jitsi/data")
|
||||
JITSI_WEB_PORT = 443
|
||||
JITSI_XMPP_PORT = 5222
|
||||
JVB_PORT = 10000
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# LXC Helper Functions
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
def lxc_exists() -> bool:
|
||||
"""Check if LXC container exists."""
|
||||
result = subprocess.run(
|
||||
["lxc-info", "-n", LXC_NAME],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def lxc_running() -> bool:
|
||||
"""Check if LXC container is running."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["lxc-info", "-n", LXC_NAME, "-s"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
return "RUNNING" in result.stdout
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def lxc_get_ip() -> Optional[str]:
|
||||
"""Get LXC container IP address."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["lxc-info", "-n", LXC_NAME, "-iH"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip().split("\n")[0]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def lxc_exec(cmd: List[str], timeout: int = 60) -> subprocess.CompletedProcess:
|
||||
"""Execute command inside LXC container."""
|
||||
return subprocess.run(
|
||||
["lxc-attach", "-n", LXC_NAME, "--"] + cmd,
|
||||
capture_output=True, text=True, timeout=timeout
|
||||
)
|
||||
|
||||
|
||||
def lxc_start() -> bool:
|
||||
"""Start the LXC container."""
|
||||
result = subprocess.run(
|
||||
["lxc-start", "-n", LXC_NAME],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def lxc_stop() -> bool:
|
||||
"""Stop the LXC container."""
|
||||
result = subprocess.run(
|
||||
["lxc-stop", "-n", LXC_NAME],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Request/Response Models
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
class RoomCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=64)
|
||||
password: Optional[str] = None
|
||||
max_participants: int = Field(default=50, ge=2, le=500)
|
||||
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
domain: Optional[str] = None
|
||||
enable_authentication: Optional[bool] = None
|
||||
enable_lobby: Optional[bool] = None
|
||||
enable_recording: Optional[bool] = None
|
||||
max_participants: Optional[int] = None
|
||||
welcome_message: Optional[str] = None
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Status Endpoints
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "module": "jitsi"}
|
||||
|
||||
|
||||
@app.get("/status")
|
||||
async def status(user=Depends(require_jwt)):
|
||||
"""Get Jitsi Meet status."""
|
||||
container_exists = lxc_exists()
|
||||
running = lxc_running() if container_exists else False
|
||||
container_ip = lxc_get_ip() if running else None
|
||||
|
||||
services = {
|
||||
"prosody": False,
|
||||
"jicofo": False,
|
||||
"jitsi-videobridge": False,
|
||||
"nginx": False,
|
||||
}
|
||||
|
||||
version = None
|
||||
domain = None
|
||||
active_conferences = 0
|
||||
active_participants = 0
|
||||
|
||||
if running:
|
||||
# Check service status
|
||||
for service in services.keys():
|
||||
result = lxc_exec(["systemctl", "is-active", service])
|
||||
services[service] = result.returncode == 0
|
||||
|
||||
# Get domain from config
|
||||
result = lxc_exec(["cat", "/etc/jitsi/meet/config.js"])
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.splitlines():
|
||||
if "domain:" in line:
|
||||
domain = line.split("'")[1] if "'" in line else None
|
||||
break
|
||||
|
||||
# Get JVB stats
|
||||
if container_ip:
|
||||
try:
|
||||
result = lxc_exec(["curl", "-s", "http://localhost:8080/colibri/stats"])
|
||||
if result.returncode == 0:
|
||||
stats = json.loads(result.stdout)
|
||||
active_conferences = stats.get("conferences", 0)
|
||||
active_participants = stats.get("participants", 0)
|
||||
version = stats.get("version")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get disk usage
|
||||
disk_usage = None
|
||||
if DATA_DIR.exists():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["du", "-sh", str(DATA_DIR)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
disk_usage = result.stdout.split()[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"container_exists": container_exists,
|
||||
"running": running,
|
||||
"container_ip": container_ip,
|
||||
"services": services,
|
||||
"version": version,
|
||||
"domain": domain,
|
||||
"active_conferences": active_conferences,
|
||||
"active_participants": active_participants,
|
||||
"disk_usage": disk_usage,
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Lifecycle Endpoints
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.post("/install")
|
||||
async def install(
|
||||
domain: str = Query(..., description="Jitsi domain (e.g., meet.example.com)"),
|
||||
user=Depends(require_jwt)
|
||||
):
|
||||
"""Install Jitsi Meet in LXC container."""
|
||||
if lxc_exists():
|
||||
raise HTTPException(400, "Container already exists")
|
||||
|
||||
log.info(f"Installing Jitsi Meet for {domain}")
|
||||
|
||||
# Create data directories
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create LXC container
|
||||
result = subprocess.run([
|
||||
"lxc-create", "-n", LXC_NAME,
|
||||
"-t", "download",
|
||||
"--",
|
||||
"-d", "debian",
|
||||
"-r", "bookworm",
|
||||
"-a", "amd64"
|
||||
], capture_output=True, text=True, timeout=600)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise HTTPException(500, f"Failed to create container: {result.stderr}")
|
||||
|
||||
# Configure container
|
||||
lxc_config = f"/var/lib/lxc/{LXC_NAME}/config"
|
||||
with open(lxc_config, "a") as f:
|
||||
f.write(f"\n# SecuBox Jitsi config\n")
|
||||
f.write(f"lxc.mount.entry = {DATA_DIR} data none bind,create=dir 0 0\n")
|
||||
f.write("lxc.start.auto = 1\n")
|
||||
|
||||
# Start container
|
||||
if not lxc_start():
|
||||
raise HTTPException(500, "Failed to start container")
|
||||
|
||||
# Wait for network
|
||||
import time
|
||||
for _ in range(30):
|
||||
if lxc_get_ip():
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# Install Jitsi
|
||||
install_script = f'''#!/bin/bash
|
||||
set -e
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Set hostname
|
||||
echo "{domain}" > /etc/hostname
|
||||
hostname {domain}
|
||||
|
||||
# Add hosts entry
|
||||
echo "127.0.0.1 {domain}" >> /etc/hosts
|
||||
|
||||
# Update and install prerequisites
|
||||
apt-get update
|
||||
apt-get install -y gnupg2 apt-transport-https curl
|
||||
|
||||
# Add Jitsi repository
|
||||
curl -fsSL https://download.jitsi.org/jitsi-key.gpg.key | gpg --dearmor -o /usr/share/keyrings/jitsi-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/jitsi-keyring.gpg] https://download.jitsi.org stable/" > /etc/apt/sources.list.d/jitsi-stable.list
|
||||
|
||||
apt-get update
|
||||
|
||||
# Pre-configure Jitsi
|
||||
echo "jitsi-videobridge2 jitsi-videobridge/jvb-hostname string {domain}" | debconf-set-selections
|
||||
echo "jitsi-meet-web-config jitsi-meet/cert-choice select Generate a new self-signed certificate" | debconf-set-selections
|
||||
|
||||
# Install Jitsi Meet
|
||||
apt-get install -y jitsi-meet
|
||||
|
||||
# Enable services
|
||||
systemctl enable prosody jicofo jitsi-videobridge2 nginx
|
||||
systemctl start prosody jicofo jitsi-videobridge2 nginx
|
||||
|
||||
echo "Jitsi Meet installed successfully"
|
||||
'''
|
||||
|
||||
result = lxc_exec(["bash", "-c", install_script], timeout=900)
|
||||
|
||||
if result.returncode != 0:
|
||||
log.error(f"Install failed: {result.stderr}")
|
||||
raise HTTPException(500, f"Installation failed: {result.stderr[:500]}")
|
||||
|
||||
log.info("Jitsi Meet installed successfully")
|
||||
return {"success": True, "message": f"Jitsi installed for {domain}"}
|
||||
|
||||
|
||||
@app.post("/start")
|
||||
async def start(user=Depends(require_jwt)):
|
||||
"""Start Jitsi container."""
|
||||
if not lxc_exists():
|
||||
raise HTTPException(400, "Container not installed")
|
||||
|
||||
if lxc_running():
|
||||
return {"success": True, "message": "Already running"}
|
||||
|
||||
if lxc_start():
|
||||
# Start all Jitsi services
|
||||
for service in ["prosody", "jicofo", "jitsi-videobridge2", "nginx"]:
|
||||
lxc_exec(["systemctl", "start", service])
|
||||
return {"success": True}
|
||||
|
||||
raise HTTPException(500, "Failed to start container")
|
||||
|
||||
|
||||
@app.post("/stop")
|
||||
async def stop(user=Depends(require_jwt)):
|
||||
"""Stop Jitsi container."""
|
||||
if not lxc_running():
|
||||
return {"success": True, "message": "Already stopped"}
|
||||
|
||||
# Stop services gracefully
|
||||
for service in ["nginx", "jitsi-videobridge2", "jicofo", "prosody"]:
|
||||
lxc_exec(["systemctl", "stop", service])
|
||||
|
||||
if lxc_stop():
|
||||
return {"success": True}
|
||||
|
||||
raise HTTPException(500, "Failed to stop container")
|
||||
|
||||
|
||||
@app.post("/restart")
|
||||
async def restart(user=Depends(require_jwt)):
|
||||
"""Restart Jitsi services."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
for service in ["prosody", "jicofo", "jitsi-videobridge2", "nginx"]:
|
||||
lxc_exec(["systemctl", "restart", service])
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.delete("/uninstall")
|
||||
async def uninstall(user=Depends(require_jwt)):
|
||||
"""Remove Jitsi container (keeps data)."""
|
||||
if lxc_running():
|
||||
lxc_stop()
|
||||
|
||||
if lxc_exists():
|
||||
result = subprocess.run(
|
||||
["lxc-destroy", "-n", LXC_NAME],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise HTTPException(500, f"Failed to destroy: {result.stderr}")
|
||||
|
||||
return {"success": True, "message": "Container removed, data preserved"}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Conference Management
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.get("/conferences")
|
||||
async def list_conferences(user=Depends(require_jwt)):
|
||||
"""List active conferences."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
container_ip = lxc_get_ip()
|
||||
if not container_ip:
|
||||
return {"conferences": []}
|
||||
|
||||
try:
|
||||
result = lxc_exec(["curl", "-s", "http://localhost:8080/colibri/stats"])
|
||||
if result.returncode == 0:
|
||||
stats = json.loads(result.stdout)
|
||||
return {
|
||||
"conferences": stats.get("conferences", 0),
|
||||
"participants": stats.get("participants", 0),
|
||||
"largest_conference": stats.get("largest_conference", 0),
|
||||
"total_conferences_created": stats.get("total_conferences_created", 0),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"conferences": [], "error": "Failed to get stats"}
|
||||
|
||||
|
||||
@app.post("/rooms")
|
||||
async def create_room(req: RoomCreate, user=Depends(require_jwt)):
|
||||
"""Create a meeting room with optional password."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
container_ip = lxc_get_ip()
|
||||
if not container_ip:
|
||||
raise HTTPException(500, "Cannot get container IP")
|
||||
|
||||
# Generate room URL
|
||||
room_name = req.name.replace(" ", "_").lower()
|
||||
room_url = f"https://{container_ip}/{room_name}"
|
||||
|
||||
# Password can only be set when joining the room
|
||||
return {
|
||||
"success": True,
|
||||
"room_name": room_name,
|
||||
"url": room_url,
|
||||
"password_required": req.password is not None,
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(user=Depends(require_jwt)):
|
||||
"""Get Jitsi configuration."""
|
||||
if not lxc_running():
|
||||
return {"error": "Container not running"}
|
||||
|
||||
config = {
|
||||
"domain": None,
|
||||
"enable_authentication": False,
|
||||
"enable_lobby": False,
|
||||
"enable_recording": False,
|
||||
"max_participants": 100,
|
||||
"welcome_message": "",
|
||||
}
|
||||
|
||||
# Read interface_config.js
|
||||
result = lxc_exec(["cat", "/etc/jitsi/meet/config.js"])
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if "domain:" in line:
|
||||
config["domain"] = line.split("'")[1] if "'" in line else None
|
||||
elif "enableLobby:" in line:
|
||||
config["enable_lobby"] = "true" in line.lower()
|
||||
elif "fileRecordingsEnabled:" in line:
|
||||
config["enable_recording"] = "true" in line.lower()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@app.post("/config")
|
||||
async def update_config(req: ConfigUpdate, user=Depends(require_jwt)):
|
||||
"""Update Jitsi configuration."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
# Update interface_config.js for UI settings
|
||||
if req.welcome_message is not None:
|
||||
result = lxc_exec([
|
||||
"sed", "-i",
|
||||
f"s/MOBILE_APP_PROMO: .*/MOBILE_APP_PROMO: false,/",
|
||||
"/usr/share/jitsi-meet/interface_config.js"
|
||||
])
|
||||
|
||||
# Restart services to apply
|
||||
for service in ["prosody", "jicofo", "jitsi-videobridge2"]:
|
||||
lxc_exec(["systemctl", "restart", service])
|
||||
|
||||
return {"success": True, "message": "Configuration updated"}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Authentication
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.post("/auth/enable")
|
||||
async def enable_authentication(user=Depends(require_jwt)):
|
||||
"""Enable Prosody authentication for moderators."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
# Get domain
|
||||
result = lxc_exec(["hostname"])
|
||||
domain = result.stdout.strip() if result.returncode == 0 else "localhost"
|
||||
|
||||
auth_script = f'''#!/bin/bash
|
||||
# Enable authentication in Prosody
|
||||
sed -i 's/authentication = "anonymous"/authentication = "internal_hashed"/' /etc/prosody/conf.avail/{domain}.cfg.lua
|
||||
|
||||
# Add guest virtual host
|
||||
cat >> /etc/prosody/conf.avail/{domain}.cfg.lua << EOF
|
||||
|
||||
VirtualHost "guest.{domain}"
|
||||
authentication = "anonymous"
|
||||
modules_enabled = {{
|
||||
"turncredentials";
|
||||
}}
|
||||
c2s_require_encryption = false
|
||||
EOF
|
||||
|
||||
# Update Jitsi config
|
||||
sed -i "s/anonymousdomain: .*/anonymousdomain: 'guest.{domain}',/" /etc/jitsi/meet/{domain}-config.js
|
||||
|
||||
# Restart services
|
||||
systemctl restart prosody jicofo
|
||||
'''
|
||||
|
||||
result = lxc_exec(["bash", "-c", auth_script])
|
||||
return {"success": result.returncode == 0, "domain": domain}
|
||||
|
||||
|
||||
@app.post("/auth/users")
|
||||
async def create_auth_user(
|
||||
username: str = Query(...),
|
||||
password: str = Query(..., min_length=8),
|
||||
user=Depends(require_jwt)
|
||||
):
|
||||
"""Create a moderator user."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
result = lxc_exec(["hostname"])
|
||||
domain = result.stdout.strip() if result.returncode == 0 else "localhost"
|
||||
|
||||
result = lxc_exec([
|
||||
"prosodyctl", "register",
|
||||
username, domain, password
|
||||
])
|
||||
|
||||
return {
|
||||
"success": result.returncode == 0,
|
||||
"username": f"{username}@{domain}"
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Recording (Jibri)
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.get("/recording")
|
||||
async def recording_status(user=Depends(require_jwt)):
|
||||
"""Get recording (Jibri) status."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
result = lxc_exec(["systemctl", "is-active", "jibri"])
|
||||
jibri_installed = result.returncode == 0 or "inactive" in result.stdout
|
||||
|
||||
return {
|
||||
"installed": jibri_installed,
|
||||
"running": result.returncode == 0,
|
||||
"message": "Jibri provides recording and streaming capabilities"
|
||||
}
|
||||
|
||||
|
||||
@app.post("/recording/install")
|
||||
async def install_jibri(user=Depends(require_jwt)):
|
||||
"""Install Jibri for recording support."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
# Jibri installation is complex and requires X11, Chrome, ffmpeg
|
||||
install_script = '''#!/bin/bash
|
||||
set -e
|
||||
|
||||
apt-get update
|
||||
apt-get install -y jibri
|
||||
|
||||
systemctl enable jibri
|
||||
systemctl start jibri
|
||||
'''
|
||||
|
||||
result = lxc_exec(["bash", "-c", install_script], timeout=600)
|
||||
|
||||
return {
|
||||
"success": result.returncode == 0,
|
||||
"message": "Jibri installed" if result.returncode == 0 else result.stderr[:200]
|
||||
}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Logs
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.get("/logs")
|
||||
async def get_logs(
|
||||
service: str = Query("jicofo", enum=["prosody", "jicofo", "jitsi-videobridge2", "nginx"]),
|
||||
lines: int = Query(100, ge=10, le=1000),
|
||||
user=Depends(require_jwt)
|
||||
):
|
||||
"""Get Jitsi service logs."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
result = lxc_exec(["journalctl", "-u", service, "-n", str(lines), "--no-pager"])
|
||||
|
||||
return {"logs": result.stdout.splitlines() if result.returncode == 0 else []}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Stats
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.get("/stats")
|
||||
async def get_stats(user=Depends(require_jwt)):
|
||||
"""Get detailed JVB statistics."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
try:
|
||||
result = lxc_exec(["curl", "-s", "http://localhost:8080/colibri/stats"])
|
||||
if result.returncode == 0:
|
||||
return json.loads(result.stdout)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"error": "Failed to get stats"}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# SSL Certificates
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.post("/ssl/letsencrypt")
|
||||
async def setup_letsencrypt(
|
||||
email: str = Query(...),
|
||||
user=Depends(require_jwt)
|
||||
):
|
||||
"""Setup Let's Encrypt SSL certificate."""
|
||||
if not lxc_running():
|
||||
raise HTTPException(400, "Container not running")
|
||||
|
||||
result = lxc_exec(["hostname"])
|
||||
domain = result.stdout.strip() if result.returncode == 0 else "localhost"
|
||||
|
||||
ssl_script = f'''#!/bin/bash
|
||||
set -e
|
||||
|
||||
apt-get install -y certbot python3-certbot-nginx
|
||||
|
||||
certbot --nginx -d {domain} --non-interactive --agree-tos -m {email}
|
||||
|
||||
systemctl restart nginx
|
||||
'''
|
||||
|
||||
result = lxc_exec(["bash", "-c", ssl_script], timeout=300)
|
||||
|
||||
return {
|
||||
"success": result.returncode == 0,
|
||||
"domain": domain,
|
||||
"message": "SSL certificate installed" if result.returncode == 0 else result.stderr[:200]
|
||||
}
|
||||
10
packages/secubox-jitsi/debian/changelog
Normal file
10
packages/secubox-jitsi/debian/changelog
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
secubox-jitsi (1.0.0-1) bookworm; urgency=medium
|
||||
|
||||
* Initial release
|
||||
* LXC-based Jitsi Meet video conferencing
|
||||
* Prosody + Jicofo + JVB stack
|
||||
* Authentication support
|
||||
* Let's Encrypt SSL integration
|
||||
* Recording support via Jibri
|
||||
|
||||
-- Gandalf / CyberMind <gk@cybermind.fr> Fri, 28 Mar 2026 09:30:00 +0100
|
||||
27
packages/secubox-jitsi/debian/control
Normal file
27
packages/secubox-jitsi/debian/control
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
Source: secubox-jitsi
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Gandalf / CyberMind <gk@cybermind.fr>
|
||||
Build-Depends: debhelper-compat (= 13)
|
||||
Standards-Version: 4.6.2
|
||||
Homepage: https://cybermind.fr/secubox
|
||||
|
||||
Package: secubox-jitsi
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends},
|
||||
secubox-core (>= 1.0),
|
||||
python3-uvicorn,
|
||||
lxc,
|
||||
lxc-templates
|
||||
Recommends: nginx
|
||||
Description: SecuBox Jitsi Meet - Video Conferencing
|
||||
LXC-based Jitsi Meet video conferencing for SecuBox.
|
||||
Provides secure, scalable video meetings with screen sharing.
|
||||
.
|
||||
Features:
|
||||
- Full Jitsi Meet in LXC container
|
||||
- Prosody XMPP server for signaling
|
||||
- Jitsi Videobridge (JVB) for media
|
||||
- Optional authentication
|
||||
- Let's Encrypt SSL support
|
||||
- Recording with Jibri (optional)
|
||||
19
packages/secubox-jitsi/debian/postinst
Executable file
19
packages/secubox-jitsi/debian/postinst
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
# Create data directories
|
||||
install -d -o secubox -g secubox -m 750 /var/lib/secubox/jitsi
|
||||
install -d -o secubox -g secubox -m 750 /var/lib/secubox/jitsi/data
|
||||
|
||||
# Enable and start service
|
||||
systemctl daemon-reload
|
||||
systemctl enable secubox-jitsi.service
|
||||
systemctl start secubox-jitsi.service || true
|
||||
|
||||
# Reload nginx if installed
|
||||
systemctl reload nginx 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
#DEBHELPER#
|
||||
16
packages/secubox-jitsi/debian/rules
Executable file
16
packages/secubox-jitsi/debian/rules
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/make -f
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_install:
|
||||
install -d $(CURDIR)/debian/secubox-jitsi/usr/lib/secubox/jitsi/api
|
||||
install -m 644 api/main.py $(CURDIR)/debian/secubox-jitsi/usr/lib/secubox/jitsi/api/
|
||||
touch $(CURDIR)/debian/secubox-jitsi/usr/lib/secubox/jitsi/api/__init__.py
|
||||
install -d $(CURDIR)/debian/secubox-jitsi/usr/share/secubox/www/jitsi
|
||||
install -m 644 www/jitsi/index.html $(CURDIR)/debian/secubox-jitsi/usr/share/secubox/www/jitsi/
|
||||
install -d $(CURDIR)/debian/secubox-jitsi/etc/nginx/secubox.d
|
||||
install -m 644 nginx/jitsi.conf $(CURDIR)/debian/secubox-jitsi/etc/nginx/secubox.d/
|
||||
install -d $(CURDIR)/debian/secubox-jitsi/usr/share/secubox/menu.d
|
||||
install -m 644 menu.d/809-jitsi.json $(CURDIR)/debian/secubox-jitsi/usr/share/secubox/menu.d/
|
||||
install -d $(CURDIR)/debian/secubox-jitsi/usr/lib/systemd/system
|
||||
install -m 644 debian/secubox-jitsi.service $(CURDIR)/debian/secubox-jitsi/usr/lib/systemd/system/
|
||||
22
packages/secubox-jitsi/debian/secubox-jitsi.service
Normal file
22
packages/secubox-jitsi/debian/secubox-jitsi.service
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[Unit]
|
||||
Description=SecuBox Jitsi API
|
||||
After=network.target secubox-core.service
|
||||
Requires=secubox-core.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/usr/lib/secubox/jitsi
|
||||
ExecStart=/usr/bin/uvicorn api.main:app --uds /run/secubox/jitsi.sock --log-level warning
|
||||
UMask=0117
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
PrivateTmp=true
|
||||
NoNewPrivileges=false
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/run/secubox /var/lib/secubox /var/lib/lxc
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
packages/secubox-jitsi/menu.d/809-jitsi.json
Normal file
9
packages/secubox-jitsi/menu.d/809-jitsi.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "jitsi",
|
||||
"name": "Jitsi Meet",
|
||||
"icon": "📹",
|
||||
"path": "/jitsi/",
|
||||
"category": "apps",
|
||||
"order": 809,
|
||||
"description": "Video conferencing and meetings"
|
||||
}
|
||||
16
packages/secubox-jitsi/nginx/jitsi.conf
Normal file
16
packages/secubox-jitsi/nginx/jitsi.conf
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# SecuBox Jitsi API proxy
|
||||
location /api/v1/jitsi/ {
|
||||
proxy_pass http://unix:/run/secubox/jitsi.sock;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# SecuBox Jitsi Web UI
|
||||
location /jitsi/ {
|
||||
alias /usr/share/secubox/www/jitsi/;
|
||||
try_files $uri $uri/ /jitsi/index.html;
|
||||
}
|
||||
574
packages/secubox-jitsi/www/jitsi/index.html
Normal file
574
packages/secubox-jitsi/www/jitsi/index.html
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Jitsi Meet - SecuBox</title>
|
||||
<link rel="stylesheet" href="/shared/crt-light.css">
|
||||
<link rel="stylesheet" href="/shared/sidebar-light.css">
|
||||
<style>
|
||||
:root {
|
||||
--jitsi-blue: #246fe5;
|
||||
--jitsi-light: #5090e8;
|
||||
--jitsi-dark: #1a5cc4;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
--jitsi-blue: #5090e8;
|
||||
--jitsi-light: #7aabe8;
|
||||
--jitsi-dark: #246fe5;
|
||||
}
|
||||
|
||||
.accent { color: var(--jitsi-blue); }
|
||||
.accent-bg { background-color: var(--jitsi-blue); }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.header h1 { margin: 0; font-size: 1.5rem; }
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-badge.running { background: var(--success-bg); color: var(--success); }
|
||||
.status-badge.stopped { background: var(--error-bg); color: var(--error); }
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--jitsi-blue);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.service-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.service-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.service-status.active { background: var(--success); }
|
||||
.service-status.inactive { background: var(--error); }
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn:hover { background: var(--hover); }
|
||||
.btn.primary { background: var(--jitsi-blue); color: white; border-color: var(--jitsi-blue); }
|
||||
.btn.primary:hover { background: var(--jitsi-dark); }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { color: var(--jitsi-blue); border-bottom-color: var(--jitsi-blue); }
|
||||
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
.log-viewer {
|
||||
background: var(--bg-dark, #1a1a2e);
|
||||
color: var(--text-light, #e0e0e0);
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.quick-join {
|
||||
max-width: 400px;
|
||||
margin: 1rem auto;
|
||||
padding: 1.5rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-join input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.open-app-btn {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--jitsi-blue);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 2rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 4px 12px rgba(36, 111, 229, 0.4);
|
||||
}
|
||||
|
||||
.open-app-btn:hover { background: var(--jitsi-dark); }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.install-form {
|
||||
max-width: 400px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.install-form h2 { margin-top: 0; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="crt-light">
|
||||
<nav class="sidebar" id="sidebar"></nav>
|
||||
<main class="main-content">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><span class="accent">Jitsi</span> Meet</h1>
|
||||
<span id="status-badge" class="status-badge stopped">Stopped</span>
|
||||
</div>
|
||||
|
||||
<div id="install-section" class="install-form hidden">
|
||||
<h2>Install Jitsi Meet</h2>
|
||||
<form onsubmit="installJitsi(event)">
|
||||
<div class="form-group">
|
||||
<label>Domain</label>
|
||||
<input type="text" id="install-domain" placeholder="meet.example.com" required>
|
||||
<small style="color: var(--text-secondary);">Must resolve to this server</small>
|
||||
</div>
|
||||
<button type="submit" class="btn primary" style="width: 100%; margin-top: 1rem;">Install Jitsi</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="main-section" class="hidden">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="active-conferences">0</div>
|
||||
<div class="stat-label">Active Meetings</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="active-participants">0</div>
|
||||
<div class="stat-label">Participants</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="largest-conference">0</div>
|
||||
<div class="stat-label">Largest Meeting</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="total-conferences">0</div>
|
||||
<div class="stat-label">Total Created</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="services-grid" id="services-grid"></div>
|
||||
|
||||
<div class="action-bar">
|
||||
<button class="btn primary" id="btn-start" onclick="startService()">Start</button>
|
||||
<button class="btn" id="btn-stop" onclick="stopService()" disabled>Stop</button>
|
||||
<button class="btn" id="btn-restart" onclick="restartService()" disabled>Restart</button>
|
||||
</div>
|
||||
|
||||
<div class="quick-join">
|
||||
<h3 style="margin-top: 0;">Quick Join</h3>
|
||||
<input type="text" id="room-name" placeholder="Enter room name...">
|
||||
<button class="btn primary" onclick="joinRoom()" style="width: 100%;">Join Meeting</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="overview">Overview</button>
|
||||
<button class="tab" data-tab="auth">Authentication</button>
|
||||
<button class="tab" data-tab="logs">Logs</button>
|
||||
<button class="tab" data-tab="settings">Settings</button>
|
||||
</div>
|
||||
|
||||
<div id="tab-overview" class="tab-content active">
|
||||
<div class="info-grid" style="display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
|
||||
<div class="info-card" style="background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem;">
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase;">Domain</div>
|
||||
<div style="font-size: 1.25rem; font-weight: 500; margin-top: 0.25rem;" id="jitsi-domain">-</div>
|
||||
</div>
|
||||
<div class="info-card" style="background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem;">
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase;">Container IP</div>
|
||||
<div style="font-size: 1.25rem; font-weight: 500; margin-top: 0.25rem;" id="container-ip">-</div>
|
||||
</div>
|
||||
<div class="info-card" style="background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem;">
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase;">Disk Usage</div>
|
||||
<div style="font-size: 1.25rem; font-weight: 500; margin-top: 0.25rem;" id="disk-usage">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-auth" class="tab-content">
|
||||
<p style="margin-bottom: 1rem;">Enable authentication to require login for meeting moderators.</p>
|
||||
<div class="action-bar">
|
||||
<button class="btn primary" onclick="enableAuth()">Enable Authentication</button>
|
||||
</div>
|
||||
<h4>Create Moderator User</h4>
|
||||
<form class="settings-form" onsubmit="createUser(event)">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="auth-username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" id="auth-password" minlength="8" required>
|
||||
</div>
|
||||
<button type="submit" class="btn primary">Create User</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="tab-logs" class="tab-content">
|
||||
<div class="action-bar">
|
||||
<select id="log-service">
|
||||
<option value="jicofo">Jicofo</option>
|
||||
<option value="prosody">Prosody</option>
|
||||
<option value="jitsi-videobridge2">JVB</option>
|
||||
<option value="nginx">Nginx</option>
|
||||
</select>
|
||||
<button class="btn" onclick="loadLogs()">Refresh</button>
|
||||
</div>
|
||||
<div id="log-viewer" class="log-viewer">Select a service and click Refresh</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-settings" class="tab-content">
|
||||
<h4>SSL Certificate</h4>
|
||||
<form class="settings-form" onsubmit="setupSSL(event)">
|
||||
<div class="form-group">
|
||||
<label>Email (for Let's Encrypt)</label>
|
||||
<input type="email" id="ssl-email" required>
|
||||
</div>
|
||||
<button type="submit" class="btn primary">Setup Let's Encrypt</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="open-app-btn" class="open-app-btn hidden" onclick="openJitsi()">
|
||||
Open Jitsi Meet
|
||||
</button>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/api/v1/jitsi';
|
||||
let containerIP = null;
|
||||
|
||||
async function apiCall(endpoint, options = {}) {
|
||||
const token = localStorage.getItem('sbx_token');
|
||||
const response = await fetch(API_BASE + endpoint, {
|
||||
...options,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const data = await apiCall('/status');
|
||||
const badge = document.getElementById('status-badge');
|
||||
const btnStart = document.getElementById('btn-start');
|
||||
const btnStop = document.getElementById('btn-stop');
|
||||
const btnRestart = document.getElementById('btn-restart');
|
||||
const openBtn = document.getElementById('open-app-btn');
|
||||
|
||||
if (!data.container_exists) {
|
||||
document.getElementById('install-section').classList.remove('hidden');
|
||||
document.getElementById('main-section').classList.add('hidden');
|
||||
badge.textContent = 'Not Installed';
|
||||
badge.className = 'status-badge stopped';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('install-section').classList.add('hidden');
|
||||
document.getElementById('main-section').classList.remove('hidden');
|
||||
|
||||
if (data.running) {
|
||||
badge.textContent = 'Running';
|
||||
badge.className = 'status-badge running';
|
||||
btnStart.disabled = true;
|
||||
btnStop.disabled = false;
|
||||
btnRestart.disabled = false;
|
||||
containerIP = data.container_ip;
|
||||
openBtn.classList.remove('hidden');
|
||||
|
||||
// Update stats
|
||||
document.getElementById('active-conferences').textContent = data.active_conferences || 0;
|
||||
document.getElementById('active-participants').textContent = data.active_participants || 0;
|
||||
} else {
|
||||
badge.textContent = 'Stopped';
|
||||
badge.className = 'status-badge stopped';
|
||||
btnStart.disabled = false;
|
||||
btnStop.disabled = true;
|
||||
btnRestart.disabled = true;
|
||||
openBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('jitsi-domain').textContent = data.domain || '-';
|
||||
document.getElementById('container-ip').textContent = data.container_ip || '-';
|
||||
document.getElementById('disk-usage').textContent = data.disk_usage || '-';
|
||||
|
||||
// Services
|
||||
const servicesGrid = document.getElementById('services-grid');
|
||||
if (data.services) {
|
||||
servicesGrid.innerHTML = Object.entries(data.services).map(([name, active]) => `
|
||||
<div class="service-badge">
|
||||
<span class="service-status ${active ? 'active' : 'inactive'}"></span>
|
||||
${name}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Load conference stats
|
||||
if (data.running) loadConferences();
|
||||
} catch (e) {
|
||||
console.error('Status load failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConferences() {
|
||||
try {
|
||||
const data = await apiCall('/conferences');
|
||||
document.getElementById('active-conferences').textContent = data.conferences || 0;
|
||||
document.getElementById('active-participants').textContent = data.participants || 0;
|
||||
document.getElementById('largest-conference').textContent = data.largest_conference || 0;
|
||||
document.getElementById('total-conferences').textContent = data.total_conferences_created || 0;
|
||||
} catch (e) {
|
||||
console.error('Conferences load failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function installJitsi(e) {
|
||||
e.preventDefault();
|
||||
const domain = document.getElementById('install-domain').value;
|
||||
if (!domain) return;
|
||||
|
||||
const btn = e.target.querySelector('button');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Installing... (this takes several minutes)';
|
||||
|
||||
try {
|
||||
await apiCall(`/install?domain=${encodeURIComponent(domain)}`, { method: 'POST' });
|
||||
await loadStatus();
|
||||
} catch (err) {
|
||||
alert('Installation failed: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Install Jitsi';
|
||||
}
|
||||
}
|
||||
|
||||
async function startService() {
|
||||
document.getElementById('btn-start').disabled = true;
|
||||
try {
|
||||
await apiCall('/start', { method: 'POST' });
|
||||
await loadStatus();
|
||||
} catch (e) {
|
||||
alert('Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopService() {
|
||||
document.getElementById('btn-stop').disabled = true;
|
||||
try {
|
||||
await apiCall('/stop', { method: 'POST' });
|
||||
await loadStatus();
|
||||
} catch (e) {
|
||||
alert('Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartService() {
|
||||
document.getElementById('btn-restart').disabled = true;
|
||||
try {
|
||||
await apiCall('/restart', { method: 'POST' });
|
||||
await loadStatus();
|
||||
} catch (e) {
|
||||
alert('Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function joinRoom() {
|
||||
const roomName = document.getElementById('room-name').value.trim();
|
||||
if (!roomName) {
|
||||
alert('Enter a room name');
|
||||
return;
|
||||
}
|
||||
if (containerIP) {
|
||||
window.open(`https://${containerIP}/${roomName}`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
async function enableAuth() {
|
||||
if (!confirm('Enable authentication? This will require users to log in as moderators.')) return;
|
||||
try {
|
||||
await apiCall('/auth/enable', { method: 'POST' });
|
||||
alert('Authentication enabled');
|
||||
} catch (e) {
|
||||
alert('Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function createUser(e) {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('auth-username').value;
|
||||
const password = document.getElementById('auth-password').value;
|
||||
try {
|
||||
const result = await apiCall(`/auth/users?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`, { method: 'POST' });
|
||||
alert(`User created: ${result.username}`);
|
||||
e.target.reset();
|
||||
} catch (err) {
|
||||
alert('Failed: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const service = document.getElementById('log-service').value;
|
||||
try {
|
||||
const data = await apiCall(`/logs?service=${service}&lines=200`);
|
||||
document.getElementById('log-viewer').textContent = data.logs?.join('\n') || 'No logs';
|
||||
} catch (e) {
|
||||
document.getElementById('log-viewer').textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function setupSSL(e) {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('ssl-email').value;
|
||||
try {
|
||||
await apiCall(`/ssl/letsencrypt?email=${encodeURIComponent(email)}`, { method: 'POST' });
|
||||
alert('SSL certificate installed');
|
||||
} catch (err) {
|
||||
alert('Failed: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function openJitsi() {
|
||||
if (containerIP) window.open(`https://${containerIP}/`, '_blank');
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
||||
if (tab.dataset.tab === 'logs') loadLogs();
|
||||
});
|
||||
});
|
||||
|
||||
if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
|
||||
document.body.classList.add('dark');
|
||||
}
|
||||
|
||||
loadStatus();
|
||||
setInterval(loadStatus, 30000);
|
||||
</script>
|
||||
<script src="/shared/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user