secubox-openwrt/package/secubox/luci-app-secubox-users/root/usr/libexec/rpcd/luci.secubox-users
CyberMind-FR e7b6039c96 feat(portal): Add SSO authentication with SecuBox core users
Portal login now authenticates against SecuBox users (UCI config)
instead of hardcoded credentials.

New RPCD methods in luci.secubox-users:
- authenticate: Verify username/password, return session token
- recover: Send password reset email
- reset_password: Set new password with recovery token

Portal pages:
- login.html: Login form with password recovery link
- reset.html: Password reset form (from email link)

Features:
- SHA256 password hashing
- Session tokens stored in /tmp/secubox-sessions/
- Email-based password recovery via mailctl
- Public ACL access (no LuCI login required)
- Passwords synced to services if sync_passwords=1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 12:58:38 +01:00

324 lines
9.2 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"}'
}
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"}}
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 ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
*) echo '{"error":"Unknown command"}' ;;
esac