Merge pull request #187 from outis1one/claude/asterisk-digital-ocean-w22kk8
Claude/asterisk digital ocean w22kk8
This commit is contained in:
@@ -543,78 +543,20 @@ WEB_ADMIN_AUTH_DISABLED=false
|
|||||||
ENV
|
ENV
|
||||||
chmod 600 .env
|
chmod 600 .env
|
||||||
|
|
||||||
# ── UFW firewall rules (host-level) ───────────────────────────────────────
|
|
||||||
if command -v ufw &>/dev/null; then
|
|
||||||
log_info "Opening UFW ports for Asterisk + coturn..."
|
|
||||||
ufw allow 5060/udp
|
|
||||||
ufw allow 5060/tcp
|
|
||||||
ufw allow 5061/tcp
|
|
||||||
ufw allow "${WEB_ADMIN_PORT_VAL}/tcp"
|
|
||||||
ufw allow 8088/tcp
|
|
||||||
ufw allow 8089/tcp
|
|
||||||
ufw allow 3478/udp
|
|
||||||
ufw allow 3478/tcp
|
|
||||||
ufw allow 10000:20000/udp
|
|
||||||
ufw allow 49152:49252/udp
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v ufw &>/dev/null; then
|
|
||||||
log_success "UFW rules added."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── DigitalOcean Cloud Firewall (network edge, in front of the droplet) ───
|
|
||||||
local DO_FW_RULES=(
|
|
||||||
"protocol:tcp,ports:22,address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:tcp,ports:5060,address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:udp,ports:5060,address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:tcp,ports:5061,address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:tcp,ports:${WEB_ADMIN_PORT_VAL},address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:tcp,ports:8088-8089,address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:tcp,ports:3478,address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:udp,ports:3478,address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:udp,ports:10000-20000,address:0.0.0.0/0,address:::/0"
|
|
||||||
"protocol:udp,ports:49152-49252,address:0.0.0.0/0,address:::/0"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
if [[ -n "$DROPLET_ID" ]] && command -v doctl &>/dev/null && doctl account get &>/dev/null; then
|
|
||||||
local EXISTING_FW
|
|
||||||
EXISTING_FW="$(doctl compute firewall list --format ID,DropletIDs --no-header 2>/dev/null \
|
|
||||||
| grep -E "(^|[, ])${DROPLET_ID}([, ]|\$)" | awk '{print $1}' | head -1)"
|
|
||||||
|
|
||||||
if [[ -n "$EXISTING_FW" ]]; then
|
|
||||||
log_warning "A Cloud Firewall (id $EXISTING_FW) is already attached to this droplet — not touching it."
|
|
||||||
log_warning "Add these inbound rules to it yourself (Networking → Firewalls in the DO console):"
|
|
||||||
printf ' %s\n' "${DO_FW_RULES[@]}"
|
|
||||||
else
|
|
||||||
local DO_FW=""
|
|
||||||
prompt_yn "Create a DigitalOcean Cloud Firewall for this droplet via doctl now? (y/n):" "y" DO_FW
|
|
||||||
if [[ "$DO_FW" =~ ^[Yy]$ ]]; then
|
|
||||||
if doctl compute firewall create \
|
|
||||||
--name "asterisk-digital-ocean" \
|
|
||||||
--droplet-ids "$DROPLET_ID" \
|
|
||||||
--inbound-rules "$(IFS=' '; echo "${DO_FW_RULES[*]}")" \
|
|
||||||
--outbound-rules "protocol:tcp,ports:all,address:0.0.0.0/0,address:::/0 protocol:udp,ports:all,address:0.0.0.0/0,address:::/0 protocol:icmp,ports:0,address:0.0.0.0/0,address:::/0" \
|
|
||||||
&>/dev/null; then
|
|
||||||
log_success "Cloud Firewall 'asterisk-digital-ocean' created and attached (SSH/22 included so you don't get locked out)."
|
|
||||||
log_info "Verify it in the DO console — adjust the SSH rule if you use a non-default SSH port."
|
|
||||||
else
|
|
||||||
log_warning "doctl firewall create failed — add the rules manually (see README)."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_info "doctl not installed/authenticated — configure a DigitalOcean Cloud Firewall manually:"
|
|
||||||
log_info "Control Panel → Networking → Firewalls → create, attach to this droplet, allow:"
|
|
||||||
printf ' %s\n' "${DO_FW_RULES[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Caddy: reverse-proxy the web admin on the SAME FQDN used for SIP ──────
|
# ── Caddy: reverse-proxy the web admin on the SAME FQDN used for SIP ──────
|
||||||
# Caddy only holds a cert for domains it's actively serving. If the web
|
# Caddy only holds a cert for domains it's actively serving. If the web
|
||||||
# admin were proxied on a different "admin" subdomain, Caddy would obtain
|
# admin were proxied on a different "admin" subdomain, Caddy would obtain
|
||||||
# a cert for THAT domain instead — the sync earlier would never find one
|
# a cert for THAT domain instead — the sync earlier would never find one
|
||||||
# matching $DOMAIN_NAME, and SIP TLS would silently stay self-signed. So
|
# matching $DOMAIN_NAME, and SIP TLS would silently stay self-signed. So
|
||||||
# there's no separate domain prompt: this always targets $DOMAIN_NAME.
|
# there's no separate domain prompt: this always targets $DOMAIN_NAME.
|
||||||
|
#
|
||||||
|
# Decided before the firewall rules below so they can be scoped
|
||||||
|
# correctly: if Caddy ends up fronting the web admin locally, there's no
|
||||||
|
# reason to also expose it directly to the internet — Caddy already
|
||||||
|
# reaches it over the host's internal network (host.docker.internal),
|
||||||
|
# and leaving the bare IP:port open would let anyone bypass Caddy/
|
||||||
|
# Authelia entirely.
|
||||||
|
local WEB_ADMIN_PUBLIC_ACCESS_NEEDED=true
|
||||||
if [[ -z "$DOMAIN_NAME" ]]; then
|
if [[ -z "$DOMAIN_NAME" ]]; then
|
||||||
log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL} (nothing for Caddy to do)."
|
log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL} (nothing for Caddy to do)."
|
||||||
elif [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -z "${CADDY_REMOTE_HOST:-}" ]]; then
|
elif [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -z "${CADDY_REMOTE_HOST:-}" ]]; then
|
||||||
@@ -705,6 +647,9 @@ CADDY_BLOCK
|
|||||||
)"
|
)"
|
||||||
|
|
||||||
if [[ "$_CADDY_MODE" == "local" ]]; then
|
if [[ "$_CADDY_MODE" == "local" ]]; then
|
||||||
|
# Caddy reaches this over the host's internal network — no
|
||||||
|
# need to keep the port open to the public internet.
|
||||||
|
WEB_ADMIN_PUBLIC_ACCESS_NEEDED=false
|
||||||
local _CADDYFILE="$DOCKER_DIR/caddy/Caddyfile"
|
local _CADDYFILE="$DOCKER_DIR/caddy/Caddyfile"
|
||||||
local _CADDY_BACKUP="$_CADDYFILE.backup.$(date +%Y%m%d-%H%M%S)"
|
local _CADDY_BACKUP="$_CADDYFILE.backup.$(date +%Y%m%d-%H%M%S)"
|
||||||
if [[ -f "$_CADDYFILE" ]]; then
|
if [[ -f "$_CADDYFILE" ]]; then
|
||||||
@@ -739,10 +684,83 @@ CADDY_BLOCK
|
|||||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_SNIPPET_DIR/asterisk-digital-ocean.caddy" 2>/dev/null || true
|
chown "$ACTUAL_USER:$ACTUAL_USER" "$_SNIPPET_DIR/asterisk-digital-ocean.caddy" 2>/dev/null || true
|
||||||
log_success "Snippet saved: $_SNIPPET_DIR/asterisk-digital-ocean.caddy"
|
log_success "Snippet saved: $_SNIPPET_DIR/asterisk-digital-ocean.caddy"
|
||||||
log_info "Copy to your Caddy machine: scp $_SNIPPET_DIR/asterisk-digital-ocean.caddy caddy-host:~/caddy-snippets/"
|
log_info "Copy to your Caddy machine: scp $_SNIPPET_DIR/asterisk-digital-ocean.caddy caddy-host:~/caddy-snippets/"
|
||||||
|
log_info "Remote Caddy reaches this droplet over its public IP, so the web admin port stays open below."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── UFW firewall rules (host-level) ───────────────────────────────────────
|
||||||
|
if command -v ufw &>/dev/null; then
|
||||||
|
log_info "Opening UFW ports for Asterisk + coturn..."
|
||||||
|
ufw allow 5060/udp
|
||||||
|
ufw allow 5060/tcp
|
||||||
|
ufw allow 5061/tcp
|
||||||
|
if [[ "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" == true ]]; then
|
||||||
|
ufw allow "${WEB_ADMIN_PORT_VAL}/tcp"
|
||||||
|
else
|
||||||
|
ufw delete allow "${WEB_ADMIN_PORT_VAL}/tcp" 2>/dev/null || true
|
||||||
|
log_info "Web admin port ${WEB_ADMIN_PORT_VAL} kept closed to the internet — Caddy fronts it locally."
|
||||||
|
fi
|
||||||
|
ufw allow 8088/tcp
|
||||||
|
ufw allow 8089/tcp
|
||||||
|
ufw allow 3478/udp
|
||||||
|
ufw allow 3478/tcp
|
||||||
|
ufw allow 10000:20000/udp
|
||||||
|
ufw allow 49152:49252/udp
|
||||||
|
log_success "UFW rules added."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── DigitalOcean Cloud Firewall (network edge, in front of the droplet) ───
|
||||||
|
local DO_FW_RULES=(
|
||||||
|
"protocol:tcp,ports:22,address:0.0.0.0/0,address:::/0"
|
||||||
|
"protocol:tcp,ports:5060,address:0.0.0.0/0,address:::/0"
|
||||||
|
"protocol:udp,ports:5060,address:0.0.0.0/0,address:::/0"
|
||||||
|
"protocol:tcp,ports:5061,address:0.0.0.0/0,address:::/0"
|
||||||
|
)
|
||||||
|
if [[ "$WEB_ADMIN_PUBLIC_ACCESS_NEEDED" == true ]]; then
|
||||||
|
DO_FW_RULES+=("protocol:tcp,ports:${WEB_ADMIN_PORT_VAL},address:0.0.0.0/0,address:::/0")
|
||||||
|
fi
|
||||||
|
DO_FW_RULES+=(
|
||||||
|
"protocol:tcp,ports:8088-8089,address:0.0.0.0/0,address:::/0"
|
||||||
|
"protocol:tcp,ports:3478,address:0.0.0.0/0,address:::/0"
|
||||||
|
"protocol:udp,ports:3478,address:0.0.0.0/0,address:::/0"
|
||||||
|
"protocol:udp,ports:10000-20000,address:0.0.0.0/0,address:::/0"
|
||||||
|
"protocol:udp,ports:49152-49252,address:0.0.0.0/0,address:::/0"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [[ -n "$DROPLET_ID" ]] && command -v doctl &>/dev/null && doctl account get &>/dev/null; then
|
||||||
|
local EXISTING_FW
|
||||||
|
EXISTING_FW="$(doctl compute firewall list --format ID,DropletIDs --no-header 2>/dev/null \
|
||||||
|
| grep -E "(^|[, ])${DROPLET_ID}([, ]|\$)" | awk '{print $1}' | head -1)"
|
||||||
|
|
||||||
|
if [[ -n "$EXISTING_FW" ]]; then
|
||||||
|
log_warning "A Cloud Firewall (id $EXISTING_FW) is already attached to this droplet — not touching it."
|
||||||
|
log_warning "Add these inbound rules to it yourself (Networking → Firewalls in the DO console):"
|
||||||
|
printf ' %s\n' "${DO_FW_RULES[@]}"
|
||||||
|
else
|
||||||
|
local DO_FW=""
|
||||||
|
prompt_yn "Create a DigitalOcean Cloud Firewall for this droplet via doctl now? (y/n):" "y" DO_FW
|
||||||
|
if [[ "$DO_FW" =~ ^[Yy]$ ]]; then
|
||||||
|
if doctl compute firewall create \
|
||||||
|
--name "asterisk-digital-ocean" \
|
||||||
|
--droplet-ids "$DROPLET_ID" \
|
||||||
|
--inbound-rules "$(IFS=' '; echo "${DO_FW_RULES[*]}")" \
|
||||||
|
--outbound-rules "protocol:tcp,ports:all,address:0.0.0.0/0,address:::/0 protocol:udp,ports:all,address:0.0.0.0/0,address:::/0 protocol:icmp,ports:0,address:0.0.0.0/0,address:::/0" \
|
||||||
|
&>/dev/null; then
|
||||||
|
log_success "Cloud Firewall 'asterisk-digital-ocean' created and attached (SSH/22 included so you don't get locked out)."
|
||||||
|
log_info "Verify it in the DO console — adjust the SSH rule if you use a non-default SSH port."
|
||||||
|
else
|
||||||
|
log_warning "doctl firewall create failed — add the rules manually (see README)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_info "doctl not installed/authenticated — configure a DigitalOcean Cloud Firewall manually:"
|
||||||
|
log_info "Control Panel → Networking → Firewalls → create, attach to this droplet, allow:"
|
||||||
|
printf ' %s\n' "${DO_FW_RULES[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── CrowdSec note ──────────────────────────────────────────────────────────
|
# ── CrowdSec note ──────────────────────────────────────────────────────────
|
||||||
# Not installed here — select it separately from the whiptail menu, or
|
# Not installed here — select it separately from the whiptail menu, or
|
||||||
# `sudo ./setup.sh crowdsec`. Its own installer (services/crowdsec.sh)
|
# `sudo ./setup.sh crowdsec`. Its own installer (services/crowdsec.sh)
|
||||||
@@ -833,7 +851,7 @@ plan for the admin panel.
|
|||||||
| 22 | TCP | SSH (keep this open or you're locked out) |
|
| 22 | TCP | SSH (keep this open or you're locked out) |
|
||||||
| 5060 | UDP/TCP | SIP signalling (unencrypted) |
|
| 5060 | UDP/TCP | SIP signalling (unencrypted) |
|
||||||
| 5061 | TCP | SIP over TLS |
|
| 5061 | TCP | SIP over TLS |
|
||||||
| ${WEB_ADMIN_PORT_VAL} | TCP | Easy Asterisk web admin (auto-picked — see \`.env\`) |
|
| ${WEB_ADMIN_PORT_VAL} | TCP | Easy Asterisk web admin (auto-picked — see \`.env\`). Only opened publicly if Caddy isn't fronting it locally — otherwise it's reachable only via \`https://${DOMAIN_NAME:-your-domain}/\`, not the bare IP:port. |
|
||||||
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
|
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
|
||||||
| 3478 | UDP/TCP | TURN/STUN (coturn) |
|
| 3478 | UDP/TCP | TURN/STUN (coturn) |
|
||||||
| 10000–20000 | UDP | RTP media streams |
|
| 10000–20000 | UDP | RTP media streams |
|
||||||
|
|||||||
+9
-2
@@ -431,8 +431,15 @@ fi
|
|||||||
chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk 2>/dev/null || true
|
chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk 2>/dev/null || true
|
||||||
|
|
||||||
# ── 10. Start Web Admin in background ─────────────────────────
|
# ── 10. Start Web Admin in background ─────────────────────────
|
||||||
# The web admin script is generated by the 'easy-asterisk' management tool.
|
# The web admin script is generated by the 'easy-asterisk' management tool,
|
||||||
# On first run: docker exec -it easy-asterisk easy-asterisk → Web Admin menu → Start
|
# but it only lives in the container's writable layer (not baked into the
|
||||||
|
# image or bind-mounted), so it's gone every time the container is
|
||||||
|
# recreated. Regenerate it unconditionally instead of only starting it if
|
||||||
|
# it happens to already exist — otherwise the web admin never comes back
|
||||||
|
# on its own after a restart, requiring a manual CLI trip every time.
|
||||||
|
if [[ -x /usr/local/bin/easy-asterisk ]]; then
|
||||||
|
/usr/local/bin/easy-asterisk --write-web-admin-script >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
if [[ -f "$WEB_ADMIN_SCRIPT" ]]; then
|
if [[ -f "$WEB_ADMIN_SCRIPT" ]]; then
|
||||||
log_info "Starting Web Admin on port ${WEB_ADMIN_PORT:-8080}..."
|
log_info "Starting Web Admin on port ${WEB_ADMIN_PORT:-8080}..."
|
||||||
WEBADMIN_PORT="${WEB_ADMIN_PORT:-8080}" \
|
WEBADMIN_PORT="${WEB_ADMIN_PORT:-8080}" \
|
||||||
|
|||||||
+113
-17
@@ -4779,7 +4779,13 @@ qualify_frequency=30
|
|||||||
subprocess.run(['chown', 'asterisk:asterisk', PJSIP_CONF], capture_output=True)
|
subprocess.run(['chown', 'asterisk:asterisk', PJSIP_CONF], capture_output=True)
|
||||||
subprocess.run(['/usr/local/bin/easy-asterisk', '--rebuild-dialplan'], capture_output=True)
|
subprocess.run(['/usr/local/bin/easy-asterisk', '--rebuild-dialplan'], capture_output=True)
|
||||||
|
|
||||||
return True, {'extension': extension, 'password': password, 'name': name}
|
return True, {
|
||||||
|
'extension': extension,
|
||||||
|
'password': password,
|
||||||
|
'name': name,
|
||||||
|
'transport': 'tls' if conn_type == 'fqdn' else 'udp',
|
||||||
|
'port': 5061 if conn_type == 'fqdn' else 5060,
|
||||||
|
}
|
||||||
|
|
||||||
def get_server_info():
|
def get_server_info():
|
||||||
"""Get server configuration info including TURN/STUN details"""
|
"""Get server configuration info including TURN/STUN details"""
|
||||||
@@ -5099,7 +5105,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
<p>Save these credentials - the password cannot be retrieved later</p>
|
<p>Save these credentials - the password cannot be retrieved later</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="credentials" id="credentials-display"></div>
|
<div class="credentials" id="credentials-display"></div>
|
||||||
<div style="margin-top: 20px; text-align: right;">
|
<div style="margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end;">
|
||||||
|
<button class="btn" onclick="copyDevicePassword()" style="background: #e2e8f0;">Copy Password</button>
|
||||||
|
<button class="btn" onclick="copyAllCredentials()" style="background: #e2e8f0;">Copy All</button>
|
||||||
<button class="btn btn-primary" onclick="closeCredentialsModal()">Done</button>
|
<button class="btn btn-primary" onclick="closeCredentialsModal()">Done</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -5702,24 +5710,49 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
closeModal();
|
closeModal();
|
||||||
let credHtml = `
|
const d = result.data;
|
||||||
<p><strong>Extension:</strong> ${result.data.extension}</p>
|
const transportLabel = d.transport === 'tls' ? 'TLS' : 'UDP';
|
||||||
<p><strong>Password:</strong> ${result.data.password}</p>
|
|
||||||
<p><strong>Name:</strong> ${result.data.name}</p>
|
// Fetch server info for the domain and TURN details
|
||||||
`;
|
let srv = {};
|
||||||
// Fetch server info to show TURN details
|
|
||||||
try {
|
try {
|
||||||
const srvRes = await fetch(API_BASE + '/server');
|
const srvRes = await fetch(API_BASE + '/server');
|
||||||
const srv = await srvRes.json();
|
srv = await srvRes.json();
|
||||||
if (srv.turn_enabled && srv.turn_server) {
|
|
||||||
credHtml += `<hr style="margin:12px 0;border-color:#e2e8f0">
|
|
||||||
<p style="font-size:13px;color:#64748b;margin-bottom:6px">STUN/TURN (configure in app Network settings)</p>
|
|
||||||
<p><strong>STUN/TURN server:</strong> ${srv.turn_server}</p>
|
|
||||||
<p><strong>TURN username:</strong> ${srv.turn_username}</p>
|
|
||||||
<p><strong>TURN password:</strong> ${srv.turn_password}</p>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
const server = srv.domain || srv.server_ip || '';
|
||||||
|
|
||||||
|
// Everything a SIP client (e.g. Sipnetic) needs, kept in one
|
||||||
|
// object so both the display and the copy buttons read from
|
||||||
|
// the same source instead of re-parsing rendered HTML.
|
||||||
|
lastDeviceCreds = {
|
||||||
|
name: d.name,
|
||||||
|
server: server,
|
||||||
|
port: d.port,
|
||||||
|
transport: transportLabel,
|
||||||
|
username: d.extension,
|
||||||
|
password: d.password,
|
||||||
|
turnEnabled: !!(srv.turn_enabled && srv.turn_server),
|
||||||
|
turnServer: srv.turn_server || '',
|
||||||
|
turnUsername: srv.turn_username || '',
|
||||||
|
turnPassword: srv.turn_password || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
let credHtml = `
|
||||||
|
<p><strong>Display Name:</strong> ${lastDeviceCreds.name}</p>
|
||||||
|
<p><strong>Server:</strong> ${lastDeviceCreds.server}</p>
|
||||||
|
<p><strong>Port:</strong> ${lastDeviceCreds.port}</p>
|
||||||
|
<p><strong>Transport:</strong> ${lastDeviceCreds.transport}</p>
|
||||||
|
<p><strong>Username:</strong> ${lastDeviceCreds.username}</p>
|
||||||
|
<p><strong>Password:</strong> ${lastDeviceCreds.password}</p>
|
||||||
|
`;
|
||||||
|
if (lastDeviceCreds.turnEnabled) {
|
||||||
|
credHtml += `<hr style="margin:12px 0;border-color:#e2e8f0">
|
||||||
|
<p style="font-size:13px;color:#64748b;margin-bottom:6px">STUN/TURN (configure in app Network settings)</p>
|
||||||
|
<p><strong>STUN/TURN server:</strong> ${lastDeviceCreds.turnServer}</p>
|
||||||
|
<p><strong>TURN username:</strong> ${lastDeviceCreds.turnUsername}</p>
|
||||||
|
<p><strong>TURN password:</strong> ${lastDeviceCreds.turnPassword}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
document.getElementById('credentials-display').innerHTML = credHtml;
|
document.getElementById('credentials-display').innerHTML = credHtml;
|
||||||
document.getElementById('credentials-modal').classList.add('active');
|
document.getElementById('credentials-modal').classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
@@ -5730,6 +5763,57 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Holds the most recently created device's credentials so the
|
||||||
|
// Copy All / Copy Password buttons don't need to re-parse the DOM.
|
||||||
|
let lastDeviceCreds = null;
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
return navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
// Fallback for non-HTTPS/non-secure contexts where the Clipboard
|
||||||
|
// API is unavailable (e.g. plain-HTTP self-signed-cert access).
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
ta.style.position = 'fixed';
|
||||||
|
ta.style.opacity = '0';
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.focus();
|
||||||
|
ta.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyAllCredentials() {
|
||||||
|
if (!lastDeviceCreds) return;
|
||||||
|
const c = lastDeviceCreds;
|
||||||
|
let text = `Display Name: ${c.name}\n` +
|
||||||
|
`Server: ${c.server}\n` +
|
||||||
|
`Port: ${c.port}\n` +
|
||||||
|
`Transport: ${c.transport}\n` +
|
||||||
|
`Username: ${c.username}\n` +
|
||||||
|
`Password: ${c.password}\n`;
|
||||||
|
if (c.turnEnabled) {
|
||||||
|
text += `STUN/TURN server: ${c.turnServer}\n` +
|
||||||
|
`TURN username: ${c.turnUsername}\n` +
|
||||||
|
`TURN password: ${c.turnPassword}\n`;
|
||||||
|
}
|
||||||
|
copyToClipboard(text)
|
||||||
|
.then(() => showAlert('All settings copied to clipboard', 'success'))
|
||||||
|
.catch(() => showAlert('Copy failed — select and copy manually', 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDevicePassword() {
|
||||||
|
if (!lastDeviceCreds) return;
|
||||||
|
copyToClipboard(lastDeviceCreds.password)
|
||||||
|
.then(() => showAlert('Password copied to clipboard', 'success'))
|
||||||
|
.catch(() => showAlert('Copy failed — select and copy manually', 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteDevice(ext, name) {
|
async function deleteDevice(ext, name) {
|
||||||
if (!confirm(`Delete device ${ext} (${name})?`)) return;
|
if (!confirm(`Delete device ${ext} (${name})?`)) return;
|
||||||
|
|
||||||
@@ -6940,4 +7024,16 @@ if [[ "${1:-}" == "--rebuild-dialplan" ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Non-interactive entry point used by the container entrypoint on every
|
||||||
|
# start. create_web_admin_script() writes /usr/local/bin/easy-asterisk-
|
||||||
|
# webadmin, but that file lives only in the container's writable layer —
|
||||||
|
# it isn't baked into the image or bind-mounted anywhere — so it's wiped
|
||||||
|
# on every recreate. Without regenerating it here, the web admin only
|
||||||
|
# ever starts after someone manually runs it once from the interactive
|
||||||
|
# CLI menu, which defeats the point of it auto-starting at all.
|
||||||
|
if [[ "${1:-}" == "--write-web-admin-script" ]]; then
|
||||||
|
create_web_admin_script
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user