Fix Caddy routing: use container:port via caddy_net (DoTheEvo pattern)
Undo the host.docker.internal approach from the previous commit — proper Docker networking routes Caddy to services by container name on the shared caddy_net, not via the host gateway. - lib/common.sh: configure_caddy_for_service now accepts either a plain port number (localhost:PORT fallback) or container:port (preferred). The Caddyfile entry uses the container name for direct Docker DNS routing. - services/caddy.sh: remove extra_hosts hack; update Caddyfile template comments to show container_name:port format - All service files: update configure_caddy_for_service calls to pass container_name:internal_port (e.g. "filebrowser:80", "mealie:9000"). Services using network_mode:host keep plain port numbers. - tools/manage_users.sh: new FileBrowser user-management script (deployed to ~/docker/filebrowser/ during installation). Manages users via the FileBrowser REST API: list, add, delete, passwd, scope, info commands. Documents username format (letters/numbers/hyphens/underscores), password rules (min 8 chars, letter + number required), and scope path convention relative to /srv (= FB_PATH on the host). https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29
This commit is contained in:
+15
-4
@@ -261,9 +261,20 @@ write_readme() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── Caddy reverse-proxy wiring (shared by every web service) ─────────────────
|
# ── Caddy reverse-proxy wiring (shared by every web service) ─────────────────
|
||||||
# Usage: configure_caddy_for_service "Name" "PORT" "default-subdomain" ["extra"]
|
# Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"]
|
||||||
|
# UPSTREAM: container:port for caddy_net routing (e.g. "filebrowser:80"),
|
||||||
|
# or plain port number for localhost fallback (e.g. "8085").
|
||||||
configure_caddy_for_service() {
|
configure_caddy_for_service() {
|
||||||
local SERVICE_NAME="$1" SERVICE_PORT="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}"
|
local SERVICE_NAME="$1" SERVICE_UPSTREAM="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}"
|
||||||
|
|
||||||
|
# Derive the proxy upstream and a port number for display messages.
|
||||||
|
# Plain number → localhost:PORT (host-network or legacy services)
|
||||||
|
# name:port → used as-is (preferred: service on shared caddy_net)
|
||||||
|
local _UPSTREAM _DISPLAY_PORT
|
||||||
|
case "$SERVICE_UPSTREAM" in
|
||||||
|
*:*) _UPSTREAM="$SERVICE_UPSTREAM"; _DISPLAY_PORT="${SERVICE_UPSTREAM##*:}" ;;
|
||||||
|
*) _UPSTREAM="localhost:$SERVICE_UPSTREAM"; _DISPLAY_PORT="$SERVICE_UPSTREAM" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Caddy not installed → nothing to do
|
# Caddy not installed → nothing to do
|
||||||
[ -d "$DOCKER_DIR/caddy" ] || return 0
|
[ -d "$DOCKER_DIR/caddy" ] || return 0
|
||||||
@@ -280,7 +291,7 @@ configure_caddy_for_service() {
|
|||||||
prompt_yn "Configure Caddy reverse proxy for $SERVICE_NAME? (y/n):" "n" CONFIGURE_CADDY
|
prompt_yn "Configure Caddy reverse proxy for $SERVICE_NAME? (y/n):" "n" CONFIGURE_CADDY
|
||||||
if [ "$CONFIGURE_CADDY" != "y" ] && [ "$CONFIGURE_CADDY" != "Y" ]; then
|
if [ "$CONFIGURE_CADDY" != "y" ] && [ "$CONFIGURE_CADDY" != "Y" ]; then
|
||||||
echo " Skipping Caddy configuration."
|
echo " Skipping Caddy configuration."
|
||||||
echo " Access $SERVICE_NAME at: http://localhost:$SERVICE_PORT"
|
echo " Access $SERVICE_NAME at: http://localhost:$_DISPLAY_PORT"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -320,7 +331,7 @@ configure_caddy_for_service() {
|
|||||||
|
|
||||||
# $SERVICE_NAME
|
# $SERVICE_NAME
|
||||||
$SERVICE_DOMAIN {
|
$SERVICE_DOMAIN {
|
||||||
reverse_proxy host.docker.internal:$SERVICE_PORT
|
reverse_proxy $_UPSTREAM
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
header {
|
header {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ AB_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$AB_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$AB_DIR"
|
||||||
log_success "Actual Budget configured at $AB_DIR"
|
log_success "Actual Budget configured at $AB_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "ActualBudget" "5006" "budget"
|
configure_caddy_for_service "ActualBudget" "actualbudget:5006" "budget"
|
||||||
|
|
||||||
write_readme "$AB_DIR" << MD
|
write_readme "$AB_DIR" << MD
|
||||||
# Actual Budget
|
# Actual Budget
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ ARM_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ARM_OUTPUT"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ARM_OUTPUT"
|
||||||
log_success "A.R.M. configured at $ARM_DIR"
|
log_success "A.R.M. configured at $ARM_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "A.R.M." "8080" "arm"
|
configure_caddy_for_service "A.R.M." "arm:8080" "arm"
|
||||||
|
|
||||||
write_readme "$ARM_DIR" << MD
|
write_readme "$ARM_DIR" << MD
|
||||||
# A.R.M. (Automatic Ripping Machine)
|
# A.R.M. (Automatic Ripping Machine)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ ABS_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ABS_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ABS_DIR"
|
||||||
log_success "Audiobookshelf configured at $ABS_DIR"
|
log_success "Audiobookshelf configured at $ABS_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "AudioBookshelf" "13378" "audiobooks"
|
configure_caddy_for_service "AudioBookshelf" "audiobookshelf:80" "audiobooks"
|
||||||
|
|
||||||
write_readme "$ABS_DIR" << MD
|
write_readme "$ABS_DIR" << MD
|
||||||
# Audiobookshelf
|
# Audiobookshelf
|
||||||
|
|||||||
+3
-5
@@ -82,8 +82,6 @@ services:
|
|||||||
- ACME_AGREE=true
|
- ACME_AGREE=true
|
||||||
labels:
|
labels:
|
||||||
- "io.podman.annotations.label/crowdsec.enable=true"
|
- "io.podman.annotations.label/crowdsec.enable=true"
|
||||||
extra_hosts:
|
|
||||||
- "host.docker.internal:host-gateway"
|
|
||||||
networks:
|
networks:
|
||||||
- caddy_net
|
- caddy_net
|
||||||
|
|
||||||
@@ -123,7 +121,7 @@ CADDY_COMPOSE
|
|||||||
# Example:
|
# Example:
|
||||||
# myservice.yourdomain.com {
|
# myservice.yourdomain.com {
|
||||||
# import authelia
|
# import authelia
|
||||||
# reverse_proxy host.docker.internal:PORT
|
# reverse_proxy container_name:port
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# ActualBudget
|
# ActualBudget
|
||||||
@@ -133,7 +131,7 @@ CADDY_COMPOSE
|
|||||||
# format json
|
# format json
|
||||||
# level INFO
|
# level INFO
|
||||||
# }
|
# }
|
||||||
# reverse_proxy host.docker.internal:5006
|
# reverse_proxy actualbudget:5006
|
||||||
# header {
|
# header {
|
||||||
# Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
# Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
# X-Frame-Options "SAMEORIGIN"
|
# X-Frame-Options "SAMEORIGIN"
|
||||||
@@ -174,7 +172,7 @@ a web service). You can also edit it by hand:
|
|||||||
|
|
||||||
```
|
```
|
||||||
myservice.example.com {
|
myservice.example.com {
|
||||||
reverse_proxy host.docker.internal:1234
|
reverse_proxy container_name:1234
|
||||||
log {
|
log {
|
||||||
output file /var/log/caddy/myservice.example.com.log
|
output file /var/log/caddy/myservice.example.com.log
|
||||||
format json
|
format json
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ EMBY_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$EMBY_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$EMBY_DIR"
|
||||||
log_success "Emby configured at $EMBY_DIR"
|
log_success "Emby configured at $EMBY_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Emby" "8096" "emby"
|
configure_caddy_for_service "Emby" "emby:8096" "emby"
|
||||||
|
|
||||||
write_readme "$EMBY_DIR" << MD
|
write_readme "$EMBY_DIR" << MD
|
||||||
# Emby
|
# Emby
|
||||||
|
|||||||
+11
-1
@@ -67,10 +67,20 @@ FB_SETTINGS
|
|||||||
|
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR"
|
||||||
|
|
||||||
|
# Deploy user-management helper script
|
||||||
|
local _TOOLS_DIR
|
||||||
|
_TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../tools" 2>/dev/null && pwd)" || true
|
||||||
|
if [ -f "$_TOOLS_DIR/manage_users.sh" ]; then
|
||||||
|
cp "$_TOOLS_DIR/manage_users.sh" "$FB_DIR/manage_users.sh"
|
||||||
|
chmod 750 "$FB_DIR/manage_users.sh"
|
||||||
|
chown "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR/manage_users.sh"
|
||||||
|
log_success "manage_users.sh installed at $FB_DIR/manage_users.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
log_success "Filebrowser configured at $FB_DIR"
|
log_success "Filebrowser configured at $FB_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "FileBrowser" "8085" "files"
|
configure_caddy_for_service "FileBrowser" "filebrowser:80" "files"
|
||||||
|
|
||||||
write_readme "$FB_DIR" << MD
|
write_readme "$FB_DIR" << MD
|
||||||
# FileBrowser
|
# FileBrowser
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ FMD_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FMD_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FMD_DIR"
|
||||||
log_success "FindMyDevice configured at $FMD_DIR"
|
log_success "FindMyDevice configured at $FMD_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "FindMyDevice" "8084" "fmd"
|
configure_caddy_for_service "FindMyDevice" "fmd:8080" "fmd"
|
||||||
|
|
||||||
write_readme "$FMD_DIR" << MD
|
write_readme "$FMD_DIR" << MD
|
||||||
# FindMyDevice (FMD)
|
# FindMyDevice (FMD)
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ FNEOF
|
|||||||
# ── Caddy snippet ──────────────────────────────────────────────────────────
|
# ── Caddy snippet ──────────────────────────────────────────────────────────
|
||||||
if [ -n "$FRIGATE_PUBLIC_URL" ] && [ "$FRIGATE_PUBLIC_URL" != "https://cam.yourdomain.com" ]; then
|
if [ -n "$FRIGATE_PUBLIC_URL" ] && [ "$FRIGATE_PUBLIC_URL" != "https://cam.yourdomain.com" ]; then
|
||||||
local _DOM="${FRIGATE_PUBLIC_URL#https://}"
|
local _DOM="${FRIGATE_PUBLIC_URL#https://}"
|
||||||
configure_caddy_for_service "Frigate" "8971" "frigate-audio" || true
|
configure_caddy_for_service "Frigate" "frigate-audio:8971" "frigate-audio" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ensure_docker_dir_ownership "$DIR"
|
ensure_docker_dir_ownership "$DIR"
|
||||||
|
|||||||
+1
-1
@@ -128,7 +128,7 @@ FRIGATE_CONFIG
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true
|
||||||
log_success "Frigate configured at $FRIGATE_DIR"
|
log_success "Frigate configured at $FRIGATE_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Frigate" "5000" "frigate"
|
configure_caddy_for_service "Frigate" "frigate:5000" "frigate"
|
||||||
|
|
||||||
write_readme "$FRIGATE_DIR" << MD
|
write_readme "$FRIGATE_DIR" << MD
|
||||||
# Frigate NVR
|
# Frigate NVR
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ HA_CONFIG
|
|||||||
echo ""
|
echo ""
|
||||||
log_success "Home Assistant configured at $HOMEASSISTANT_DIR"
|
log_success "Home Assistant configured at $HOMEASSISTANT_DIR"
|
||||||
|
|
||||||
|
if [ "$HA_NETMODE" = "2" ]; then
|
||||||
configure_caddy_for_service "Home Assistant" "8123" "home"
|
configure_caddy_for_service "Home Assistant" "8123" "home"
|
||||||
|
else
|
||||||
|
configure_caddy_for_service "Home Assistant" "homeassistant:8123" "home"
|
||||||
|
fi
|
||||||
|
|
||||||
local START_HA=""
|
local START_HA=""
|
||||||
prompt_yn "Start Home Assistant now? (y/n):" "y" START_HA
|
prompt_yn "Start Home Assistant now? (y/n):" "y" START_HA
|
||||||
|
|||||||
+1
-1
@@ -572,7 +572,7 @@ IMPORT_BODY
|
|||||||
|
|
||||||
log_success "Immich configured at $IMMICH_DIR"
|
log_success "Immich configured at $IMMICH_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Immich" "2283" "immich"
|
configure_caddy_for_service "Immich" "immich-server:2283" "immich"
|
||||||
|
|
||||||
write_readme "$IMMICH_DIR" << MD
|
write_readme "$IMMICH_DIR" << MD
|
||||||
# Immich
|
# Immich
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ JELLYFIN_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$JELLYFIN_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$JELLYFIN_DIR"
|
||||||
log_success "Jellyfin configured at $JELLYFIN_DIR"
|
log_success "Jellyfin configured at $JELLYFIN_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Jellyfin" "8096" "jellyfin"
|
configure_caddy_for_service "Jellyfin" "jellyfin:8096" "jellyfin"
|
||||||
|
|
||||||
write_readme "$JELLYFIN_DIR" << MD
|
write_readme "$JELLYFIN_DIR" << MD
|
||||||
# Jellyfin
|
# Jellyfin
|
||||||
|
|||||||
+1
-1
@@ -275,7 +275,7 @@ COMPOSE
|
|||||||
log_success "js99er configured at $JS99ER_DIR"
|
log_success "js99er configured at $JS99ER_DIR"
|
||||||
|
|
||||||
# ── 5. Reverse proxy (no-ops if Caddy isn't installed locally) ───────────
|
# ── 5. Reverse proxy (no-ops if Caddy isn't installed locally) ───────────
|
||||||
configure_caddy_for_service "js99er" "$JS99ER_PORT" "js99er"
|
configure_caddy_for_service "js99er" "js99er:80" "js99er"
|
||||||
|
|
||||||
# ── 6. Build & start ─────────────────────────────────────────────────────
|
# ── 6. Build & start ─────────────────────────────────────────────────────
|
||||||
local START_JS99ER=""
|
local START_JS99ER=""
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ MM_COMPOSE
|
|||||||
log_success "MagicMirror instance $i configured at $MM_DIR (port $MM_PORT)"
|
log_success "MagicMirror instance $i configured at $MM_DIR (port $MM_PORT)"
|
||||||
|
|
||||||
# Offer Caddy only for first instance
|
# Offer Caddy only for first instance
|
||||||
[ "$i" -eq 1 ] && configure_caddy_for_service "MagicMirror" "$MM_PORT" "mirror"
|
[ "$i" -eq 1 ] && configure_caddy_for_service "MagicMirror" "magicmirror-${MM_PORT}:8080" "mirror"
|
||||||
|
|
||||||
local START_MM=""
|
local START_MM=""
|
||||||
prompt_yn "Start instance $i now? (y/n):" "y" START_MM
|
prompt_yn "Start instance $i now? (y/n):" "y" START_MM
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ MEALIE_COMPOSE
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MEALIE_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MEALIE_DIR"
|
||||||
log_success "Mealie configured at $MEALIE_DIR"
|
log_success "Mealie configured at $MEALIE_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Mealie" "9925" "recipes"
|
configure_caddy_for_service "Mealie" "mealie:9000" "recipes"
|
||||||
|
|
||||||
write_readme "$MEALIE_DIR" << MD
|
write_readme "$MEALIE_DIR" << MD
|
||||||
# Mealie
|
# Mealie
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ MC_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MC_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MC_DIR"
|
||||||
log_success "MeshCentral configured at $MC_DIR"
|
log_success "MeshCentral configured at $MC_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "MeshCentral" "4430" "mesh"
|
configure_caddy_for_service "MeshCentral" "meshcentral:443" "mesh"
|
||||||
|
|
||||||
write_readme "$MC_DIR" << MD
|
write_readme "$MC_DIR" << MD
|
||||||
# MeshCentral
|
# MeshCentral
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ NTFY_ENV
|
|||||||
echo ""
|
echo ""
|
||||||
log_success "ntfy configured at $NTFY_DIR"
|
log_success "ntfy configured at $NTFY_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "ntfy" "8090" "ntfy"
|
configure_caddy_for_service "ntfy" "ntfy:80" "ntfy"
|
||||||
|
|
||||||
write_readme "$NTFY_DIR" << MD
|
write_readme "$NTFY_DIR" << MD
|
||||||
# ntfy
|
# ntfy
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ PORTAINER_COMPOSE
|
|||||||
echo ""
|
echo ""
|
||||||
log_success "Portainer configured at $PORTAINER_DIR"
|
log_success "Portainer configured at $PORTAINER_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Portainer" "9000" "portainer"
|
configure_caddy_for_service "Portainer" "portainer:9000" "portainer"
|
||||||
|
|
||||||
write_readme "$PORTAINER_DIR" << MD
|
write_readme "$PORTAINER_DIR" << MD
|
||||||
# Portainer
|
# Portainer
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ TRACCAR_XML
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TRACCAR_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TRACCAR_DIR"
|
||||||
log_success "Traccar configured at $TRACCAR_DIR"
|
log_success "Traccar configured at $TRACCAR_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Traccar" "8082" "traccar"
|
configure_caddy_for_service "Traccar" "traccar:8082" "traccar"
|
||||||
|
|
||||||
write_readme "$TRACCAR_DIR" << MD
|
write_readme "$TRACCAR_DIR" << MD
|
||||||
# Traccar
|
# Traccar
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ docker compose logs -f # logs
|
|||||||
MD
|
MD
|
||||||
|
|
||||||
# Configure Caddy reverse proxy before starting
|
# Configure Caddy reverse proxy before starting
|
||||||
configure_caddy_for_service "Uptime Kuma" "3001" "uptime"
|
configure_caddy_for_service "Uptime Kuma" "uptime-kuma:3001" "uptime"
|
||||||
|
|
||||||
local START_UPTIME=""
|
local START_UPTIME=""
|
||||||
prompt_yn "Start Uptime Kuma now? (y/n):" "y" START_UPTIME
|
prompt_yn "Start Uptime Kuma now? (y/n):" "y" START_UPTIME
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ WGEASY_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WGEASY_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WGEASY_DIR"
|
||||||
log_success "wg-easy configured at $WGEASY_DIR"
|
log_success "wg-easy configured at $WGEASY_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "wg-easy" "51821" "vpn"
|
configure_caddy_for_service "wg-easy" "wg-easy:51821" "vpn"
|
||||||
|
|
||||||
write_readme "$WGEASY_DIR" << MD
|
write_readme "$WGEASY_DIR" << MD
|
||||||
# wg-easy
|
# wg-easy
|
||||||
|
|||||||
@@ -0,0 +1,380 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 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)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./manage_users.sh list
|
||||||
|
# ./manage_users.sh add <username> <scope> [--admin]
|
||||||
|
# ./manage_users.sh delete <username>
|
||||||
|
# ./manage_users.sh passwd <username>
|
||||||
|
# ./manage_users.sh scope <username> <new-scope>
|
||||||
|
# ./manage_users.sh info <username>
|
||||||
|
#
|
||||||
|
# ── Username rules ────────────────────────────────────────────────────────────
|
||||||
|
# Letters, numbers, hyphens, underscores only. No spaces or dots.
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# If FB_PATH is ~/drives/data1 then:
|
||||||
|
# / → full access to ~/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
|
||||||
|
#
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
FB_URL="${FB_URL:-http://localhost:8085}"
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
info() { echo " $*"; }
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
for cmd in "$@"; do
|
||||||
|
command -v "$cmd" &>/dev/null || die "'$cmd' not found. Install it: sudo apt install $cmd"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_scope() {
|
||||||
|
local s="$1"
|
||||||
|
[[ "$s" == /* ]] || die "Scope must be an absolute path starting with / (e.g. /music or /)"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_password() {
|
||||||
|
local varname="$1" prompt="${2:-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"
|
||||||
|
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" \
|
||||||
|
-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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── API helpers ────────────────────────────────────────────────────────────────
|
||||||
|
api_get() {
|
||||||
|
local token="$1" path="$2"
|
||||||
|
curl -sf -X GET "$FB_URL$path" -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)'
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Commands ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
local token
|
||||||
|
token=$(get_token)
|
||||||
|
echo
|
||||||
|
printf "%-20s %-5s %-30s\n" "USERNAME" "ADMIN" "SCOPE"
|
||||||
|
printf "%-20s %-5s %-30s\n" "--------" "-----" "-----"
|
||||||
|
api_get "$token" "/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"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_add() {
|
||||||
|
local username="$1" scope="$2" is_admin="${3:-false}"
|
||||||
|
validate_username "$username"
|
||||||
|
validate_scope "$scope"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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')
|
||||||
|
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}')
|
||||||
|
|
||||||
|
api_post "$token" "/api/users" "$body" >/dev/null
|
||||||
|
echo
|
||||||
|
info "User '$username' created."
|
||||||
|
info " Scope: $scope"
|
||||||
|
info " Admin: $is_admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_delete() {
|
||||||
|
local username="$1"
|
||||||
|
validate_username "$username"
|
||||||
|
|
||||||
|
local token
|
||||||
|
token=$(get_token)
|
||||||
|
|
||||||
|
local uid
|
||||||
|
uid=$(get_user_id "$token" "$username")
|
||||||
|
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_passwd() {
|
||||||
|
local username="$1"
|
||||||
|
validate_username "$username"
|
||||||
|
|
||||||
|
local token
|
||||||
|
token=$(get_token)
|
||||||
|
|
||||||
|
local uid user
|
||||||
|
uid=$(get_user_id "$token" "$username")
|
||||||
|
user=$(find_user "$token" "$username")
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Changing password for '$username'."
|
||||||
|
echo " Min 8 chars, at least one letter and one number."
|
||||||
|
echo
|
||||||
|
|
||||||
|
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
|
||||||
|
echo
|
||||||
|
info "Password updated for '$username'."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_scope() {
|
||||||
|
local username="$1" new_scope="$2"
|
||||||
|
validate_username "$username"
|
||||||
|
validate_scope "$new_scope"
|
||||||
|
|
||||||
|
local token
|
||||||
|
token=$(get_token)
|
||||||
|
|
||||||
|
local uid user old_scope
|
||||||
|
uid=$(get_user_id "$token" "$username")
|
||||||
|
user=$(find_user "$token" "$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
|
||||||
|
echo
|
||||||
|
info "Scope updated for '$username': $old_scope → $new_scope"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_info() {
|
||||||
|
local username="$1"
|
||||||
|
validate_username "$username"
|
||||||
|
|
||||||
|
local token
|
||||||
|
token=$(get_token)
|
||||||
|
|
||||||
|
local user
|
||||||
|
user=$(find_user "$token" "$username")
|
||||||
|
[[ -n "$user" ]] || die "User '$username' not found."
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "$user" | jq '{
|
||||||
|
username,
|
||||||
|
scope,
|
||||||
|
admin: .perm.admin,
|
||||||
|
create: .perm.create,
|
||||||
|
modify: .perm.modify,
|
||||||
|
delete: .perm.delete,
|
||||||
|
download: .perm.download,
|
||||||
|
execute: .perm.execute
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
FileBrowser user management
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
manage_users.sh list
|
||||||
|
manage_users.sh add <username> <scope> [--admin]
|
||||||
|
manage_users.sh delete <username>
|
||||||
|
manage_users.sh passwd <username>
|
||||||
|
manage_users.sh scope <username> <new-scope>
|
||||||
|
manage_users.sh info <username>
|
||||||
|
|
||||||
|
Scope path 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
|
||||||
|
|
||||||
|
Username: letters, numbers, hyphens, underscores only (no spaces or dots).
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dispatch ──────────────────────────────────────────────────────────────────
|
||||||
|
require_cmd curl jq
|
||||||
|
|
||||||
|
cmd="${1:-help}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
list) cmd_list ;;
|
||||||
|
add)
|
||||||
|
[[ $# -ge 2 ]] || die "Usage: manage_users.sh add <username> <scope> [--admin]"
|
||||||
|
is_admin="false"
|
||||||
|
[[ "${3:-}" == "--admin" ]] && is_admin="true"
|
||||||
|
cmd_add "$1" "$2" "$is_admin"
|
||||||
|
;;
|
||||||
|
delete) [[ $# -ge 1 ]] || die "Usage: manage_users.sh delete <username>"; cmd_delete "$1" ;;
|
||||||
|
passwd) [[ $# -ge 1 ]] || die "Usage: manage_users.sh passwd <username>"; cmd_passwd "$1" ;;
|
||||||
|
scope)
|
||||||
|
[[ $# -ge 2 ]] || die "Usage: manage_users.sh scope <username> <new-scope>"
|
||||||
|
cmd_scope "$1" "$2"
|
||||||
|
;;
|
||||||
|
info) [[ $# -ge 1 ]] || die "Usage: manage_users.sh info <username>"; cmd_info "$1" ;;
|
||||||
|
help|--help|-h) usage ;;
|
||||||
|
*) echo "Unknown command: $cmd"; echo; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user