From c72a20af4285aa084ac20a6589122661ae8cfd8f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 03:06:52 +0000 Subject: [PATCH] manage_users.sh: strip to directory-access-only tool Removed all user CRUD (add, delete, rename, passwd, scope change). The FileBrowser web UI handles those. Script is now focused solely on adding/removing extra folder shortcuts for users who have a restricted root directory. Simpler menu, ~half the code. https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt --- tools/manage_users.sh | 689 ++++++++---------------------------------- 1 file changed, 131 insertions(+), 558 deletions(-) diff --git a/tools/manage_users.sh b/tools/manage_users.sh index 280c888..9d62128 100644 --- a/tools/manage_users.sh +++ b/tools/manage_users.sh @@ -1,38 +1,14 @@ #!/usr/bin/env bash -# manage_users.sh — FileBrowser user management via the REST API. +# manage_users.sh — Add/remove extra folder access for FileBrowser users. # # Placed in ~/docker/filebrowser/ by the filebrowser installer. -# Requires: curl, jq (sudo apt install curl jq) +# Requires: curl, jq, docker (sudo apt install curl jq) # -# Run with no arguments for the interactive menu. -# Pass a command for one-shot use (see --help). +# FileBrowser only gives each user one root directory. This script adds +# shortcuts inside that directory so a user can reach multiple folders +# without getting full access to everything. # -# ── Username rules ──────────────────────────────────────────────────────────── -# 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. -# -# ── Directory ───────────────────────────────────────────────────────────────── -# Each user has a starting directory — the root they see when they log in. -# It is a path inside the container relative to /srv. -# -# New layout (fb_users named volume + /srv/data bind mount): -# /data → full access to all files (FB_PATH on the host) -# /alice → alice's folder (must already exist, or be created separately) -# -# Legacy layout (FB_PATH mounted directly at /srv): -# / → full access -# /alice → alice's subfolder inside FB_PATH on the host -# -# ── Additional directories ──────────────────────────────────────────────────── -# Each user has exactly one starting directory, but you can give them access -# to more folders by adding shortcuts inside it. -# FileBrowser follows symlinks, so a shortcut placed in /alice pointing to -# /data/music lets alice browse "music" alongside her own files. -# Shortcuts live in the Docker named volume — no clutter on the host. +# Create and manage users through the FileBrowser web UI instead. # set -Eeuo pipefail @@ -60,42 +36,7 @@ require_cmds() { done } -# ── Validation ──────────────────────────────────────────────────────────────── -validate_username() { - [[ -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() { - [[ ${#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() { - [[ "$1" == /* ]] || { errmsg "Directory must start with / (e.g. / or /alice or /music)"; return 1; } -} - -# prompt_password VARNAME [label] -# Uses nameref (bash 4.3+) so the caller's local variable is set correctly. -prompt_password() { - local -n _pp_ref="$1" - local _label="${2:-New password}" - local _p1 _p2 - while true; do - 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 -} - -# ── Auth — login once, reuse token ──────────────────────────────────────────── +# ── Auth ────────────────────────────────────────────────────────────────────── ensure_token() { [[ -n "$TOKEN" ]] && return 0 echo @@ -105,10 +46,7 @@ ensure_token() { _u="${_u:-admin}" read -r -s -p " Admin password: " _p; echo - # Build JSON with jq so special characters in passwords don't break the payload _payload=$(jq -n --arg u "$_u" --arg p "$_p" '{username:$u,password:$p}') - - # || true prevents set -e from exiting silently on connection refused _tok=$(curl -s -X POST "$FB_URL/api/login" \ -H "Content-Type: application/json" \ -d "$_payload") || true @@ -120,36 +58,13 @@ ensure_token() { ok "Logged in as $_u" else echo " FileBrowser responded: $_tok" >&2 - die "Login failed — wrong credentials, or FileBrowser is not reachable at $FB_URL" + die "Login failed — wrong credentials or FileBrowser not reachable at $FB_URL" fi } -# ── 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"; } - -find_user() { - api_get "/api/users" | jq -r --arg u "$1" '.[] | select(.username==$u)' -} - -get_user_id() { - local _j - _j=$(find_user "$1") - [[ -n "$_j" ]] || { errmsg "User '$1' not found."; return 1; } - echo "$_j" | jq -r '.id' -} - -default_perms() { - echo '{"admin":false,"execute":false,"create":true,"rename":true, - "modify":true,"delete":true,"share":false,"download":true}' -} - -# ── Docker helpers ───────────────────────────────────────────────────────────── +api_get() { curl -sf -X GET "$FB_URL$1" -H "X-Auth: $TOKEN"; } +# ── Docker helpers ──────────────────────────────────────────────────────────── get_container_name() { local _compose="$SCRIPT_DIR/docker-compose.yml" if [[ -f "$_compose" ]]; then @@ -161,14 +76,13 @@ get_container_name() { } check_container() { - require_cmds docker local _running _running=$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null || echo "false") [[ "$_running" == "true" ]] \ - || { errmsg "Container '$1' is not running. Start it: docker compose up -d"; return 1; } + || die "Container '$1' is not running — start it first: docker compose up -d" } -# Returns /srv/data if the new named-volume layout is in use, else /srv (legacy). +# Returns /srv/data (new named-volume layout) or /srv (legacy bind-mount layout). get_data_root() { local _c="$1" if docker exec "$_c" test -d /srv/data 2>/dev/null; then @@ -178,10 +92,43 @@ get_data_root() { fi } -# Print additional directories inside /srv, one per line. -list_links() { - local _c="$1" _scope="$2" - docker exec "$_c" find "/srv$_scope" -maxdepth 1 -type l \ +# ── User selection ──────────────────────────────────────────────────────────── +# Prints a numbered list and returns chosen username + directory via globals. +CHOSEN_USER="" +CHOSEN_DIR="" + +pick_user() { + ensure_token + echo + + local _raw + _raw=$(api_get "/api/users" | jq -r '.[] | [.username, .scope] | @tsv') || true + [[ -n "$_raw" ]] || die "No users returned from FileBrowser." + + local -a _users _dirs + local _i=0 + while IFS=$'\t' read -r _u _s; do + _users+=("$_u") + _dirs+=("$_s") + printf " %2d %-20s %s\n" "$((_i+1))" "$_u" "$_s" + ((_i++)) || true + done <<< "$_raw" + echo + + local _pick="" + read -r -p " Select user (number): " _pick + [[ "$_pick" =~ ^[0-9]+$ ]] || { errmsg "Enter a number."; return 1; } + local _idx=$((_pick-1)) + [[ $_idx -ge 0 && $_idx -lt ${#_users[@]} ]] || { errmsg "Out of range."; return 1; } + + CHOSEN_USER="${_users[$_idx]}" + CHOSEN_DIR="${_dirs[$_idx]}" +} + +# ── Directory listing ───────────────────────────────────────────────────────── +list_extras() { + local _c="$1" _dir="$2" + docker exec "$_c" find "/srv$_dir" -maxdepth 1 -type l \ -exec sh -c ' _name=$(basename "$1") _target=$(readlink "$1") @@ -191,513 +138,139 @@ list_links() { ' _ {} \; 2>/dev/null | sort || true } -# ── prompt_add_dirs SCOPE ───────────────────────────────────────────────────── -# Loop: user types folder names as they appear in FileBrowser — blank to finish. -# Shared by cmd_add (offered inline) and menu_add_dirs (add option). -prompt_add_dirs() { - local _scope="$1" - local _c _data_root - _c=$(get_container_name) - check_container "$_c" || return 1 - _data_root=$(get_data_root "$_c") +# ── Add extra directories ───────────────────────────────────────────────────── +add_extras() { + local _dir="$1" _c="$2" _data_root="$3" + local _scope_dir="/srv$_dir" - local _scope_dir="/srv$_scope" if ! docker exec "$_c" test -d "$_scope_dir" 2>/dev/null; then echo - echo " The directory '$_scope' does not exist in the container yet." + echo " The directory '$_dir' does not exist in the container yet." local _mk="" read -r -p " Create it now? [y/N]: " _mk if [[ "${_mk,,}" == "y" ]]; then - if ! docker exec "$_c" mkdir -p "$_scope_dir" 2>/dev/null; then - errmsg "Could not create '$_scope_dir' in container (permission denied?)." - return 1 - fi - ok "Created '$_scope'" + docker exec "$_c" mkdir -p "$_scope_dir" \ + || die "Could not create '$_scope_dir' (permission denied?)." + ok "Created '$_dir'" else - errmsg "Cannot add directories — '$_scope' must exist first." + errmsg "Cannot add extras — '$_dir' must exist first." return 1 fi fi echo - echo " Type a folder name to add — ? to list available folders, blank when done." + echo " Type a folder name to add — ? to list available, blank when done." echo while true; do local _src="" - read -r -p " Directory to add [done]: " _src + read -r -p " Folder to add [done]: " _src [[ -n "$_src" ]] || break if [[ "$_src" == "?" ]]; then local _avail _avail=$(docker exec "$_c" find "$_data_root" -maxdepth 1 -mindepth 1 \ - \( -type d -o -type l \) \ - -not -name ".*" 2>/dev/null \ + \( -type d -o -type l \) -not -name ".*" 2>/dev/null \ | sed "s|^${_data_root}/||" | sort | tr '\n' ' ') || true - echo " Available: ${_avail:-(none — no subdirectories found)}" + echo " Available: ${_avail:-(none found)}" echo continue fi - # Normalise: strip surrounding slashes - _src="${_src#/}" - _src="${_src%/}" + _src="${_src#/}"; _src="${_src%/}" [[ -n "$_src" ]] || continue local _target="$_data_root/$_src" - local _link_name - _link_name=$(basename "$_src") + local _link_name; _link_name=$(basename "$_src") + local _link_path="$_scope_dir/$_link_name" if ! docker exec "$_c" test -e "$_target" 2>/dev/null; then errmsg "'$_src' not found — type ? to see available folders." continue fi - - local _link_path="$_scope_dir/$_link_name" if docker exec "$_c" test -e "$_link_path" 2>/dev/null; then - errmsg "'$_link_name' already added — use Remove to clear it first." + errmsg "'$_link_name' already added." continue fi - if ! docker exec "$_c" ln -s "$_target" "$_link_path" 2>/dev/null; then - errmsg "Failed to add directory (check: docker logs filebrowser)." + errmsg "Failed to add '$_link_name' (check: docker logs filebrowser)." continue fi - if [[ "$_src" == "$_link_name" ]]; then - ok "Added '$_link_name' to user's view" - else - ok "Added '$_link_name' ${DIM}(from $_src)${R}" - fi + ok "Added '$_link_name'" done } -# ── Additional directories submenu (shown from Modify option 5) ────────────── -menu_add_dirs() { - local _scope="$1" - local _c - _c=$(get_container_name) - check_container "$_c" || return 1 +# ── Remove extra directory ──────────────────────────────────────────────────── +remove_extra() { + local _dir="$1" _c="$2" + local _scope_dir="/srv$_dir" - while true; do - banner "Additional directories ($_scope)" - echo " Extra folders this user can access:" - echo - local _links - _links=$(list_links "$_c" "$_scope") - if [[ -n "$_links" ]]; then - echo "$_links" - else - echo " (none)" - fi - echo - echo " 1 Add a directory" - echo " 2 Remove a directory" - echo " 0 Back" - echo - local _ch="" - read -r -p " Choice: " _ch - case "$_ch" in - 1) prompt_add_dirs "$_scope" || true ;; - 2) - local _link_name="" - echo - read -r -p " Directory name to remove: " _link_name - [[ -n "$_link_name" ]] || continue - local _link_path="/srv${_scope}/${_link_name}" - if ! docker exec "$_c" test -L "$_link_path" 2>/dev/null; then - errmsg "'$_link_name' is not an added directory — refusing to delete." - continue - fi - docker exec "$_c" rm "$_link_path" - ok "'$_link_name' removed." - ;; - 0) break ;; - *) errmsg "Invalid choice." ;; - esac - done -} - -# ── cmd: list ───────────────────────────────────────────────────────────────── -cmd_list() { - ensure_token echo - printf " ${B}%-22s %-5s %s${R}\n" "USERNAME" "ADMIN" "DIRECTORY" - 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 _u _a _s; do - printf " %-22s %-5s %s\n" "$_u" "$_a" "$_s" - done + local _links + _links=$(list_extras "$_c" "$_dir") + if [[ -z "$_links" ]]; then + echo " No extras to remove." + return 0 + fi + echo "$_links" echo -} -# ── cmd: add ───────────────────────────────────────────────────────────────── -cmd_add() { - local _username="${1:-}" _scope="${2:-}" _is_admin="false" - [[ "${3:-}" == "--admin" ]] && _is_admin="true" + local _name="" + read -r -p " Name to remove: " _name + [[ -n "$_name" ]] || return 0 - ensure_token - - 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}Directory:${R} the folder the user sees as their root — add extras after." - echo - read -r -p " Username: " _username - local _adm="" - read -r -p " Admin? [y/N]: " _adm - [[ "${_adm,,}" == "y" ]] && _is_admin="true" - fi - - validate_username "$_username" || return 1 - - if [[ -z "$_scope" ]]; then - echo - echo " Starting directory — what the user sees as their root when they log in:" - echo " /data full access to all files (new layout)" - echo " / full access to all files (legacy layout)" - echo " /$_username this user's own folder (must already exist)" - echo " /music music folder only" - echo - read -r -p " Directory for '$_username': " _scope - _scope="/${_scope#/}" # ensure leading / - 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." + local _link_path="$_scope_dir/$_name" + if ! docker exec "$_c" test -L "$_link_path" 2>/dev/null; then + errmsg "'$_name' is not an added folder — not removing." return 1 fi + docker exec "$_c" rm "$_link_path" + ok "'$_name' removed." +} +# ── Main ────────────────────────────────────────────────────────────────────── +require_cmds curl jq docker + +_c=$(get_container_name) +check_container "$_c" +_data_root=$(get_data_root "$_c") + +while true; do + banner "FileBrowser — Extra Folder Access" + echo " ${DIM}Manage additional folders for users beyond their root directory.${R}" + echo " ${DIM}Create/delete users in the FileBrowser web UI.${R}" echo - 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", - perm:$perms, commands:[], lockPassword:false}') - - local _resp _http_code - _resp=$(curl -s -w '\n%{http_code}' -X POST "$FB_URL/api/users" \ - -H "X-Auth: $TOKEN" \ - -H "Content-Type: application/json" \ - -d "$_body" 2>&1) || true - _http_code=$(printf '%s' "$_resp" | tail -1) - _resp=$(printf '%s' "$_resp" | head -n -1) - - if [[ "$_http_code" != "200" && "$_http_code" != "201" ]]; then - local _msg - _msg=$(printf '%s' "$_resp" | jq -r '.message // .error // empty' 2>/dev/null) || true - [[ -z "$_msg" ]] && _msg="$_resp" - [[ -z "$_msg" ]] && _msg="HTTP $_http_code" - errmsg "Failed to create user '$_username': $_msg" - return 1 - fi - + echo " 1 Add extra folders to a user" + echo " 2 Remove an extra folder from a user" + echo " 3 Show a user's extra folders" + echo " 0 Exit" echo - ok "User '$_username' created | directory: $_scope | admin: $_is_admin" + _ch="" + read -r -p " Choice: " _ch - # Offer additional directories when user has a private home (not full access) - if command -v docker &>/dev/null && [[ "$_scope" != "/" && "$_scope" != "/data" ]]; then - local _do_dirs="" - read -r -p " Add additional directories for '$_username'? [y/N]: " _do_dirs - if [[ "${_do_dirs,,}" == "y" ]]; then - prompt_add_dirs "$_scope" || true - fi - fi -} - -# ── cmd: delete ─────────────────────────────────────────────────────────────── -cmd_delete() { - local _username="${1:-}" - ensure_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 "$_username") || return 1 - - 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." - echo " ${DIM}Note: their directory and any added extras still exist in the Docker volume.${R}" -} - -# ── cmd: passwd ─────────────────────────────────────────────────────────────── -cmd_passwd() { - local _username="${1:-}" - ensure_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 "$_username") || return 1 - _user=$(find_user "$_username") - - echo - 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 _body - _body=$(echo "$_user" | jq --arg p "$password" '. + {password: $p}') - api_put "/api/users/$_uid" "$_body" >/dev/null - echo - ok "Password updated for '$_username'." -} - -# ── cmd: scope ──────────────────────────────────────────────────────────────── -cmd_scope() { - local _username="${1:-}" _new_scope="${2:-}" - ensure_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 "$_username") || return 1 - _user=$(find_user "$_username") - _old_scope=$(echo "$_user" | jq -r '.scope') - - if [[ -z "$_new_scope" ]]; then - echo - echo " Current directory: $_old_scope" - echo " ${DIM}Additional directories from the old location are not moved automatically.${R}" - echo - read -r -p " New directory: " _new_scope - _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 - ok "Directory updated for '$_username': $_old_scope → $_new_scope" - - # Offer to add directories into the new scope - if command -v docker &>/dev/null && [[ "$_new_scope" != "/" && "$_new_scope" != "/data" ]]; then - local _do_dirs="" - read -r -p " Add additional directories into '$_new_scope'? [y/N]: " _do_dirs - if [[ "${_do_dirs,,}" == "y" ]]; then - prompt_add_dirs "$_new_scope" || true - fi - fi -} - -# ── 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:-}" - ensure_token - - 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, - admin: .perm.admin, - create: .perm.create, - modify: .perm.modify, - delete: .perm.delete, - download: .perm.download, - 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}Directory:${R} $_scope" - echo " ${B}Admin:${R} $_admin" - echo - echo " 1 Change username" - echo " 2 Change password" - echo " 3 Change directory" - echo " 4 Toggle admin status" - echo " 5 Additional directories ${DIM}(add/remove extra folder access)${R}" - 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 - ;; - 5) menu_add_dirs "$_scope" || true ;; - 0) break ;; - *) errmsg "Invalid choice." ;; - esac - done -} - -# ── usage ───────────────────────────────────────────────────────────────────── -usage() { - cat <<'EOF' -FileBrowser user management - - 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 - -Directory is a path relative to /srv inside the container. - New layout: /data = full access, /alice = alice's folder - Legacy: / = full access, /alice = alice's folder -Username: letters, numbers, hyphens, underscores only. No dots or @. -Password: min 8 chars, at least one letter and one number. - -Additional directories: use the interactive menu — extras -are offered automatically when you add a user or change their directory. - -Override URL: FB_URL=http://localhost:8085 ./manage_users.sh -EOF -} - -# ── 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 / directory / admin / extras)" - 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 -} - -# ── Entry point ─────────────────────────────────────────────────────────────── -require_cmds curl jq - -_cmd="${1:-}" -shift || true - -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 ;; - *) errmsg "Unknown command: $_cmd"; echo; usage; exit 2 ;; -esac + case "$_ch" in + 1) + pick_user || continue + banner "Add extras — $CHOSEN_USER (${CHOSEN_DIR})" + if [[ "$CHOSEN_DIR" == "/" || "$CHOSEN_DIR" == "/data" ]]; then + echo " This user has full access — no extras needed." + else + add_extras "$CHOSEN_DIR" "$_c" "$_data_root" || true + fi + ;; + 2) + pick_user || continue + banner "Remove extra — $CHOSEN_USER (${CHOSEN_DIR})" + remove_extra "$CHOSEN_DIR" "$_c" || true + ;; + 3) + pick_user || continue + banner "Extras — $CHOSEN_USER (${CHOSEN_DIR})" + _links=$(list_extras "$_c" "$CHOSEN_DIR") + if [[ -n "$_links" ]]; then echo "$_links"; else echo " (none)"; fi + echo + ;; + 0) echo; echo " Goodbye."; echo; exit 0 ;; + *) errmsg "Invalid choice." ;; + esac +done