add the emergency relocate/reset-mfa scripts the tests exercise
This commit is contained in:
7 files changed
+621
No files matched your search
Executable
+236
@@ -0,0 +1,236 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Shared helpers for relocate-stack-paths (SQLite + PostgreSQL variants source this).
|
||||
# Contains the pure re-anchor rule and the interactive/dry-run presentation, so the
|
||||
# only difference between the two DB variants is how they read/write the database.
|
||||
#
|
||||
# The DB variant must define, before sourcing this file:
|
||||
# db_list_stack_paths -> prints one row per (compose|env) path as:
|
||||
# <id>\t<field>\t<label>\t<stored_path>
|
||||
# (field = "compose" or "env"; label = human name)
|
||||
# db_list_external_paths -> prints one line per external_stack_paths entry:
|
||||
# <index>\t<stored_path>
|
||||
# db_update_stack_path <id> <field> <new_path> -> UPDATE that one column
|
||||
# db_update_external_path <index> <new_path> -> replace that array entry
|
||||
#
|
||||
# It reads: OLD_DIR, NEW_DIR, MODE (dry|apply), APPLY_ALL (0|1).
|
||||
|
||||
# --- pure re-anchor rule: rewrite a stored path from the old DATA_DIR prefix to the
|
||||
# new one, matching only on a full path segment (so /app/data never matches
|
||||
# /app/database), with a single trailing slash stripped from each dir. ---
|
||||
|
||||
# strip a single trailing slash
|
||||
_strip_trailing_slash() { printf '%s' "$1" | sed 's:/*$::'; }
|
||||
|
||||
# rewrite_under_data_dir <stored> <oldDir> <newDir>
|
||||
# Prints the rewritten path, or nothing when <stored> is not under <oldDir> (segment
|
||||
# boundary, so /app/data never matches /app/database) or the rewrite is a no-op.
|
||||
rewrite_under_data_dir() {
|
||||
stored="$1"; old="$(_strip_trailing_slash "$2")"; new="$(_strip_trailing_slash "$3")"
|
||||
[ -z "$stored" ] && return 0
|
||||
[ -z "$old" ] && return 0
|
||||
[ "$old" = "$new" ] && return 0
|
||||
if [ "$stored" = "$old" ]; then printf '%s' "$new"; return 0; fi
|
||||
case "$stored" in
|
||||
"$old"/*) printf '%s%s' "$new" "${stored#"$old"}" ;;
|
||||
*) return 0 ;; # not under old dir - leave it
|
||||
esac
|
||||
}
|
||||
|
||||
# classify_path <stored> : echoes "STATUS\tNEWPATH\tREASON"
|
||||
# WILL_UPDATE <new> "" - old missing AND new exists
|
||||
# SKIP "" outside - not under the old DATA_DIR (NEWPATH is empty)
|
||||
# SKIP <new> oldexists - old file still present (adopted/not moved)
|
||||
# SKIP <new> newmissing - rewritten target not on disk yet
|
||||
classify_path() {
|
||||
stored="$1"
|
||||
new="$(rewrite_under_data_dir "$stored" "$OLD_DIR" "$NEW_DIR")"
|
||||
if [ -z "$new" ]; then
|
||||
printf 'SKIP\t\toutside'
|
||||
return 0
|
||||
fi
|
||||
if [ -e "$stored" ]; then
|
||||
printf 'SKIP\t%s\toldexists' "$new"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -e "$new" ]; then
|
||||
printf 'SKIP\t%s\tnewmissing' "$new"
|
||||
return 0
|
||||
fi
|
||||
printf 'WILL_UPDATE\t%s\t' "$new"
|
||||
}
|
||||
|
||||
_reason_text() {
|
||||
case "$1" in
|
||||
outside) printf 'outside the old DATA_DIR - left as-is' ;;
|
||||
oldexists) printf 'old file still exists - not stale, left as-is' ;;
|
||||
newmissing) printf 'new file not found (copy the files across first)' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- header ---
|
||||
print_header() {
|
||||
echo "========================================================================"
|
||||
echo " Dockhand - Relocate stack paths ($DB_LABEL)"
|
||||
echo "========================================================================"
|
||||
echo ""
|
||||
echo " Updates where Dockhand LOOKS FOR each stack's compose/env files in its"
|
||||
echo " database, after you changed DATA_DIR."
|
||||
echo ""
|
||||
echo " >> It does NOT move, copy, or delete any files. Your files are untouched."
|
||||
echo " It only rewrites the stored path in the database. A pointer is changed"
|
||||
echo " only when the old file is gone AND the new file already exists on disk."
|
||||
echo ""
|
||||
echo " Old DATA_DIR: $OLD_DIR"
|
||||
echo " New DATA_DIR: $NEW_DIR"
|
||||
[ -n "$DB_DISPLAY" ] && echo " Database: $DB_DISPLAY"
|
||||
echo ""
|
||||
if [ "$MODE" = "apply" ]; then
|
||||
echo " >> Make a database backup before proceeding if you want a safety net."
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# --- main pass: build a work list, present it, and (in apply mode) apply it ---
|
||||
|
||||
run_relocation() {
|
||||
will=0; applied=0; declined=0
|
||||
# Collect WILL_UPDATE items into a temp file: <kind>\t<id-or-index>\t<field>\t<label>\t<old>\t<new>
|
||||
worklist="$(mktemp)"
|
||||
|
||||
print_header
|
||||
echo "------------------------------------------------------------------------"
|
||||
|
||||
# stack_sources rows
|
||||
db_list_stack_paths | while IFS=' ' read -r id field label stored; do
|
||||
[ -z "$stored" ] && continue
|
||||
res="$(classify_path "$stored")"
|
||||
status="$(printf '%s' "$res" | cut -f1)"
|
||||
new="$(printf '%s' "$res" | cut -f2)"
|
||||
reason="$(printf '%s' "$res" | cut -f3)"
|
||||
if [ "$status" = "WILL_UPDATE" ]; then
|
||||
printf 'stack\t%s\t%s\t%s\t%s\t%s\n' "$id" "$field" "$label" "$stored" "$new" >> "$worklist"
|
||||
printf ' %-22s %-8s WILL UPDATE\n' "$label" "$field"
|
||||
printf ' %s\n -> %s\n' "$stored" "$new"
|
||||
else
|
||||
printf ' %-22s %-8s SKIP (%s)\n' "$label" "$field" "$(_reason_text "$reason")"
|
||||
printf ' %s\n' "$stored"
|
||||
fi
|
||||
done
|
||||
|
||||
# external_stack_paths entries
|
||||
db_list_external_paths | while IFS=' ' read -r idx stored; do
|
||||
[ -z "$stored" ] && continue
|
||||
res="$(classify_path "$stored")"
|
||||
status="$(printf '%s' "$res" | cut -f1)"
|
||||
new="$(printf '%s' "$res" | cut -f2)"
|
||||
reason="$(printf '%s' "$res" | cut -f3)"
|
||||
if [ "$status" = "WILL_UPDATE" ]; then
|
||||
# field is '-' (a placeholder) because a tab-IFS `read` collapses an EMPTY
|
||||
# field and would shift every later column.
|
||||
printf 'external\t%s\t-\texternal_stack_paths[%s]\t%s\t%s\n' "$idx" "$idx" "$stored" "$new" >> "$worklist"
|
||||
printf ' %-22s %-8s WILL UPDATE\n' "external_stack_paths[$idx]" ""
|
||||
printf ' %s\n -> %s\n' "$stored" "$new"
|
||||
else
|
||||
printf ' %-22s %-8s SKIP (%s)\n' "external_stack_paths[$idx]" "" "$(_reason_text "$reason")"
|
||||
printf ' %s\n' "$stored"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "------------------------------------------------------------------------"
|
||||
will="$(wc -l < "$worklist" | tr -d ' ')"
|
||||
|
||||
if [ "$MODE" = "dry" ]; then
|
||||
echo " Summary: $will will update."
|
||||
echo ""
|
||||
echo " This was a DRY RUN. Nothing was changed."
|
||||
echo " To apply, re-run with --apply (review each change), or --apply --all."
|
||||
echo "========================================================================"
|
||||
rm -f "$worklist"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$will" -eq 0 ]; then
|
||||
echo " Nothing to update."
|
||||
echo "========================================================================"
|
||||
rm -f "$worklist"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Interactive review needs a terminal. Without one (e.g. `docker exec` with no
|
||||
# -it), tell the user how to proceed instead of erroring on each prompt.
|
||||
if [ "$APPLY_ALL" -ne 1 ] && [ ! -r /dev/tty ]; then
|
||||
echo " No terminal available for interactive review."
|
||||
echo " Re-run with 'docker exec -it ...' to confirm each change, or add --all"
|
||||
echo " to apply all $will change(s) without prompting."
|
||||
echo "========================================================================"
|
||||
rm -f "$worklist"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# apply mode. Read the work list on FD 3, not stdin, so a db_update_* helper that
|
||||
# happens to touch stdin (e.g. a client that reads it) can't swallow the list.
|
||||
i=0
|
||||
while IFS=' ' read -r kind key field label old new <&3; do
|
||||
i=$((i + 1))
|
||||
show_field="$field"; [ "$show_field" = "-" ] && show_field=""
|
||||
printf '\n [%s/%s] %s %s\n' "$i" "$will" "$label" "$show_field"
|
||||
printf ' %s\n -> %s\n' "$old" "$new"
|
||||
if [ "$APPLY_ALL" -eq 1 ]; then
|
||||
ans="y"
|
||||
else
|
||||
printf ' Apply this change? [y/n/q] '
|
||||
read -r ans </dev/tty
|
||||
fi
|
||||
case "$ans" in
|
||||
y|Y)
|
||||
if [ "$kind" = "stack" ]; then
|
||||
db_update_stack_path "$key" "$field" "$new"
|
||||
else
|
||||
db_update_external_path "$key" "$new"
|
||||
fi
|
||||
applied=$((applied + 1))
|
||||
echo " ... updated."
|
||||
;;
|
||||
q|Q)
|
||||
echo " ... quit."
|
||||
break
|
||||
;;
|
||||
*)
|
||||
declined=$((declined + 1))
|
||||
echo " ... skipped."
|
||||
;;
|
||||
esac
|
||||
done 3< "$worklist"
|
||||
|
||||
echo ""
|
||||
echo " Done. $applied applied, $declined skipped by you."
|
||||
echo " Restart Dockhand to pick up the changes."
|
||||
echo "========================================================================"
|
||||
rm -f "$worklist"
|
||||
}
|
||||
|
||||
# --- argument parsing shared by both variants ---
|
||||
parse_relocate_args() {
|
||||
OLD_DIR=""; NEW_DIR=""; MODE="dry"; APPLY_ALL=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--apply) MODE="apply" ;;
|
||||
--all) APPLY_ALL=1 ;;
|
||||
--*) echo "Unknown option: $arg"; exit 2 ;;
|
||||
*)
|
||||
if [ -z "$OLD_DIR" ]; then OLD_DIR="$arg"
|
||||
elif [ -z "$NEW_DIR" ]; then NEW_DIR="$arg"
|
||||
else echo "Unexpected argument: $arg"; exit 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ -z "$OLD_DIR" ] || [ -z "$NEW_DIR" ]; then
|
||||
echo "Usage: relocate-stack-paths.sh <old-data-dir> <new-data-dir> [--apply] [--all]"
|
||||
exit 2
|
||||
fi
|
||||
case "$OLD_DIR" in /*) ;; *) echo "Error: old-data-dir must be an absolute path"; exit 2 ;; esac
|
||||
case "$NEW_DIR" in /*) ;; *) echo "Error: new-data-dir must be an absolute path"; exit 2 ;; esac
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PostgreSQL: relocate stored stack paths after a DATA_DIR change.
|
||||
# See ../relocate-stack-paths.sh for usage. Does NOT move files - updates DB only.
|
||||
# Requires DATABASE_URL.
|
||||
|
||||
DB_LABEL="PostgreSQL"
|
||||
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "Error: DATABASE_URL environment variable not set"
|
||||
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v psql >/dev/null 2>&1; then
|
||||
echo "Error: psql not found. Install the postgresql-client package."
|
||||
exit 1
|
||||
fi
|
||||
DB_DISPLAY="$(printf '%s' "$DATABASE_URL" | sed 's#://[^@]*@#://***@#')" # hide credentials
|
||||
|
||||
# Fail loudly on a query error rather than silently returning an empty list (which
|
||||
# would read as "nothing to do"). ON_ERROR_STOP + no 2>/dev/null on reads.
|
||||
_psql() { psql "$DATABASE_URL" -v ON_ERROR_STOP=1 "$@"; }
|
||||
|
||||
# SQL-escape for single-quoted literals.
|
||||
_sql_escape() { printf "%s" "$1" | sed "s/'/''/g"; }
|
||||
|
||||
db_list_stack_paths() {
|
||||
_psql -t -A -F ' ' -c "
|
||||
SELECT s.id, 'compose',
|
||||
s.stack_name || CASE WHEN e.name IS NOT NULL THEN ' (' || e.name || ')' ELSE '' END,
|
||||
s.compose_path
|
||||
FROM stack_sources s LEFT JOIN environments e ON e.id = s.environment_id
|
||||
WHERE s.compose_path IS NOT NULL AND s.compose_path <> ''
|
||||
UNION ALL
|
||||
SELECT s.id, 'env',
|
||||
s.stack_name || CASE WHEN e.name IS NOT NULL THEN ' (' || e.name || ')' ELSE '' END,
|
||||
s.env_path
|
||||
FROM stack_sources s LEFT JOIN environments e ON e.id = s.environment_id
|
||||
WHERE s.env_path IS NOT NULL AND s.env_path <> ''
|
||||
ORDER BY 1;
|
||||
"
|
||||
}
|
||||
|
||||
db_list_external_paths() {
|
||||
# settings.value holds a JSON array as text; parse it with jsonb so commas/quotes/
|
||||
# escapes inside a path are handled correctly. WITH ORDINALITY gives a 0-based index.
|
||||
_psql -t -A -F ' ' -c "
|
||||
SELECT ord - 1, elem
|
||||
FROM settings s,
|
||||
jsonb_array_elements_text(s.value::jsonb) WITH ORDINALITY AS t(elem, ord)
|
||||
WHERE s.key = 'external_stack_paths';
|
||||
"
|
||||
}
|
||||
|
||||
db_update_stack_path() {
|
||||
id="$1"; field="$2"; new="$(_sql_escape "$3")"
|
||||
col="compose_path"; [ "$field" = "env" ] && col="env_path"
|
||||
_psql -q -c "UPDATE stack_sources SET $col = '$new', updated_at = NOW() WHERE id = $id;" >/dev/null
|
||||
}
|
||||
|
||||
db_update_external_path() {
|
||||
idx="$1"; new="$(_sql_escape "$2")"
|
||||
# Replace ONLY entry <idx> via jsonb_set; other entries (and any commas/escapes in
|
||||
# them) stay intact. value is text, so cast to jsonb and back.
|
||||
_psql -q -c "
|
||||
UPDATE settings
|
||||
SET value = jsonb_set(value::jsonb, ARRAY['$idx'], to_jsonb('$new'::text))::text,
|
||||
updated_at = NOW()
|
||||
WHERE key = 'external_stack_paths';
|
||||
" >/dev/null
|
||||
}
|
||||
|
||||
. "$(dirname "$0")/../_relocate-common.sh"
|
||||
parse_relocate_args "$@"
|
||||
run_relocation
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PostgreSQL: Emergency script to disable a user's MFA (TOTP)
|
||||
# Use this if a user is locked out of MFA (lost their authenticator app)
|
||||
#
|
||||
# Usage:
|
||||
# docker exec -it dockhand /app/scripts/emergency/postgres/reset-mfa.sh <username>
|
||||
#
|
||||
# Example:
|
||||
# docker exec -it dockhand /app/scripts/emergency/postgres/reset-mfa.sh admin
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo "============================================"
|
||||
echo " Dockhand - Reset User MFA (PostgreSQL)"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# Check arguments
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <username>"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 admin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USERNAME="$1"
|
||||
|
||||
# Check DATABASE_URL
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "Error: DATABASE_URL environment variable not set"
|
||||
echo ""
|
||||
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd /app 2>/dev/null || true
|
||||
|
||||
# Check if user exists using psql
|
||||
USER_EXISTS=$(psql "$DATABASE_URL" -t -A -c "SELECT COUNT(*) FROM users WHERE username = '$USERNAME';" 2>/dev/null)
|
||||
|
||||
if [ "$USER_EXISTS" = "0" ]; then
|
||||
echo "Error: User '$USERNAME' not found"
|
||||
echo ""
|
||||
echo "Available users:"
|
||||
psql "$DATABASE_URL" -t -A -c "SELECT username FROM users;" 2>/dev/null | while read user; do
|
||||
echo " - $user"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show current MFA state
|
||||
MFA_ENABLED=$(psql "$DATABASE_URL" -t -A -c "SELECT mfa_enabled FROM users WHERE username = '$USERNAME';" 2>/dev/null)
|
||||
if [ "$MFA_ENABLED" = "t" ] || [ "$MFA_ENABLED" = "true" ]; then
|
||||
STATUS="enabled"
|
||||
else
|
||||
STATUS="already disabled"
|
||||
fi
|
||||
|
||||
echo "This script will disable MFA (TOTP) for user '$USERNAME'."
|
||||
echo "Backup codes will also be wiped — they cannot be reused."
|
||||
echo ""
|
||||
echo "Database: $DATABASE_URL"
|
||||
echo "Username: $USERNAME"
|
||||
echo "Current MFA: $STATUS"
|
||||
echo ""
|
||||
printf "Continue? [y/N]: "
|
||||
read CONFIRM
|
||||
|
||||
case "$CONFIRM" in
|
||||
[yY]|[yY][eE][sS])
|
||||
;;
|
||||
*)
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "Disabling MFA for user '$USERNAME'..."
|
||||
psql "$DATABASE_URL" -c "UPDATE users SET mfa_enabled = false, mfa_secret = NULL, updated_at = NOW() WHERE username = '$USERNAME';"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo "MFA disabled successfully for user '$USERNAME'."
|
||||
echo "The user can now log in with just their password and can re-enable MFA from their profile."
|
||||
else
|
||||
echo "Error: Failed to disable MFA"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Emergency script: relocate stored stack paths after a DATA_DIR change.
|
||||
# Automatically detects database type (SQLite or PostgreSQL).
|
||||
#
|
||||
# When you change DATA_DIR (e.g. move the bind mount from /app/data to a host
|
||||
# path), the stack files move on disk but Dockhand's database still points at the
|
||||
# OLD absolute paths, so stacks show an empty compose. This script rewrites those
|
||||
# stored pointers from the old DATA_DIR to the new one.
|
||||
#
|
||||
# It does NOT move, copy, or delete any files - it only updates where Dockhand
|
||||
# looks in the database. A pointer is rewritten only when the old file is missing
|
||||
# AND the new file already exists on disk.
|
||||
#
|
||||
# Usage:
|
||||
# relocate-stack-paths.sh <old-data-dir> <new-data-dir> # dry run (default)
|
||||
# relocate-stack-paths.sh <old-data-dir> <new-data-dir> --apply # interactive apply
|
||||
# relocate-stack-paths.sh <old-data-dir> <new-data-dir> --apply --all # apply all
|
||||
#
|
||||
# Example:
|
||||
# docker exec -it dockhand /app/scripts/emergency/relocate-stack-paths.sh \
|
||||
# /app/data /mnt/pool/dockhand --apply
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
|
||||
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||
exec "$SCRIPT_DIR/postgres/relocate-stack-paths.sh" "$@"
|
||||
else
|
||||
exec "$SCRIPT_DIR/sqlite/relocate-stack-paths.sh" "$@"
|
||||
fi
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Emergency script to disable a user's MFA (TOTP)
|
||||
# Automatically detects database type (SQLite or PostgreSQL)
|
||||
#
|
||||
# Usage:
|
||||
# docker exec -it dockhand /app/scripts/reset-mfa.sh <username>
|
||||
#
|
||||
# Example:
|
||||
# docker exec -it dockhand /app/scripts/reset-mfa.sh admin
|
||||
#
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
|
||||
# Detect database type
|
||||
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||
exec "$SCRIPT_DIR/postgres/reset-mfa.sh" "$@"
|
||||
else
|
||||
exec "$SCRIPT_DIR/sqlite/reset-mfa.sh" "$@"
|
||||
fi
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# SQLite: relocate stored stack paths after a DATA_DIR change.
|
||||
# See ../relocate-stack-paths.sh for usage. Does NOT move files - updates DB only.
|
||||
|
||||
set -e
|
||||
|
||||
DB_LABEL="SQLite"
|
||||
|
||||
# Locate the database (env override, Docker default, then local dev).
|
||||
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||
DB_PATH="./data/db/dockhand.db"
|
||||
fi
|
||||
if [ ! -f "$DB_PATH" ]; then
|
||||
echo "Error: SQLite database not found at $DB_PATH"
|
||||
echo "Set DOCKHAND_DB to the database path."
|
||||
exit 1
|
||||
fi
|
||||
DB_DISPLAY="$DB_PATH"
|
||||
|
||||
# SQL-escape a value for single-quoted string literals.
|
||||
_sql_escape() { printf "%s" "$1" | sed "s/'/''/g"; }
|
||||
|
||||
db_list_stack_paths() {
|
||||
# One line per non-null compose/env path: <id>\t<field>\t<label>\t<path>
|
||||
sqlite3 -separator ' ' "$DB_PATH" "
|
||||
SELECT s.id, 'compose',
|
||||
s.stack_name || CASE WHEN e.name IS NOT NULL THEN ' (' || e.name || ')' ELSE '' END,
|
||||
s.compose_path
|
||||
FROM stack_sources s LEFT JOIN environments e ON e.id = s.environment_id
|
||||
WHERE s.compose_path IS NOT NULL AND s.compose_path <> ''
|
||||
UNION ALL
|
||||
SELECT s.id, 'env',
|
||||
s.stack_name || CASE WHEN e.name IS NOT NULL THEN ' (' || e.name || ')' ELSE '' END,
|
||||
s.env_path
|
||||
FROM stack_sources s LEFT JOIN environments e ON e.id = s.environment_id
|
||||
WHERE s.env_path IS NOT NULL AND s.env_path <> ''
|
||||
ORDER BY 1;
|
||||
"
|
||||
}
|
||||
|
||||
db_list_external_paths() {
|
||||
# settings.external_stack_paths is a JSON array; emit <index>\t<path> per entry.
|
||||
# Use json_each so commas/quotes/escapes inside a path are handled correctly.
|
||||
sqlite3 -separator ' ' "$DB_PATH" "
|
||||
SELECT je.key, je.value
|
||||
FROM settings s, json_each(s.value) je
|
||||
WHERE s.key = 'external_stack_paths';
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
db_update_stack_path() {
|
||||
id="$1"; field="$2"; new="$(_sql_escape "$3")"
|
||||
col="compose_path"; [ "$field" = "env" ] && col="env_path"
|
||||
sqlite3 "$DB_PATH" "UPDATE stack_sources SET $col = '$new', updated_at = datetime('now') WHERE id = $id;"
|
||||
}
|
||||
|
||||
db_update_external_path() {
|
||||
idx="$1"; new="$(_sql_escape "$2")"
|
||||
# Replace ONLY entry <idx> via json_set; other entries (and any commas/escapes in
|
||||
# them) are left byte-for-byte intact.
|
||||
sqlite3 "$DB_PATH" "
|
||||
UPDATE settings
|
||||
SET value = json_set(value, '\$[$idx]', '$new'), updated_at = datetime('now')
|
||||
WHERE key = 'external_stack_paths';
|
||||
"
|
||||
}
|
||||
|
||||
. "$(dirname "$0")/../_relocate-common.sh"
|
||||
parse_relocate_args "$@"
|
||||
run_relocation
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# SQLite: Emergency script to disable a user's MFA (TOTP)
|
||||
# Use this if a user is locked out of MFA (lost their authenticator app)
|
||||
#
|
||||
# Usage:
|
||||
# docker exec -it dockhand /app/scripts/emergency/sqlite/reset-mfa.sh <username>
|
||||
#
|
||||
# Example:
|
||||
# docker exec -it dockhand /app/scripts/emergency/sqlite/reset-mfa.sh admin
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================"
|
||||
echo " Dockhand - Reset User MFA (SQLite)"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Check arguments
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <username>"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 admin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USERNAME="$1"
|
||||
|
||||
# Default database path
|
||||
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||
|
||||
# Check if running locally (not in Docker)
|
||||
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||
DB_PATH="./data/db/dockhand.db"
|
||||
fi
|
||||
|
||||
if [ ! -f "$DB_PATH" ]; then
|
||||
echo "Error: Database not found at $DB_PATH"
|
||||
echo "Set DOCKHAND_DB environment variable to specify the database path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if user exists
|
||||
EXISTING=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM users WHERE username='$USERNAME';")
|
||||
|
||||
if [ "$EXISTING" -eq "0" ]; then
|
||||
echo "Error: User '$USERNAME' not found"
|
||||
echo ""
|
||||
echo "Available users:"
|
||||
sqlite3 "$DB_PATH" "SELECT username FROM users;" | while read user; do
|
||||
echo " - $user"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show current MFA state
|
||||
MFA_ENABLED=$(sqlite3 "$DB_PATH" "SELECT mfa_enabled FROM users WHERE username='$USERNAME';")
|
||||
if [ "$MFA_ENABLED" = "1" ] || [ "$MFA_ENABLED" = "true" ]; then
|
||||
STATUS="enabled"
|
||||
else
|
||||
STATUS="already disabled"
|
||||
fi
|
||||
|
||||
echo "This script will disable MFA (TOTP) for user '$USERNAME'."
|
||||
echo "Backup codes will also be wiped — they cannot be reused."
|
||||
echo ""
|
||||
echo "Database: $DB_PATH"
|
||||
echo "Username: $USERNAME"
|
||||
echo "Current MFA: $STATUS"
|
||||
echo ""
|
||||
printf "Continue? [y/N]: "
|
||||
read CONFIRM
|
||||
|
||||
case "$CONFIRM" in
|
||||
[yY]|[yY][eE][sS])
|
||||
;;
|
||||
*)
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "Disabling MFA for user '$USERNAME'..."
|
||||
sqlite3 "$DB_PATH" "UPDATE users SET mfa_enabled=0, mfa_secret=NULL, updated_at=datetime('now') WHERE username='$USERNAME';"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo "MFA disabled successfully for user '$USERNAME'."
|
||||
echo "The user can now log in with just their password and can re-enable MFA from their profile."
|
||||
else
|
||||
echo "Error: Failed to disable MFA"
|
||||
exit 1
|
||||
fi
|
||||
Reference in new issue
Block a user