secubox-openwrt/package/secubox/luci-app-secubox-users/root/usr/libexec/rpcd/luci.secubox-users
CyberMind-FR 629c21a75c feat(portal): Add password change feature for authenticated users
- New RPC method `change_password` in luci.secubox-users
  - Verifies current password before allowing change
  - Syncs new password to all enabled services (email, jabber, nextcloud)
  - Matrix/PeerTube require manual password update (noted in response)
- Portal UI updates:
  - New "Account" section with "Change Password" card
  - Password change modal with current/new/confirm fields
  - "My Services" card showing enabled services
- ACL updated to include new authentication methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 12:05:29 +01:00

425 lines
13 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" ;;
*) 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)
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
}
}
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
json_init
json_add_boolean "success" 1
json_add_string "password" "$password"
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
;;
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