#!/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 </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