From 141c9115a77f2455e098472d47a036138106cb6a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 22:52:42 +0000 Subject: [PATCH] Rewrite manage_users.sh: add interactive menu, fix password nameref bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Interactive menu when run with no args (login once, reuse token) - Modify submenu: change username, password, scope, or toggle admin - Fix: prompt_password now uses local -n nameref (bash 4.3+) so the caller's local variable is actually set; printf -v was writing to global scope and being shadowed by the caller's local declaration - One-shot commands unchanged: list/add/delete/passwd/scope/rename/info - FileBrowser only supports one scope per user — documented clearly https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29 --- tools/manage_users.sh | 613 ++++++++++++++++++++++++------------------ 1 file changed, 357 insertions(+), 256 deletions(-) diff --git a/tools/manage_users.sh b/tools/manage_users.sh index d8e3cfa..ff43d91 100644 --- a/tools/manage_users.sh +++ b/tools/manage_users.sh @@ -2,379 +2,480 @@ # manage_users.sh — FileBrowser user management via the REST API. # # Placed in ~/docker/filebrowser/ by the filebrowser installer. -# Requires: curl, jq (apt install curl jq) +# Requires: curl, jq (sudo apt install curl jq) # -# Usage: -# ./manage_users.sh list -# ./manage_users.sh add [--admin] -# ./manage_users.sh delete -# ./manage_users.sh passwd -# ./manage_users.sh scope -# ./manage_users.sh info +# Run with no arguments for the interactive menu. +# Pass a command for one-shot use (see --help). # # ── Username rules ──────────────────────────────────────────────────────────── -# Letters, numbers, hyphens, underscores only. No spaces or dots. +# Letters, numbers, hyphens, underscores only. No spaces, dots, or @. # Examples: alice bob-smith data_user2 # # ── Password rules ──────────────────────────────────────────────────────────── # Minimum 8 characters. No maximum. # Must contain at least one letter and one number. -# Special characters are allowed. # -# ── Scope rules ─────────────────────────────────────────────────────────────── -# Scope is a path INSIDE the container, relative to the FileBrowser root (/srv). -# The volume in docker-compose.yml mounts your host path (FB_PATH) as /srv. +# ── Scope (file path) ───────────────────────────────────────────────────────── +# FileBrowser supports ONE scope path per user. +# Scope is an absolute path inside the container, relative to /srv (= FB_PATH). # -# If FB_PATH is ~/drives/data1 then: -# / → full access to ~/drives/data1 +# If FB_PATH=~/drives/data1: +# / → full access (all of ~/drives/data1) # /music → ~/drives/data1/music only # /docs/bob → ~/drives/data1/docs/bob only # -# Admin account created on first login gets scope / by default. -# -# ── Examples ───────────────────────────────────────────────────────────────── -# Add admin with full access: -# ./manage_users.sh add admin / -# -# Add alice with access to just the music directory: -# ./manage_users.sh add alice /music -# -# Add bob as an admin with full access: -# ./manage_users.sh add bob / --admin -# -# Change alice's password: -# ./manage_users.sh passwd alice -# -# Restrict alice to a subdirectory: -# ./manage_users.sh scope alice /music/alice -# -# List all users: -# ./manage_users.sh list -# -# Delete bob: -# ./manage_users.sh delete bob +# For access to multiple unrelated directories: +# • Set scope to a common parent (e.g. /) +# • Create OS-level symlinks inside the scope dir pointing elsewhere # set -Eeuo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FB_URL="${FB_URL:-http://localhost:8085}" +TOKEN="" -# ── Helpers ─────────────────────────────────────────────────────────────────── -die() { echo "ERROR: $*" >&2; exit 1; } -info() { echo " $*"; } +# ── Output helpers ──────────────────────────────────────────────────────────── +if [[ -t 1 ]]; then + B=$'\e[1m' R=$'\e[0m' GRN=$'\e[32m' RED=$'\e[31m' DIM=$'\e[2m' +else + B="" R="" GRN="" RED="" DIM="" +fi -require_cmd() { - for cmd in "$@"; do - command -v "$cmd" &>/dev/null || die "'$cmd' not found. Install it: sudo apt install $cmd" +die() { echo "${RED}ERROR:${R} $*" >&2; exit 1; } +ok() { echo " ${GRN}✓${R} $*"; } +errmsg() { echo " ${RED}✗${R} $*" >&2; } +hr() { printf ' %s\n' "────────────────────────────────────────────"; } +banner() { echo; hr; printf " ${B}%-44s${R}\n" "$*"; hr; } + +# ── Prerequisites ───────────────────────────────────────────────────────────── +require_cmds() { + for _c in "$@"; do + command -v "$_c" &>/dev/null || die "'$_c' not found — sudo apt install $_c" done } +# ── Validation ──────────────────────────────────────────────────────────────── validate_username() { - local u="$1" - [[ -n "$u" ]] || die "Username cannot be empty." - [[ "$u" =~ ^[a-zA-Z0-9_-]+$ ]] || die "Invalid username '$u'. Only letters, numbers, hyphens, underscores allowed." + [[ -n "$1" ]] || { errmsg "Username cannot be empty."; return 1; } + [[ "$1" =~ ^[a-zA-Z0-9_-]+$ ]] || { + errmsg "Invalid username '$1'. Use only letters, numbers, hyphens, underscores." + return 1 + } } validate_password() { - local p="$1" - [[ ${#p} -ge 8 ]] || die "Password too short (minimum 8 characters)." - [[ "$p" =~ [a-zA-Z] ]] || die "Password must contain at least one letter." - [[ "$p" =~ [0-9] ]] || die "Password must contain at least one number." + [[ ${#1} -ge 8 ]] || { errmsg "Password too short (minimum 8 characters)."; return 1; } + [[ "$1" =~ [a-zA-Z] ]] || { errmsg "Password must contain at least one letter."; return 1; } + [[ "$1" =~ [0-9] ]] || { errmsg "Password must contain at least one number."; return 1; } } validate_scope() { - local s="$1" - [[ "$s" == /* ]] || die "Scope must be an absolute path starting with / (e.g. /music or /)" + [[ "$1" == /* ]] || { errmsg "Scope must start with / (e.g. / or /music or /docs/bob)"; return 1; } } +# prompt_password VARNAME [label] +# Uses nameref (bash 4.3+) so the caller's local variable is set correctly. prompt_password() { - local varname="$1" prompt="${2:-Password}" - local p1 p2 + local -n _pp_ref="$1" + local _label="${2:-New password}" + local _p1 _p2 while true; do - read -r -s -p "$prompt: " p1; echo - read -r -s -p "Confirm: " p2; echo - [[ "$p1" == "$p2" ]] || { echo " Passwords do not match. Try again."; continue; } - validate_password "$p1" - printf -v "$varname" "%s" "$p1" + read -r -s -p " $_label: " _p1; echo + validate_password "$_p1" || continue + read -r -s -p " Confirm: " _p2; echo + [[ "$_p1" == "$_p2" ]] || { errmsg "Passwords do not match. Try again."; continue; } + _pp_ref="$_p1" break done } -# ── Authentication ───────────────────────────────────────────────────────────── -get_token() { - local admin_user admin_pass - read -r -p "FileBrowser admin username [admin]: " admin_user - admin_user="${admin_user:-admin}" - read -r -s -p "FileBrowser admin password: " admin_pass; echo - - local resp - resp=$(curl -s -o /dev/null -w "%{http_code}:%{stderr}" \ - -X POST "$FB_URL/api/login" \ +# ── Auth — login once, reuse token ──────────────────────────────────────────── +ensure_token() { + [[ -n "$TOKEN" ]] && return 0 + echo + echo " ${B}FileBrowser login${R} ${DIM}(${FB_URL})${R}" + local _u _p _tok + read -r -p " Admin username [admin]: " _u + _u="${_u:-admin}" + read -r -s -p " Admin password: " _p; echo + _tok=$(curl -s -X POST "$FB_URL/api/login" \ -H "Content-Type: application/json" \ - -d "{\"username\":\"$admin_user\",\"password\":\"$admin_pass\"}" 2>/dev/null || true) - - local token - token=$(curl -s -X POST "$FB_URL/api/login" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"$admin_user\",\"password\":\"$admin_pass\"}") - - [[ "$token" == *"."*"."* ]] || die "Login failed. Check credentials and that FileBrowser is running." - echo "$token" + -d "{\"username\":\"$_u\",\"password\":\"$_p\"}") + [[ "$_tok" == *"."*"."* ]] \ + || die "Login failed. Check credentials and that FileBrowser is running at $FB_URL" + TOKEN="$_tok" + ok "Logged in as $_u" } -# ── API helpers ──────────────────────────────────────────────────────────────── -api_get() { - local token="$1" path="$2" - curl -sf -X GET "$FB_URL$path" -H "X-Auth: $token" -} +# ── REST wrappers ───────────────────────────────────────────────────────────── +api_get() { curl -sf -X GET "$FB_URL$1" -H "X-Auth: $TOKEN"; } +api_post() { curl -sf -X POST "$FB_URL$1" -H "X-Auth: $TOKEN" \ + -H "Content-Type: application/json" -d "$2"; } +api_put() { curl -sf -X PUT "$FB_URL$1" -H "X-Auth: $TOKEN" \ + -H "Content-Type: application/json" -d "$2"; } +api_delete() { curl -sf -X DELETE "$FB_URL$1" -H "X-Auth: $TOKEN"; } -api_post() { - local token="$1" path="$2" body="$3" - curl -sf -X POST "$FB_URL$path" \ - -H "X-Auth: $token" -H "Content-Type: application/json" -d "$body" -} - -api_put() { - local token="$1" path="$2" body="$3" - curl -sf -X PUT "$FB_URL$path" \ - -H "X-Auth: $token" -H "Content-Type: application/json" -d "$body" -} - -api_delete() { - local token="$1" path="$2" - curl -sf -X DELETE "$FB_URL$path" -H "X-Auth: $token" -} - -# Returns user JSON object for the given username, or empty string if not found. find_user() { - local token="$1" username="$2" - api_get "$token" "/api/users" | jq -r --arg u "$username" '.[] | select(.username==$u)' + api_get "/api/users" | jq -r --arg u "$1" '.[] | select(.username==$u)' } get_user_id() { - local token="$1" username="$2" - local user - user=$(find_user "$token" "$username") - [[ -n "$user" ]] || die "User '$username' not found." - echo "$user" | jq -r '.id' + local _j + _j=$(find_user "$1") + [[ -n "$_j" ]] || { errmsg "User '$1' not found."; return 1; } + echo "$_j" | jq -r '.id' } -# ── Default permissions for new non-admin users ────────────────────────────── default_perms() { - cat <<'JSON' -{ - "admin": false, - "execute": false, - "create": true, - "rename": true, - "modify": true, - "delete": true, - "share": false, - "download": true -} -JSON + echo '{"admin":false,"execute":false,"create":true,"rename":true, + "modify":true,"delete":true,"share":false,"download":true}' } -# ── Commands ────────────────────────────────────────────────────────────────── - +# ── cmd: list ───────────────────────────────────────────────────────────────── cmd_list() { - local token - token=$(get_token) + ensure_token echo - printf "%-20s %-5s %-30s\n" "USERNAME" "ADMIN" "SCOPE" - printf "%-20s %-5s %-30s\n" "--------" "-----" "-----" - api_get "$token" "/api/users" | \ + printf " ${B}%-22s %-5s %s${R}\n" "USERNAME" "ADMIN" "SCOPE" + printf " %-22s %-5s %s\n" "--------" "-----" "-----" + api_get "/api/users" | \ jq -r '.[] | [.username, (if .perm.admin then "yes" else "no" end), .scope] | @tsv' | \ - while IFS=$'\t' read -r uname is_admin scope; do - printf "%-20s %-5s %s\n" "$uname" "$is_admin" "$scope" + while IFS=$'\t' read -r _u _a _s; do + printf " %-22s %-5s %s\n" "$_u" "$_a" "$_s" done + echo } +# ── cmd: add ───────────────────────────────────────────────────────────────── cmd_add() { - local username="$1" scope="$2" is_admin="${3:-false}" - validate_username "$username" - validate_scope "$scope" + local _username="${1:-}" _scope="${2:-}" _is_admin="false" + [[ "${3:-}" == "--admin" ]] && _is_admin="true" - local password - echo - echo "Setting password for new user '$username'." - echo " Min 8 chars, at least one letter and one number." - echo - prompt_password password "New password for $username" + ensure_token - local token - token=$(get_token) - - # Check if user already exists - local existing - existing=$(find_user "$token" "$username") - [[ -z "$existing" ]] || die "User '$username' already exists. Use 'passwd' or 'scope' to modify." - - local perms - perms=$(default_perms) - if [[ "$is_admin" == "true" ]]; then - perms=$(echo "$perms" | jq '.admin = true') + if [[ -z "$_username" ]]; then + echo + echo " ${B}Username:${R} letters, numbers, hyphens, underscores only. No dots or @." + echo " ${B}Password:${R} min 8 chars, at least 1 letter and 1 number." + echo " ${B}Scope:${R} FileBrowser supports ONE path per user." + echo " For multi-dir access: use a common parent or OS symlinks." + echo + read -r -p " Username: " _username + local _adm="" + read -r -p " Admin? [y/N]: " _adm + [[ "${_adm,,}" == "y" ]] && _is_admin="true" fi - local body - body=$(jq -n \ - --arg u "$username" \ - --arg p "$password" \ - --arg s "$scope" \ - --argjson perms "$perms" \ - '{username: $u, password: $p, scope: $s, locale: "en", - viewMode: "list", singleClick: false, sorting: {by: "name", asc: true}, - perm: $perms, commands: [], lockPassword: false, - hideDotfiles: false, dateFormat: false}') + validate_username "$_username" || return 1 + + if [[ -z "$_scope" ]]; then + echo + echo " Scope examples:" + echo " / full access (all of FB_PATH)" + echo " /music music subdir only" + echo " /docs/bob docs/bob subdir only" + echo + read -r -p " Scope for '$_username': " _scope + fi + + validate_scope "$_scope" || return 1 + + local _ex + _ex=$(find_user "$_username") + if [[ -n "$_ex" ]]; then + errmsg "User '$_username' already exists. Use Modify to change it." + return 1 + fi - api_post "$token" "/api/users" "$body" >/dev/null echo - info "User '$username' created." - info " Scope: $scope" - info " Admin: $is_admin" + local password="" + prompt_password password "Password for $_username" + + local _perms _body + _perms=$(default_perms) + [[ "$_is_admin" == "true" ]] && _perms=$(echo "$_perms" | jq '.admin = true') + _body=$(jq -n \ + --arg u "$_username" --arg p "$password" --arg s "$_scope" \ + --argjson perms "$_perms" \ + '{username:$u, password:$p, scope:$s, locale:"en", viewMode:"list", + singleClick:false, sorting:{by:"name",asc:true}, perm:$perms, + commands:[], lockPassword:false, hideDotfiles:false, dateFormat:false}') + api_post "/api/users" "$_body" >/dev/null + echo + ok "User '$_username' created | scope: $_scope | admin: $_is_admin" } +# ── cmd: delete ─────────────────────────────────────────────────────────────── cmd_delete() { - local username="$1" - validate_username "$username" + local _username="${1:-}" + ensure_token - local token - token=$(get_token) + if [[ -z "$_username" ]]; then + cmd_list + read -r -p " Username to delete: " _username + fi + validate_username "$_username" || return 1 - local uid - uid=$(get_user_id "$token" "$username") + local _uid + _uid=$(get_user_id "$_username") || return 1 - local confirm - read -r -p "Delete user '$username' (id=$uid)? [y/N]: " confirm - [[ "${confirm,,}" == "y" ]] || { echo "Aborted."; exit 0; } - - api_delete "$token" "/api/users/$uid" >/dev/null - echo - info "User '$username' deleted." + local _c="" + read -r -p " Delete '$_username' (id $_uid)? [y/N]: " _c + [[ "${_c,,}" == "y" ]] || { echo " Aborted."; return 0; } + api_delete "/api/users/$_uid" >/dev/null + ok "User '$_username' deleted." } +# ── cmd: passwd ─────────────────────────────────────────────────────────────── cmd_passwd() { - local username="$1" - validate_username "$username" + local _username="${1:-}" + ensure_token - local token - token=$(get_token) + if [[ -z "$_username" ]]; then + cmd_list + read -r -p " Username: " _username + fi + validate_username "$_username" || return 1 - local uid user - uid=$(get_user_id "$token" "$username") - user=$(find_user "$token" "$username") + local _uid _user + _uid=$(get_user_id "$_username") || return 1 + _user=$(find_user "$_username") echo - echo "Changing password for '$username'." - echo " Min 8 chars, at least one letter and one number." + echo " ${B}Password rules:${R} min 8 chars, at least 1 letter and 1 number." echo + local password="" + prompt_password password "New password for $_username" - local password - prompt_password password "New password for $username" - - local body - body=$(echo "$user" | jq --arg p "$password" '. + {password: $p}') - api_put "$token" "/api/users/$uid" "$body" >/dev/null + local _body + _body=$(echo "$_user" | jq --arg p "$password" '. + {password: $p}') + api_put "/api/users/$_uid" "$_body" >/dev/null echo - info "Password updated for '$username'." + ok "Password updated for '$_username'." } +# ── cmd: scope ──────────────────────────────────────────────────────────────── cmd_scope() { - local username="$1" new_scope="$2" - validate_username "$username" - validate_scope "$new_scope" + local _username="${1:-}" _new_scope="${2:-}" + ensure_token - local token - token=$(get_token) + if [[ -z "$_username" ]]; then + cmd_list + read -r -p " Username: " _username + fi + validate_username "$_username" || return 1 - local uid user old_scope - uid=$(get_user_id "$token" "$username") - user=$(find_user "$token" "$username") - old_scope=$(echo "$user" | jq -r '.scope') + local _uid _user _old_scope + _uid=$(get_user_id "$_username") || return 1 + _user=$(find_user "$_username") + _old_scope=$(echo "$_user" | jq -r '.scope') - local body - body=$(echo "$user" | jq --arg s "$new_scope" '. + {scope: $s}') - api_put "$token" "/api/users/$uid" "$body" >/dev/null + if [[ -z "$_new_scope" ]]; then + echo + echo " Current scope: $_old_scope" + echo " Note: FileBrowser supports ONE path per user." + echo " Examples: / /music /docs/bob" + echo + read -r -p " New scope: " _new_scope + fi + validate_scope "$_new_scope" || return 1 + + local _body + _body=$(echo "$_user" | jq --arg s "$_new_scope" '. + {scope: $s}') + api_put "/api/users/$_uid" "$_body" >/dev/null echo - info "Scope updated for '$username': $old_scope → $new_scope" + ok "Scope updated for '$_username': $_old_scope → $_new_scope" } +# ── cmd: rename ─────────────────────────────────────────────────────────────── +cmd_rename() { + local _username="${1:-}" _new_username="${2:-}" + ensure_token + + if [[ -z "$_username" ]]; then + cmd_list + read -r -p " Username to rename: " _username + fi + validate_username "$_username" || return 1 + + if [[ -z "$_new_username" ]]; then + read -r -p " New username: " _new_username + fi + validate_username "$_new_username" || return 1 + + local _uid _user _body + _uid=$(get_user_id "$_username") || return 1 + _user=$(find_user "$_username") + _body=$(echo "$_user" | jq --arg u "$_new_username" '. + {username: $u}') + api_put "/api/users/$_uid" "$_body" >/dev/null + ok "Renamed: '$_username' → '$_new_username'" +} + +# ── cmd: info ───────────────────────────────────────────────────────────────── cmd_info() { - local username="$1" - validate_username "$username" + local _username="${1:-}" + ensure_token - local token - token=$(get_token) - - local user - user=$(find_user "$token" "$username") - [[ -n "$user" ]] || die "User '$username' not found." + if [[ -z "$_username" ]]; then + cmd_list + read -r -p " Username: " _username + fi + validate_username "$_username" || return 1 + local _user + _user=$(find_user "$_username") + [[ -n "$_user" ]] || { errmsg "User '$_username' not found."; return 1; } echo - echo "$user" | jq '{ - username, - scope, + echo "$_user" | jq '{username, scope, admin: .perm.admin, create: .perm.create, modify: .perm.modify, delete: .perm.delete, download: .perm.download, - execute: .perm.execute - }' + execute: .perm.execute}' + echo } +# ── Modify submenu ──────────────────────────────────────────────────────────── +menu_modify() { + ensure_token + cmd_list + + local _cur="" + read -r -p " Username to modify: " _cur + validate_username "$_cur" || return 1 + get_user_id "$_cur" >/dev/null || return 1 + + while true; do + local _user _scope _admin + _user=$(find_user "$_cur") || { errmsg "User '$_cur' no longer exists."; break; } + [[ -n "$_user" ]] || { errmsg "User '$_cur' no longer exists."; break; } + _scope=$(echo "$_user" | jq -r '.scope') + _admin=$(echo "$_user" | jq -r 'if .perm.admin then "yes" else "no" end') + + banner "Modify: $_cur" + echo " ${B}Scope:${R} $_scope" + echo " ${B}Admin:${R} $_admin" + echo + echo " 1 Change username" + echo " 2 Change password" + echo " 3 Change scope (file path)" + echo " 4 Toggle admin status" + echo " 0 Back" + echo + local _ch="" + read -r -p " Choice: " _ch + + case "$_ch" in + 1) + local _new_u="" + echo + read -r -p " New username: " _new_u + validate_username "$_new_u" || continue + local _uid1 _body1 + _uid1=$(get_user_id "$_cur") || continue + _body1=$(echo "$_user" | jq --arg u "$_new_u" '. + {username: $u}') + if api_put "/api/users/$_uid1" "$_body1" >/dev/null; then + ok "Renamed: '$_cur' → '$_new_u'" + _cur="$_new_u" + else + errmsg "Rename failed." + fi + ;; + 2) cmd_passwd "$_cur" || true ;; + 3) cmd_scope "$_cur" || true ;; + 4) + local _uid4 + _uid4=$(get_user_id "$_cur") || continue + local _toggled + _toggled=$(echo "$_user" | jq '.perm.admin = (.perm.admin | not)') + if api_put "/api/users/$_uid4" "$_toggled" >/dev/null; then + local _new_admin + _new_admin=$(echo "$_toggled" | jq -r 'if .perm.admin then "yes" else "no" end') + ok "Admin for '$_cur' is now: $_new_admin" + else + errmsg "Toggle failed." + fi + ;; + 0) break ;; + *) errmsg "Invalid choice." ;; + esac + done +} + +# ── usage ───────────────────────────────────────────────────────────────────── usage() { - cat <<'USAGE' + cat <<'EOF' FileBrowser user management -Usage: + Run with no arguments for the interactive menu. + +One-shot usage: manage_users.sh list manage_users.sh add [--admin] manage_users.sh delete manage_users.sh passwd manage_users.sh scope + manage_users.sh rename manage_users.sh info -Scope path is relative to /srv inside the container (= FB_PATH on the host). +Scope is relative to /srv inside the container (= FB_PATH on the host): / full access to everything under FB_PATH - /music only ~/drives/data1/music (if FB_PATH=~/drives/data1) - /docs/bob only ~/drives/data1/docs/bob + /music ~/drives/data1/music only (if FB_PATH=~/drives/data1) + /docs/bob ~/drives/data1/docs/bob only -Username: letters, numbers, hyphens, underscores only (no spaces or dots). +FileBrowser supports ONE scope path per user. +For multi-directory access: use a common parent, or place OS-level +symlinks inside the scope dir pointing to other locations. + +Username: letters, numbers, hyphens, underscores only. No dots or @. Password: min 8 chars, at least one letter and one number. -Examples: - ./manage_users.sh add admin / - ./manage_users.sh add alice /music - ./manage_users.sh add bob / --admin - ./manage_users.sh passwd alice - ./manage_users.sh scope alice /music/alice - ./manage_users.sh list - ./manage_users.sh delete bob -USAGE +Override URL: FB_URL=http://localhost:8085 ./manage_users.sh +EOF } -# ── Dispatch ────────────────────────────────────────────────────────────────── -require_cmd curl jq +# ── Main interactive menu ───────────────────────────────────────────────────── +run_interactive() { + ensure_token + while true; do + banner "FileBrowser User Manager" + echo " ${DIM}${FB_URL}${R}" + echo + echo " 1 List users" + echo " 2 Add user" + echo " 3 Delete user" + echo " 4 Modify user (username / password / scope / admin)" + echo " 5 View user details" + echo " 0 Exit" + echo + local _ch="" + read -r -p " Choice: " _ch + case "$_ch" in + 1) cmd_list || true ;; + 2) cmd_add || true ;; + 3) cmd_delete || true ;; + 4) menu_modify || true ;; + 5) cmd_info || true ;; + 0) echo; echo " Goodbye."; echo; exit 0 ;; + *) errmsg "Invalid choice." ;; + esac + done +} -cmd="${1:-help}" +# ── Entry point ─────────────────────────────────────────────────────────────── +require_cmds curl jq + +_cmd="${1:-}" shift || true -case "$cmd" in - list) cmd_list ;; - add) - [[ $# -ge 2 ]] || die "Usage: manage_users.sh add [--admin]" - is_admin="false" - [[ "${3:-}" == "--admin" ]] && is_admin="true" - cmd_add "$1" "$2" "$is_admin" - ;; - delete) [[ $# -ge 1 ]] || die "Usage: manage_users.sh delete "; cmd_delete "$1" ;; - passwd) [[ $# -ge 1 ]] || die "Usage: manage_users.sh passwd "; cmd_passwd "$1" ;; - scope) - [[ $# -ge 2 ]] || die "Usage: manage_users.sh scope " - cmd_scope "$1" "$2" - ;; - info) [[ $# -ge 1 ]] || die "Usage: manage_users.sh info "; cmd_info "$1" ;; +case "$_cmd" in + "") run_interactive ;; + list) ensure_token; cmd_list ;; + add) ensure_token; cmd_add "$@" ;; + delete|del) ensure_token; cmd_delete "${1:-}" ;; + passwd|pw) ensure_token; cmd_passwd "${1:-}" ;; + scope) ensure_token; cmd_scope "${1:-}" "${2:-}" ;; + rename) ensure_token; cmd_rename "${1:-}" "${2:-}" ;; + info) ensure_token; cmd_info "${1:-}" ;; help|--help|-h) usage ;; - *) echo "Unknown command: $cmd"; echo; usage; exit 2 ;; + *) errmsg "Unknown command: $_cmd"; echo; usage; exit 2 ;; esac