secubox-openwrt/package/secubox/luci-app-secubox-users/root/usr/libexec/rpcd/luci.secubox-users
CyberMind-FR 2bb40d9419 fix(users,routing): Add gitea/jellyfin support and fix mitmproxy routes
secubox-users:
- Add gitea and jellyfin to supported services list
- Add create/update/delete handlers for gitea (via API) and jellyfin
- Update CLI help and status display to include new services

luci-app-secubox-users:
- Add jellyfin service checkbox and badge in frontend
- Update RPCD handler to check jellyfin service status

mitmproxy routing fix:
- nextcloudctl: Use host LAN IP instead of 127.0.0.1 for WAF routes
  (mitmproxy runs in container, can't reach host's localhost)
- metablogizerctl: Same fix for mitmproxy route registration
- mitmproxyctl: Fix sync_metablogizer_routes to use host IP

This fixes 502/403 errors when accessing services through HAProxy->mitmproxy
because the mitmproxy container couldn't route to 127.0.0.1 on the host.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 10:16:07 +01:00

512 lines
16 KiB
Bash

#!/bin/sh
# RPCD handler for SecuBox User Management
. /usr/share/libubox/jshn.sh
CONFIG="secubox-users"
uci_get() { uci -q get ${CONFIG}.$1; }
# Check if service is running
check_service() {
local service="$1"
case "$service" in
nextcloud) [ -x /usr/sbin/nextcloudctl ] && lxc-info -n nextcloud 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;;
peertube) [ -x /usr/sbin/peertubectl ] && lxc-info -n peertube 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;;
matrix) [ -x /usr/sbin/matrixctl ] && lxc-info -n matrix 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;;
jabber) [ -x /usr/sbin/jabberctl ] && lxc-info -n jabber 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;;
email) [ -x /usr/sbin/mailserverctl ] && lxc-info -n mailserver 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;;
gitea) [ -x /usr/sbin/giteactl ] && lxc-info -n gitea 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;;
jellyfin) [ -x /usr/sbin/jellyfinctl ] && lxc-info -n jellyfin 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;;
*) echo "0" ;;
esac
}
get_status() {
local domain=$(uci_get main.domain || echo "secubox.in")
local matrix_server=$(uci_get main.matrix_server || echo "matrix.local")
local user_count=$(uci show ${CONFIG} 2>/dev/null | grep -c "=user$" || echo 0)
local nc_running=$(check_service nextcloud)
local pt_running=$(check_service peertube)
local mx_running=$(check_service matrix)
local jb_running=$(check_service jabber)
local em_running=$(check_service email)
local gt_running=$(check_service gitea)
local jf_running=$(check_service jellyfin)
cat <<EOFJ
{
"domain": "$domain",
"matrix_server": "$matrix_server",
"user_count": $user_count,
"services": {
"nextcloud": $nc_running,
"peertube": $pt_running,
"matrix": $mx_running,
"jabber": $jb_running,
"email": $em_running,
"gitea": $gt_running,
"jellyfin": $jf_running
}
}
EOFJ
}
get_users() {
local users=$(uci show ${CONFIG} 2>/dev/null | grep "=user$" | cut -d'.' -f2 | cut -d'=' -f1)
json_init
json_add_array "users"
for user in $users; do
json_add_object
json_add_string "username" "$user"
json_add_string "email" "$(uci_get ${user}.email)"
json_add_string "enabled" "$(uci_get ${user}.enabled)"
json_add_string "created" "$(uci_get ${user}.created)"
# Get services as array
local services=$(uci -q get ${CONFIG}.${user}.services 2>/dev/null)
json_add_array "services"
for svc in $services; do
json_add_string "" "$svc"
done
json_close_array
json_close_object
done
json_close_array
json_dump
}
add_user() {
read -r input
local username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null)
local password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null)
local services=$(echo "$input" | jsonfilter -e '@.services' 2>/dev/null)
if [ -z "$username" ]; then
echo '{"success":false,"error":"Username required"}'
return
fi
# Generate password if not provided
if [ -z "$password" ]; then
password=$(head -c 12 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16)
fi
# Run secubox-users add
local output
if [ -n "$services" ]; then
output=$(secubox-users add "$username" "$password" "$services" 2>&1)
else
output=$(secubox-users add "$username" "$password" 2>&1)
fi
if echo "$output" | grep -q "USER CREDENTIALS"; then
json_init
json_add_boolean "success" 1
json_add_string "username" "$username"
json_add_string "password" "$password"
json_add_string "email" "${username}@$(uci_get main.domain)"
json_dump
else
json_init
json_add_boolean "success" 0
json_add_string "error" "Failed to create user"
json_add_string "output" "$output"
json_dump
fi
}
delete_user() {
read -r input
local username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null)
if [ -z "$username" ]; then
echo '{"success":false,"error":"Username required"}'
return
fi
local output=$(secubox-users del "$username" 2>&1)
if echo "$output" | grep -q "deleted"; then
echo '{"success":true}'
else
json_init
json_add_boolean "success" 0
json_add_string "error" "Failed to delete user"
json_add_string "output" "$output"
json_dump
fi
}
update_password() {
read -r input
local username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null)
local password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null)
if [ -z "$username" ]; then
echo '{"success":false,"error":"Username required"}'
return
fi
local output
if [ -n "$password" ]; then
output=$(secubox-users passwd "$username" "$password" 2>&1)
else
output=$(secubox-users passwd "$username" 2>&1)
password=$(echo "$output" | grep "Generated password:" | cut -d: -f2 | xargs)
fi
if echo "$output" | grep -q "Password updated"; then
# Sync to all enabled services
local services=$(uci -q get ${CONFIG}.${username}.services 2>/dev/null | tr ' ' '\n' | sort -u)
local synced=""
local failed=""
for svc in $services; do
case "$svc" in
gitea)
local gitea_url=$(uci -q get gitea.main.url 2>/dev/null || echo "http://192.168.255.1:3001")
local gitea_token=$(uci -q get gitea.main.token 2>/dev/null)
if [ -n "$gitea_token" ]; then
local result=$(curl -s -X PATCH \
-H "Authorization: token $gitea_token" \
-H "Content-Type: application/json" \
-d "{\"login_name\":\"$username\",\"password\":\"$password\"}" \
"${gitea_url}/api/v1/admin/users/${username}" 2>/dev/null)
if echo "$result" | grep -q "\"username\""; then
synced="$synced gitea"
else
failed="$failed gitea"
fi
fi
;;
email)
if [ -x /usr/sbin/mailserverctl ]; then
local domain=$(uci -q get ${CONFIG}.main.domain || echo "secubox.in")
local email="${username}@${domain}"
. /usr/lib/mailserver/users.sh 2>/dev/null
if user_passwd "$email" "$password" >/dev/null 2>&1; then
synced="$synced email"
else
failed="$failed email"
fi
fi
;;
jabber)
if [ -x /usr/sbin/jabberctl ]; then
local domain=$(uci -q get ${CONFIG}.main.domain || echo "secubox.in")
if jabberctl user passwd "${username}@${domain}" "$password" >/dev/null 2>&1; then
synced="$synced jabber"
else
failed="$failed jabber"
fi
fi
;;
nextcloud)
if [ -x /usr/sbin/nextcloudctl ]; then
export OC_PASS="$password"
if nextcloudctl occ user:resetpassword "$username" --password-from-env >/dev/null 2>&1; then
synced="$synced nextcloud"
else
failed="$failed nextcloud"
fi
fi
;;
esac
done
logger -t secubox-users "Password updated for $username (synced:$synced failed:$failed)"
json_init
json_add_boolean "success" 1
json_add_string "password" "$password"
json_add_string "synced" "$synced"
json_add_string "failed" "$failed"
json_dump
else
json_init
json_add_boolean "success" 0
json_add_string "error" "Failed to update password"
json_dump
fi
}
# Portal authentication - verify username/password
authenticate() {
read -r input
local username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null)
local password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null)
if [ -z "$username" ] || [ -z "$password" ]; then
echo '{"success":false,"error":"Username and password required"}'
return
fi
# Check user exists and is enabled
local enabled=$(uci -q get ${CONFIG}.${username}.enabled)
if [ "$enabled" != "1" ]; then
echo '{"success":false,"error":"Invalid credentials"}'
return
fi
# Get stored hash and verify
local stored_hash=$(uci -q get ${CONFIG}.${username}.password_hash)
local input_hash=$(echo -n "$password" | sha256sum | cut -d' ' -f1)
if [ "$stored_hash" = "$input_hash" ]; then
local email=$(uci -q get ${CONFIG}.${username}.email)
local token=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 32)
local expiry=$(($(date +%s) + 86400))
# Store session
mkdir -p /tmp/secubox-sessions
echo "${username}:${expiry}" > /tmp/secubox-sessions/${token}
# Log success
logger -t secubox-portal "Login success: $username"
echo "{\"success\":true,\"username\":\"$username\",\"email\":\"$email\",\"token\":\"$token\"}"
else
logger -t secubox-portal "Login failed: $username"
echo '{"success":false,"error":"Invalid credentials"}'
fi
}
# Password recovery - send reset email
recover() {
read -r input
local email=$(echo "$input" | jsonfilter -e '@.email' 2>/dev/null)
if [ -z "$email" ]; then
echo '{"success":false,"error":"Email required"}'
return
fi
# Find user by email
local username=""
for user in $(uci show ${CONFIG} 2>/dev/null | grep "=user$" | cut -d. -f2 | cut -d= -f1); do
local user_email=$(uci -q get ${CONFIG}.${user}.email)
if [ "$user_email" = "$email" ]; then
username="$user"
break
fi
done
if [ -z "$username" ]; then
# Don't reveal if email exists - always return success
echo '{"success":true,"message":"If this email exists, a recovery link has been sent"}'
return
fi
# Generate reset token
local reset_token=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 24)
local expiry=$(($(date +%s) + 3600))
mkdir -p /tmp/secubox-recovery
echo "${username}:${expiry}" > /tmp/secubox-recovery/${reset_token}
# Send recovery email
local domain=$(uci -q get ${CONFIG}.main.domain || echo "secubox.in")
local portal_url="https://portal.${domain}/reset.html?token=${reset_token}"
# Use mailctl if available, otherwise sendmail
if [ -x /usr/sbin/mailctl ]; then
echo -e "Subject: SecuBox Password Recovery\n\nClick here to reset your password:\n${portal_url}\n\nThis link expires in 1 hour." | mailctl send "$email" 2>/dev/null &
fi
logger -t secubox-portal "Recovery email sent: $username"
echo '{"success":true,"message":"If this email exists, a recovery link has been sent"}'
}
# Reset password with token
reset_password() {
read -r input
local token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)
local password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null)
if [ -z "$token" ] || [ -z "$password" ]; then
echo '{"success":false,"error":"Token and password required"}'
return
fi
local token_file="/tmp/secubox-recovery/${token}"
if [ ! -f "$token_file" ]; then
echo '{"success":false,"error":"Invalid or expired token"}'
return
fi
local data=$(cat "$token_file")
local username=$(echo "$data" | cut -d: -f1)
local expiry=$(echo "$data" | cut -d: -f2)
local now=$(date +%s)
if [ "$now" -gt "$expiry" ]; then
rm -f "$token_file"
echo '{"success":false,"error":"Token expired"}'
return
fi
# Update password
local new_hash=$(echo -n "$password" | sha256sum | cut -d' ' -f1)
uci set ${CONFIG}.${username}.password_hash="$new_hash"
uci commit ${CONFIG}
# Cleanup token
rm -f "$token_file"
# Sync to services if enabled
[ "$(uci -q get ${CONFIG}.main.sync_passwords)" = "1" ] && secubox-users sync "$username" "$password" 2>/dev/null &
logger -t secubox-portal "Password reset: $username"
echo '{"success":true,"message":"Password updated"}'
}
# Authenticated password change (user changes their own password)
change_password() {
read -r input
local username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null)
local current_password=$(echo "$input" | jsonfilter -e '@.current_password' 2>/dev/null)
local new_password=$(echo "$input" | jsonfilter -e '@.new_password' 2>/dev/null)
if [ -z "$username" ] || [ -z "$current_password" ] || [ -z "$new_password" ]; then
echo '{"success":false,"error":"Username, current password, and new password required"}'
return
fi
# Validate new password strength
if [ ${#new_password} -lt 8 ]; then
echo '{"success":false,"error":"New password must be at least 8 characters"}'
return
fi
# Check user exists
if ! uci -q get ${CONFIG}.${username} >/dev/null 2>&1; then
echo '{"success":false,"error":"User not found"}'
return
fi
# Verify current password
local stored_hash=$(uci -q get ${CONFIG}.${username}.password_hash)
local input_hash=$(echo -n "$current_password" | sha256sum | cut -d' ' -f1)
if [ "$stored_hash" != "$input_hash" ]; then
logger -t secubox-portal "Password change failed (wrong current password): $username"
echo '{"success":false,"error":"Current password is incorrect"}'
return
fi
# Update password in UCI
local new_hash=$(echo -n "$new_password" | sha256sum | cut -d' ' -f1)
uci set ${CONFIG}.${username}.password_hash="$new_hash"
uci commit ${CONFIG}
# Sync to all enabled services (mailserver, matrix, jabber, etc.)
local services=$(uci -q get ${CONFIG}.${username}.services 2>/dev/null | tr ' ' '\n' | sort -u)
local synced=""
local failed=""
for svc in $services; do
case "$svc" in
email)
if [ -x /usr/sbin/mailserverctl ]; then
local domain=$(uci -q get ${CONFIG}.main.domain || echo "secubox.in")
local email="${username}@${domain}"
# Update mailserver password
. /usr/lib/mailserver/users.sh 2>/dev/null
if user_passwd "$email" "$new_password" >/dev/null 2>&1; then
synced="$synced email"
else
failed="$failed email"
fi
fi
;;
jabber)
if [ -x /usr/sbin/jabberctl ]; then
local domain=$(uci -q get ${CONFIG}.main.domain || echo "secubox.in")
if jabberctl user passwd "${username}@${domain}" "$new_password" >/dev/null 2>&1; then
synced="$synced jabber"
else
failed="$failed jabber"
fi
fi
;;
gitea)
# Sync password to Gitea via API
local gitea_url=$(uci -q get gitea.main.url 2>/dev/null || echo "http://192.168.255.1:3001")
local gitea_token=$(uci -q get gitea.main.token 2>/dev/null)
if [ -n "$gitea_token" ]; then
local result=$(curl -s -X PATCH \
-H "Authorization: token $gitea_token" \
-H "Content-Type: application/json" \
-d "{\"login_name\":\"$username\",\"password\":\"$new_password\"}" \
"${gitea_url}/api/v1/admin/users/${username}" 2>/dev/null)
if echo "$result" | grep -q "\"username\""; then
synced="$synced gitea"
else
failed="$failed gitea"
fi
else
failed="$failed gitea(no-token)"
fi
;;
nextcloud)
if [ -x /usr/sbin/nextcloudctl ]; then
export OC_PASS="$new_password"
if nextcloudctl occ user:resetpassword "$username" --password-from-env >/dev/null 2>&1; then
synced="$synced nextcloud"
else
failed="$failed nextcloud"
fi
fi
;;
matrix)
# Matrix Conduit doesn't have easy password change API
# User will need to change via Element client
failed="$failed matrix(manual)"
;;
peertube)
# PeerTube doesn't have direct password change
failed="$failed peertube(manual)"
;;
esac
done
logger -t secubox-portal "Password changed: $username (synced:$synced failed:$failed)"
if [ -n "$failed" ]; then
echo "{\"success\":true,\"message\":\"Password updated. Some services require manual update:$failed\",\"synced\":\"$synced\",\"manual\":\"$failed\"}"
else
echo "{\"success\":true,\"message\":\"Password updated on all services\",\"synced\":\"$synced\"}"
fi
}
list_methods() {
cat <<'EOFM'
{"status":{},"users":{},"add":{"username":"str","password":"str","services":"str"},"delete":{"username":"str"},"passwd":{"username":"str","password":"str"},"authenticate":{"username":"str","password":"str"},"recover":{"email":"str"},"reset_password":{"token":"str","password":"str"},"change_password":{"username":"str","current_password":"str","new_password":"str"}}
EOFM
}
case "$1" in
list) list_methods ;;
call)
case "$2" in
status) get_status ;;
users) get_users ;;
add) add_user ;;
delete) delete_user ;;
passwd) update_password ;;
authenticate) authenticate ;;
recover) recover ;;
reset_password) reset_password ;;
change_password) change_password ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
*) echo '{"error":"Unknown command"}' ;;
esac