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>
512 lines
16 KiB
Bash
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
|