update
This commit is contained in:
Binary file not shown.
+9
-64
@@ -1,67 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Export local project SQLite DB (+ uploads) for backup / Docker import
|
||||
# Usage:
|
||||
# ./scripts/db-export.sh
|
||||
# ./scripts/db-export.sh ./backups/my-backup
|
||||
#
|
||||
# Sources (local):
|
||||
# server/prisma/dev.db (+ -wal / -shm if present)
|
||||
# server/uploads/
|
||||
#
|
||||
# Restore into Docker with:
|
||||
# ./scripts/db-import.sh ./backups/...
|
||||
# Deprecated alias — use docker-db-export.sh on prod, or local-db-export.sh on your laptop.
|
||||
# This wrapper keeps the old name for existing docs / muscle memory.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
LOCAL_DB="${LOCAL_DB:-$ROOT/server/prisma/dev.db}"
|
||||
LOCAL_UPLOADS="${LOCAL_UPLOADS:-$ROOT/server/uploads}"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
OUT="${1:-$ROOT/backups/casegambling-$STAMP}"
|
||||
|
||||
if [[ ! -f "$LOCAL_DB" ]]; then
|
||||
echo "Error: local database not found: $LOCAL_DB" >&2
|
||||
echo "Tip: run the app once or set LOCAL_DB=/path/to/dev.db" >&2
|
||||
exit 1
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
if [[ -f "$DIR/../docker-compose.yml" ]] && command -v docker >/dev/null 2>&1 \
|
||||
&& docker compose -f "$DIR/../docker-compose.yml" ps -q casegambling >/dev/null 2>&1; then
|
||||
echo "Note: forwarding to docker-db-export.sh (Docker detected)." >&2
|
||||
exec "$DIR/docker-db-export.sh" "$@"
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
echo "==> Exporting local database → $OUT/prod.db"
|
||||
echo " source: $LOCAL_DB"
|
||||
cp -f "$LOCAL_DB" "$OUT/prod.db"
|
||||
# keep a copy under original name too (import accepts both)
|
||||
cp -f "$LOCAL_DB" "$OUT/dev.db"
|
||||
|
||||
if [[ -f "${LOCAL_DB}-wal" ]]; then
|
||||
cp -f "${LOCAL_DB}-wal" "$OUT/prod.db-wal"
|
||||
cp -f "${LOCAL_DB}-wal" "$OUT/dev.db-wal"
|
||||
fi
|
||||
if [[ -f "${LOCAL_DB}-shm" ]]; then
|
||||
cp -f "${LOCAL_DB}-shm" "$OUT/prod.db-shm"
|
||||
cp -f "${LOCAL_DB}-shm" "$OUT/dev.db-shm"
|
||||
fi
|
||||
|
||||
echo "==> Exporting local uploads → $OUT/uploads"
|
||||
mkdir -p "$OUT/uploads"
|
||||
if [[ -d "$LOCAL_UPLOADS" ]]; then
|
||||
# copy contents; ignore empty/.gitkeep-only quietly
|
||||
cp -a "$LOCAL_UPLOADS"/. "$OUT/uploads/" 2>/dev/null || true
|
||||
else
|
||||
echo " (no uploads dir at $LOCAL_UPLOADS)"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "exported_at=$STAMP"
|
||||
echo "source=local"
|
||||
echo "local_db=$LOCAL_DB"
|
||||
echo "local_uploads=$LOCAL_UPLOADS"
|
||||
echo "database=prod.db"
|
||||
echo "uploads=uploads/"
|
||||
} > "$OUT/manifest.txt"
|
||||
|
||||
TAR="$OUT.tar.gz"
|
||||
tar -czf "$TAR" -C "$(dirname "$OUT")" "$(basename "$OUT")"
|
||||
echo "==> Archive: $TAR"
|
||||
echo "==> Done. Import to Docker: ./scripts/db-import.sh $OUT"
|
||||
echo "Note: forwarding to local-db-export.sh." >&2
|
||||
exec "$DIR/local-db-export.sh" "$@"
|
||||
|
||||
+8
-112
@@ -1,115 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Import SQLite DB (+ uploads) into the CaseGambling Docker container
|
||||
# Usage:
|
||||
# ./scripts/db-import.sh ./backups/casegambling-20260116-203000
|
||||
# ./db-import.sh ./casegambling-20260116-203000.tar.gz
|
||||
# Deprecated alias — use docker-db-import.sh (Docker) or local-db-import.sh (laptop).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# Project root: folder that contains docker-compose.yml
|
||||
if [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
|
||||
ROOT="$SCRIPT_DIR"
|
||||
elif [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
else
|
||||
ROOT="$SCRIPT_DIR"
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
if [[ -f "$DIR/../docker-compose.yml" ]] && command -v docker >/dev/null 2>&1 \
|
||||
&& docker compose -f "$DIR/../docker-compose.yml" ps -aq casegambling >/dev/null 2>&1; then
|
||||
echo "Note: forwarding to docker-db-import.sh (Docker detected)." >&2
|
||||
exec "$DIR/docker-db-import.sh" "$@"
|
||||
fi
|
||||
|
||||
# Resolve backup path from the caller's working directory (before any cd)
|
||||
if [[ -z "${1:-}" ]]; then
|
||||
echo "Usage: $0 <backup-dir-or-tar.gz>" >&2
|
||||
echo "Example: $0 ./backups/casegambling-20260116-203000.tar.gz" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$1" = /* ]]; then
|
||||
SRC="$1"
|
||||
else
|
||||
SRC="$(pwd)/$1"
|
||||
fi
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-.env.prod}"
|
||||
COMPOSE=(docker compose)
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
COMPOSE=(docker compose --env-file "$ENV_FILE")
|
||||
fi
|
||||
|
||||
SERVICE="${SERVICE:-casegambling}"
|
||||
|
||||
TMP=""
|
||||
cleanup() {
|
||||
if [[ -n "$TMP" && -d "$TMP" ]]; then
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
is_archive() {
|
||||
case "$1" in
|
||||
*.tar.gz|*.tgz) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ -f "$SRC" ]] && is_archive "$SRC"; then
|
||||
echo "==> Extracting $(basename "$SRC")"
|
||||
TMP="$(mktemp -d)"
|
||||
tar -xzf "$SRC" -C "$TMP"
|
||||
# Prefer a single top-level directory from the archive
|
||||
mapfile -t DIRS < <(find "$TMP" -mindepth 1 -maxdepth 1 -type d | sort)
|
||||
if [[ ${#DIRS[@]} -ge 1 ]]; then
|
||||
SRC="${DIRS[0]}"
|
||||
else
|
||||
SRC="$TMP"
|
||||
fi
|
||||
elif [[ -f "$SRC" ]]; then
|
||||
echo "Error: not a directory or .tar.gz archive: $SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$SRC" ]]; then
|
||||
echo "Error: backup not found: $SRC" >&2
|
||||
echo "Pass a folder or a .tar.gz produced by ./scripts/db-export.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB="$SRC/prod.db"
|
||||
if [[ ! -f "$DB" ]]; then
|
||||
if [[ -f "$SRC/dev.db" ]]; then
|
||||
DB="$SRC/dev.db"
|
||||
else
|
||||
echo "Error: no prod.db (or dev.db) in $SRC" >&2
|
||||
ls -la "$SRC" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> Stopping $SERVICE (safe SQLite replace)"
|
||||
"${COMPOSE[@]}" stop "$SERVICE" 2>/dev/null || docker stop "$SERVICE" 2>/dev/null || true
|
||||
|
||||
echo "==> Clearing old SQLite sidecars on volume"
|
||||
"${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \
|
||||
'rm -f /data/prod.db-wal /data/prod.db-shm' >/dev/null
|
||||
|
||||
echo "==> Importing database ← $DB"
|
||||
"${COMPOSE[@]}" cp "$DB" "$SERVICE:/data/prod.db"
|
||||
|
||||
if [[ -f "$SRC/prod.db-wal" ]]; then
|
||||
"${COMPOSE[@]}" cp "$SRC/prod.db-wal" "$SERVICE:/data/prod.db-wal"
|
||||
fi
|
||||
if [[ -f "$SRC/prod.db-shm" ]]; then
|
||||
"${COMPOSE[@]}" cp "$SRC/prod.db-shm" "$SERVICE:/data/prod.db-shm"
|
||||
fi
|
||||
|
||||
if [[ -d "$SRC/uploads" ]]; then
|
||||
echo "==> Importing uploads"
|
||||
"${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \
|
||||
'rm -rf /app/uploads/* /app/uploads/.[!.]* 2>/dev/null; mkdir -p /app/uploads' >/dev/null
|
||||
"${COMPOSE[@]}" cp "$SRC/uploads/." "$SERVICE:/app/uploads/"
|
||||
fi
|
||||
|
||||
echo "==> Starting $SERVICE"
|
||||
"${COMPOSE[@]}" start "$SERVICE" 2>/dev/null || "${COMPOSE[@]}" up -d "$SERVICE"
|
||||
|
||||
echo "==> Done. Check: ${COMPOSE[*]} logs -f --tail=50"
|
||||
echo "Note: forwarding to local-db-import.sh." >&2
|
||||
exec "$DIR/local-db-import.sh" "$@"
|
||||
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Export local project SQLite DB (+ uploads) for backup / Docker import.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/local-db-export.sh
|
||||
# ./scripts/local-db-export.sh ./backups/my-backup
|
||||
#
|
||||
# Sources (local):
|
||||
# server/prisma/dev.db (+ -wal / -shm if present)
|
||||
# server/uploads/
|
||||
#
|
||||
# Restore locally:
|
||||
# ./scripts/local-db-import.sh ./backups/casegambling-….tar.gz
|
||||
# Restore into Docker (prod):
|
||||
# ./scripts/docker-db-import.sh ./backups/casegambling-….tar.gz
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
LOCAL_DB="${LOCAL_DB:-$ROOT/server/prisma/dev.db}"
|
||||
LOCAL_UPLOADS="${LOCAL_UPLOADS:-$ROOT/server/uploads}"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
OUT="${1:-$ROOT/backups/casegambling-$STAMP}"
|
||||
|
||||
if [[ ! -f "$LOCAL_DB" ]]; then
|
||||
echo "Error: local database not found: $LOCAL_DB" >&2
|
||||
echo "Tip: run the app once, or set LOCAL_DB=/path/to/dev.db" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
echo "==> Exporting local database → $OUT/dev.db"
|
||||
echo " source: $LOCAL_DB"
|
||||
|
||||
# Prefer a consistent SQLite snapshot when sqlite3 is available
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
sqlite3 "$LOCAL_DB" ".backup '$OUT/dev.db'"
|
||||
cp -f "$OUT/dev.db" "$OUT/prod.db"
|
||||
else
|
||||
cp -f "$LOCAL_DB" "$OUT/dev.db"
|
||||
cp -f "$LOCAL_DB" "$OUT/prod.db"
|
||||
if [[ -f "${LOCAL_DB}-wal" ]]; then
|
||||
cp -f "${LOCAL_DB}-wal" "$OUT/dev.db-wal"
|
||||
cp -f "${LOCAL_DB}-wal" "$OUT/prod.db-wal"
|
||||
fi
|
||||
if [[ -f "${LOCAL_DB}-shm" ]]; then
|
||||
cp -f "${LOCAL_DB}-shm" "$OUT/dev.db-shm"
|
||||
cp -f "${LOCAL_DB}-shm" "$OUT/prod.db-shm"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> Exporting local uploads → $OUT/uploads"
|
||||
mkdir -p "$OUT/uploads"
|
||||
if [[ -d "$LOCAL_UPLOADS" ]]; then
|
||||
cp -a "$LOCAL_UPLOADS"/. "$OUT/uploads/" 2>/dev/null || true
|
||||
else
|
||||
echo " (no uploads dir at $LOCAL_UPLOADS)"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "exported_at=$STAMP"
|
||||
echo "source=local"
|
||||
echo "local_db=$LOCAL_DB"
|
||||
echo "local_uploads=$LOCAL_UPLOADS"
|
||||
echo "database=dev.db"
|
||||
echo "uploads=uploads/"
|
||||
} > "$OUT/manifest.txt"
|
||||
|
||||
TAR="$OUT.tar.gz"
|
||||
tar -czf "$TAR" -C "$(dirname "$OUT")" "$(basename "$OUT")"
|
||||
|
||||
DB_SIZE="$(du -h "$OUT/dev.db" | awk '{print $1}')"
|
||||
UPLOAD_COUNT="$(find "$OUT/uploads" -type f 2>/dev/null | wc -l | tr -d ' ')"
|
||||
|
||||
echo "==> Archive: $TAR"
|
||||
echo " db size: $DB_SIZE · uploads files: $UPLOAD_COUNT"
|
||||
echo "==> Done."
|
||||
echo " Local restore: ./scripts/local-db-import.sh $TAR"
|
||||
echo " Docker restore: ./scripts/docker-db-import.sh $TAR"
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Import SQLite DB (+ uploads) into the LOCAL project
|
||||
# (server/prisma/dev.db + server/uploads/).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/local-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS
|
||||
# ./scripts/local-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS.tar.gz
|
||||
#
|
||||
# Typical flow (prod → laptop):
|
||||
# # on prod:
|
||||
# ./scripts/docker-db-export.sh
|
||||
# # copy the .tar.gz here, then:
|
||||
# ./scripts/local-db-import.sh ./backups/casegambling-….tar.gz
|
||||
#
|
||||
# Stop `npm run dev` before importing so SQLite is not locked.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
if [[ -z "${1:-}" ]]; then
|
||||
echo "Usage: $0 <backup-dir-or-tar.gz>" >&2
|
||||
echo "Example: $0 ./backups/casegambling-20260116-203000.tar.gz" >&2
|
||||
echo "Create a backup from Docker: ./scripts/docker-db-export.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve backup path from the caller's cwd
|
||||
if [[ "$1" = /* ]]; then
|
||||
SRC="$1"
|
||||
else
|
||||
SRC="$(pwd)/$1"
|
||||
fi
|
||||
|
||||
LOCAL_DB="${LOCAL_DB:-$ROOT/server/prisma/dev.db}"
|
||||
LOCAL_UPLOADS="${LOCAL_UPLOADS:-$ROOT/server/uploads}"
|
||||
|
||||
TMP=""
|
||||
cleanup() {
|
||||
if [[ -n "$TMP" && -d "$TMP" ]]; then
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
is_archive() {
|
||||
case "$1" in
|
||||
*.tar.gz|*.tgz) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ -f "$SRC" ]] && is_archive "$SRC"; then
|
||||
echo "==> Extracting $(basename "$SRC")"
|
||||
TMP="$(mktemp -d)"
|
||||
tar -xzf "$SRC" -C "$TMP"
|
||||
mapfile -t DIRS < <(find "$TMP" -mindepth 1 -maxdepth 1 -type d | sort)
|
||||
if [[ ${#DIRS[@]} -ge 1 ]]; then
|
||||
SRC="${DIRS[0]}"
|
||||
else
|
||||
SRC="$TMP"
|
||||
fi
|
||||
elif [[ -f "$SRC" ]]; then
|
||||
echo "Error: not a directory or .tar.gz archive: $SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$SRC" ]]; then
|
||||
echo "Error: backup not found: $SRC" >&2
|
||||
echo "Pass a folder or a .tar.gz from docker-db-export.sh / local-db-export.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB=""
|
||||
for candidate in "$SRC/dev.db" "$SRC/prod.db"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
DB="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$DB" ]]; then
|
||||
echo "Error: no dev.db or prod.db in $SRC" >&2
|
||||
ls -la "$SRC" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$LOCAL_DB")"
|
||||
|
||||
echo "==> Importing database → $LOCAL_DB"
|
||||
echo " source: $DB"
|
||||
|
||||
# Drop local WAL/SHM so the new file is the sole source of truth
|
||||
rm -f "${LOCAL_DB}-wal" "${LOCAL_DB}-shm"
|
||||
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
# Normalize into a clean single-file DB
|
||||
sqlite3 "$DB" ".backup '$LOCAL_DB'"
|
||||
else
|
||||
cp -f "$DB" "$LOCAL_DB"
|
||||
WAL=""
|
||||
SHM=""
|
||||
[[ -f "$SRC/dev.db-wal" ]] && WAL="$SRC/dev.db-wal"
|
||||
[[ -z "$WAL" && -f "$SRC/prod.db-wal" ]] && WAL="$SRC/prod.db-wal"
|
||||
[[ -f "$SRC/dev.db-shm" ]] && SHM="$SRC/dev.db-shm"
|
||||
[[ -z "$SHM" && -f "$SRC/prod.db-shm" ]] && SHM="$SRC/prod.db-shm"
|
||||
[[ -n "$WAL" ]] && cp -f "$WAL" "${LOCAL_DB}-wal"
|
||||
[[ -n "$SHM" ]] && cp -f "$SHM" "${LOCAL_DB}-shm"
|
||||
fi
|
||||
|
||||
if [[ -d "$SRC/uploads" ]]; then
|
||||
echo "==> Importing uploads → $LOCAL_UPLOADS"
|
||||
mkdir -p "$LOCAL_UPLOADS"
|
||||
# Keep .gitkeep if present; replace other files
|
||||
find "$LOCAL_UPLOADS" -mindepth 1 ! -name '.gitkeep' -exec rm -rf {} + 2>/dev/null || true
|
||||
cp -a "$SRC/uploads"/. "$LOCAL_UPLOADS/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
DB_SIZE="$(du -h "$LOCAL_DB" | awk '{print $1}')"
|
||||
UPLOAD_COUNT="$(find "$LOCAL_UPLOADS" -type f ! -name '.gitkeep' 2>/dev/null | wc -l | tr -d ' ')"
|
||||
|
||||
echo "==> Done."
|
||||
echo " db: $LOCAL_DB ($DB_SIZE)"
|
||||
echo " uploads files: $UPLOAD_COUNT"
|
||||
echo " Tip: stop npm run dev before import if the DB was locked; then restart:"
|
||||
echo " npm run db:push && npm run dev"
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Export CaseGambling database (+ uploads) FROM Docker (production).
|
||||
# Run this on the prod server next to docker-compose.yml.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/docker-db-export.sh
|
||||
# ./scripts/docker-db-export.sh ./backups/my-backup
|
||||
#
|
||||
# Output:
|
||||
# backups/casegambling-YYYYMMDD-HHMMSS/
|
||||
# prod.db (+ optional -wal/-shm)
|
||||
# uploads/
|
||||
# manifest.txt
|
||||
# backups/casegambling-YYYYMMDD-HHMMSS.tar.gz
|
||||
#
|
||||
# Then copy the .tar.gz to your machine and import locally:
|
||||
# ./scripts/local-db-import.sh ./backups/casegambling-….tar.gz
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
elif [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
|
||||
ROOT="$SCRIPT_DIR"
|
||||
else
|
||||
echo "Error: docker-compose.yml not found near $SCRIPT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
cd "$ROOT"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-.env.prod}"
|
||||
COMPOSE=(docker compose)
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
COMPOSE=(docker compose --env-file "$ENV_FILE")
|
||||
fi
|
||||
|
||||
SERVICE="${SERVICE:-casegambling}"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
OUT="${1:-$ROOT/backups/casegambling-$STAMP}"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: docker not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! "${COMPOSE[@]}" ps -q "$SERVICE" >/dev/null 2>&1; then
|
||||
echo "Error: compose service '$SERVICE' not found" >&2
|
||||
echo "Tip: run from the folder that has docker-compose.yml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
echo "==> Exporting Docker database → $OUT/prod.db"
|
||||
echo " service: $SERVICE · volume path: /data/prod.db"
|
||||
|
||||
TMP_UPLOADS="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_UPLOADS"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
WAS_RUNNING=0
|
||||
if [[ -n "$("${COMPOSE[@]}" ps -q --status running "$SERVICE" 2>/dev/null || true)" ]]; then
|
||||
WAS_RUNNING=1
|
||||
fi
|
||||
|
||||
# Prefer a consistent SQLite snapshot inside a running container
|
||||
if [[ "$WAS_RUNNING" -eq 1 ]] \
|
||||
&& "${COMPOSE[@]}" exec -T "$SERVICE" sh -c 'command -v sqlite3 >/dev/null && test -f /data/prod.db'; then
|
||||
"${COMPOSE[@]}" exec -T "$SERVICE" sh -c "sqlite3 /data/prod.db \".backup '/tmp/prod-export.db'\""
|
||||
"${COMPOSE[@]}" cp "$SERVICE:/tmp/prod-export.db" "$OUT/prod.db"
|
||||
"${COMPOSE[@]}" exec -T "$SERVICE" rm -f /tmp/prod-export.db >/dev/null 2>&1 || true
|
||||
else
|
||||
# Safe file copy: stop if running so WAL is flushed
|
||||
if [[ "$WAS_RUNNING" -eq 1 ]]; then
|
||||
echo "==> Stopping $SERVICE for a consistent copy"
|
||||
"${COMPOSE[@]}" stop "$SERVICE" >/dev/null
|
||||
fi
|
||||
"${COMPOSE[@]}" cp "$SERVICE:/data/prod.db" "$OUT/prod.db"
|
||||
"${COMPOSE[@]}" cp "$SERVICE:/data/prod.db-wal" "$OUT/prod.db-wal" 2>/dev/null || true
|
||||
"${COMPOSE[@]}" cp "$SERVICE:/data/prod.db-shm" "$OUT/prod.db-shm" 2>/dev/null || true
|
||||
if [[ "$WAS_RUNNING" -eq 1 ]]; then
|
||||
echo "==> Starting $SERVICE"
|
||||
"${COMPOSE[@]}" start "$SERVICE" >/dev/null 2>&1 || "${COMPOSE[@]}" up -d "$SERVICE" >/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# Also write as dev.db so local import can use either name
|
||||
cp -f "$OUT/prod.db" "$OUT/dev.db"
|
||||
[[ -f "$OUT/prod.db-wal" ]] && cp -f "$OUT/prod.db-wal" "$OUT/dev.db-wal"
|
||||
[[ -f "$OUT/prod.db-shm" ]] && cp -f "$OUT/prod.db-shm" "$OUT/dev.db-shm"
|
||||
|
||||
echo "==> Exporting Docker uploads → $OUT/uploads"
|
||||
mkdir -p "$OUT/uploads"
|
||||
if "${COMPOSE[@]}" cp "$SERVICE:/app/uploads/." "$TMP_UPLOADS/" 2>/dev/null; then
|
||||
cp -a "$TMP_UPLOADS"/. "$OUT/uploads/" 2>/dev/null || true
|
||||
else
|
||||
echo " (no uploads or empty volume)"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "exported_at=$STAMP"
|
||||
echo "source=docker"
|
||||
echo "service=$SERVICE"
|
||||
echo "database=prod.db"
|
||||
echo "uploads=uploads/"
|
||||
} > "$OUT/manifest.txt"
|
||||
|
||||
TAR="$OUT.tar.gz"
|
||||
tar -czf "$TAR" -C "$(dirname "$OUT")" "$(basename "$OUT")"
|
||||
|
||||
DB_SIZE="$(du -h "$OUT/prod.db" | awk '{print $1}')"
|
||||
UPLOAD_COUNT="$(find "$OUT/uploads" -type f 2>/dev/null | wc -l | tr -d ' ')"
|
||||
|
||||
echo "==> Archive: $TAR"
|
||||
echo " db size: $DB_SIZE · uploads files: $UPLOAD_COUNT"
|
||||
echo "==> Done. On your laptop:"
|
||||
echo " scp user@prod:$TAR ./backups/"
|
||||
echo " ./scripts/local-db-import.sh ./backups/$(basename "$TAR")"
|
||||
Executable
+154
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env bash
|
||||
# Import SQLite DB (+ uploads) INTO Docker (production / compose).
|
||||
# Run this on the machine that runs docker compose.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/docker-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS
|
||||
# ./scripts/docker-db-import.sh ./backups/casegambling-YYYYMMDD-HHMMSS.tar.gz
|
||||
#
|
||||
# Expects a backup from docker-db-export.sh or local-db-export.sh
|
||||
# (prod.db or dev.db + uploads/).
|
||||
#
|
||||
# Docker paths (see docker-compose.yml):
|
||||
# DATABASE_URL=file:/data/prod.db
|
||||
# uploads → /app/uploads
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
elif [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
|
||||
ROOT="$SCRIPT_DIR"
|
||||
else
|
||||
echo "Error: docker-compose.yml not found near $SCRIPT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${1:-}" ]]; then
|
||||
echo "Usage: $0 <backup-dir-or-tar.gz>" >&2
|
||||
echo "Example: $0 ./backups/casegambling-20260116-203000.tar.gz" >&2
|
||||
echo "Create a backup first: ./scripts/docker-db-export.sh (or local-db-export.sh)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve backup path from the caller's cwd (before any cd)
|
||||
if [[ "$1" = /* ]]; then
|
||||
SRC="$1"
|
||||
else
|
||||
SRC="$(pwd)/$1"
|
||||
fi
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-.env.prod}"
|
||||
COMPOSE=(docker compose)
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
COMPOSE=(docker compose --env-file "$ENV_FILE")
|
||||
elif [[ -f ".env.prod.example" ]]; then
|
||||
echo "Warning: $ENV_FILE missing — using compose defaults." >&2
|
||||
echo " Copy .env.prod.example → .env.prod for production settings." >&2
|
||||
fi
|
||||
|
||||
SERVICE="${SERVICE:-casegambling}"
|
||||
|
||||
TMP=""
|
||||
cleanup() {
|
||||
if [[ -n "$TMP" && -d "$TMP" ]]; then
|
||||
rm -rf "$TMP"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
is_archive() {
|
||||
case "$1" in
|
||||
*.tar.gz|*.tgz) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: docker not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$SRC" ]] && is_archive "$SRC"; then
|
||||
echo "==> Extracting $(basename "$SRC")"
|
||||
TMP="$(mktemp -d)"
|
||||
tar -xzf "$SRC" -C "$TMP"
|
||||
mapfile -t DIRS < <(find "$TMP" -mindepth 1 -maxdepth 1 -type d | sort)
|
||||
if [[ ${#DIRS[@]} -ge 1 ]]; then
|
||||
SRC="${DIRS[0]}"
|
||||
else
|
||||
SRC="$TMP"
|
||||
fi
|
||||
elif [[ -f "$SRC" ]]; then
|
||||
echo "Error: not a directory or .tar.gz archive: $SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$SRC" ]]; then
|
||||
echo "Error: backup not found: $SRC" >&2
|
||||
echo "Pass a folder or a .tar.gz produced by docker-db-export.sh / local-db-export.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB=""
|
||||
for candidate in "$SRC/prod.db" "$SRC/dev.db"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
DB="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$DB" ]]; then
|
||||
echo "Error: no prod.db or dev.db in $SRC" >&2
|
||||
ls -la "$SRC" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Ensuring compose service exists"
|
||||
"${COMPOSE[@]}" up -d --no-start "$SERVICE" >/dev/null
|
||||
|
||||
echo "==> Stopping $SERVICE (safe SQLite replace)"
|
||||
"${COMPOSE[@]}" stop "$SERVICE" >/dev/null 2>&1 || true
|
||||
|
||||
echo "==> Clearing old SQLite sidecars on volume"
|
||||
"${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \
|
||||
'rm -f /data/prod.db-wal /data/prod.db-shm' >/dev/null
|
||||
|
||||
echo "==> Importing database ← $DB"
|
||||
"${COMPOSE[@]}" cp "$DB" "$SERVICE:/data/prod.db"
|
||||
|
||||
WAL=""
|
||||
SHM=""
|
||||
[[ -f "$SRC/prod.db-wal" ]] && WAL="$SRC/prod.db-wal"
|
||||
[[ -z "$WAL" && -f "$SRC/dev.db-wal" ]] && WAL="$SRC/dev.db-wal"
|
||||
[[ -f "$SRC/prod.db-shm" ]] && SHM="$SRC/prod.db-shm"
|
||||
[[ -z "$SHM" && -f "$SRC/dev.db-shm" ]] && SHM="$SRC/dev.db-shm"
|
||||
|
||||
if [[ -n "$WAL" ]]; then
|
||||
"${COMPOSE[@]}" cp "$WAL" "$SERVICE:/data/prod.db-wal"
|
||||
fi
|
||||
if [[ -n "$SHM" ]]; then
|
||||
"${COMPOSE[@]}" cp "$SHM" "$SERVICE:/data/prod.db-shm"
|
||||
fi
|
||||
|
||||
if [[ -d "$SRC/uploads" ]]; then
|
||||
echo "==> Importing uploads"
|
||||
"${COMPOSE[@]}" run --rm --no-deps --entrypoint sh "$SERVICE" -c \
|
||||
'rm -rf /app/uploads/* /app/uploads/.[!.]* 2>/dev/null; mkdir -p /app/uploads' >/dev/null
|
||||
"${COMPOSE[@]}" cp "$SRC/uploads/." "$SERVICE:/app/uploads/"
|
||||
fi
|
||||
|
||||
echo "==> Starting $SERVICE"
|
||||
"${COMPOSE[@]}" start "$SERVICE" 2>/dev/null || "${COMPOSE[@]}" up -d "$SERVICE"
|
||||
|
||||
HOST_PORT_VAL="${HOST_PORT:-}"
|
||||
if [[ -z "$HOST_PORT_VAL" && -f "$ENV_FILE" ]]; then
|
||||
HOST_PORT_VAL="$(grep -E '^HOST_PORT=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
|
||||
fi
|
||||
HOST_PORT_VAL="${HOST_PORT_VAL:-3009}"
|
||||
|
||||
echo "==> Done."
|
||||
echo " App: http://localhost:${HOST_PORT_VAL}"
|
||||
echo " Logs: ${COMPOSE[*]} logs -f --tail=50"
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
# Delete a user by username from the Docker (production) SQLite DB.
|
||||
# Run this on the prod server next to docker-compose.yml.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/xdocker-delete-user.sh <username>
|
||||
# ./scripts/xdocker-delete-user.sh <username> --yes # skip confirmation
|
||||
#
|
||||
# Removes the User and related data (inventory, transactions, deposits,
|
||||
# case-key progress, duel rooms they created). Nulls jackpot/duel winner
|
||||
# pointers that referenced them.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
elif [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
|
||||
ROOT="$SCRIPT_DIR"
|
||||
else
|
||||
echo "Error: docker-compose.yml not found near $SCRIPT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
cd "$ROOT"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-.env.prod}"
|
||||
COMPOSE=(docker compose)
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
COMPOSE=(docker compose --env-file "$ENV_FILE")
|
||||
fi
|
||||
|
||||
SERVICE="${SERVICE:-casegambling}"
|
||||
USERNAME="${1:-}"
|
||||
ASSUME_YES=0
|
||||
|
||||
if [[ -z "$USERNAME" || "$USERNAME" == "-h" || "$USERNAME" == "--help" ]]; then
|
||||
echo "Usage: $0 <username> [--yes]" >&2
|
||||
echo "Example: $0 badplayer" >&2
|
||||
echo " $0 badplayer --yes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shift || true
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--yes|-y) ASSUME_YES=1 ;;
|
||||
*)
|
||||
echo "Error: unknown argument: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: docker not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$("${COMPOSE[@]}" ps -q --status running "$SERVICE" 2>/dev/null || true)" ]]; then
|
||||
echo "Error: compose service '$SERVICE' is not running" >&2
|
||||
echo "Tip: docker compose --env-file .env.prod up -d" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_node() {
|
||||
local mode="$1"
|
||||
"${COMPOSE[@]}" exec -T \
|
||||
-e DEL_USERNAME="$USERNAME" \
|
||||
-e DEL_MODE="$mode" \
|
||||
"$SERVICE" node <<'NODE'
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
(async () => {
|
||||
const username = process.env.DEL_USERNAME;
|
||||
const mode = process.env.DEL_MODE;
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
balance: true,
|
||||
osuBalance: true,
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
inventory: true,
|
||||
transactions: true,
|
||||
jackpotDeposits: true,
|
||||
duelDeposits: true,
|
||||
duelRoomsCreated: true,
|
||||
caseKeyProgress: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.error(`Error: no user with username '${username}'`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (mode === 'info') {
|
||||
console.log(`id: ${user.id}`);
|
||||
console.log(`username: ${user.username}`);
|
||||
console.log(`role: ${user.role}`);
|
||||
console.log(`balance: ${user.balance.toString()}`);
|
||||
console.log(`osuBalance: ${user.osuBalance}`);
|
||||
console.log(`createdAt: ${user.createdAt.toISOString()}`);
|
||||
console.log(`inventory: ${user._count.inventory}`);
|
||||
console.log(`transactions: ${user._count.transactions}`);
|
||||
console.log(`jackpot deps: ${user._count.jackpotDeposits}`);
|
||||
console.log(`duel deps: ${user._count.duelDeposits}`);
|
||||
console.log(`duel rooms: ${user._count.duelRoomsCreated}`);
|
||||
console.log(`case keys: ${user._count.caseKeyProgress}`);
|
||||
if (user.role === 'admin') {
|
||||
console.log('WARNING: role=admin');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (mode !== 'delete') {
|
||||
console.error(`Unknown mode: ${mode}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Optional FKs without onDelete Cascade
|
||||
await tx.jackpotRound.updateMany({
|
||||
where: { winnerUserId: user.id },
|
||||
data: { winnerUserId: null },
|
||||
});
|
||||
await tx.duelRoom.updateMany({
|
||||
where: { winnerUserId: user.id },
|
||||
data: { winnerUserId: null },
|
||||
});
|
||||
|
||||
// Deposits reference InventoryItem — remove before user/inventory cascade
|
||||
await tx.jackpotDeposit.deleteMany({ where: { userId: user.id } });
|
||||
await tx.duelDeposit.deleteMany({ where: { userId: user.id } });
|
||||
|
||||
// Rooms created by this user (other deposits in those rooms cascade with room)
|
||||
await tx.duelRoom.deleteMany({ where: { creatorId: user.id } });
|
||||
|
||||
await tx.user.delete({ where: { id: user.id } });
|
||||
});
|
||||
|
||||
console.log(`Deleted user id=${user.id} username=${user.username}`);
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}).finally(() => prisma.$disconnect());
|
||||
NODE
|
||||
}
|
||||
|
||||
echo "==> Looking up user '$USERNAME' …"
|
||||
set +e
|
||||
INFO_OUT="$(run_node info 2>&1)"
|
||||
INFO_RC=$?
|
||||
set -e
|
||||
echo "$INFO_OUT"
|
||||
if [[ "$INFO_RC" -ne 0 ]]; then
|
||||
exit "$INFO_RC"
|
||||
fi
|
||||
|
||||
if [[ "$ASSUME_YES" -ne 1 ]]; then
|
||||
echo ""
|
||||
printf "Delete user '%s' permanently? [y/N] " "$USERNAME"
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
y|Y|yes|YES) ;;
|
||||
*)
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "==> Deleting …"
|
||||
run_node delete
|
||||
echo "==> Done."
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bash
|
||||
# Update a user's username and/or password in the Docker (production) SQLite DB.
|
||||
# Run this on the prod server next to docker-compose.yml.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/xdocker-update-user.sh <current-username> --username <new>
|
||||
# ./scripts/xdocker-update-user.sh <current-username> --password <new>
|
||||
# ./scripts/xdocker-update-user.sh <current-username> --username <new> --password <new>
|
||||
# ./scripts/xdocker-update-user.sh <current-username> --username <new> --password <new> --yes
|
||||
#
|
||||
# Password is hashed with bcrypt (cost 10), same as the app.
|
||||
# Username rules match registration: 3–32 chars, [a-zA-Z0-9_], unique.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/../docker-compose.yml" ]]; then
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
elif [[ -f "$SCRIPT_DIR/docker-compose.yml" ]]; then
|
||||
ROOT="$SCRIPT_DIR"
|
||||
else
|
||||
echo "Error: docker-compose.yml not found near $SCRIPT_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
cd "$ROOT"
|
||||
|
||||
ENV_FILE="${ENV_FILE:-.env.prod}"
|
||||
COMPOSE=(docker compose)
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
COMPOSE=(docker compose --env-file "$ENV_FILE")
|
||||
fi
|
||||
|
||||
SERVICE="${SERVICE:-casegambling}"
|
||||
CURRENT_USERNAME="${1:-}"
|
||||
NEW_USERNAME=""
|
||||
NEW_PASSWORD=""
|
||||
ASSUME_YES=0
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <current-username> [--username <new>] [--password <new>] [--yes]" >&2
|
||||
echo "Example: $0 oldname --username newname --password 's3cret'" >&2
|
||||
echo " $0 oldname --password 's3cret' --yes" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ -z "$CURRENT_USERNAME" || "$CURRENT_USERNAME" == "-h" || "$CURRENT_USERNAME" == "--help" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
shift || true
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--username|-u)
|
||||
[[ $# -ge 2 ]] || { echo "Error: $1 needs a value" >&2; exit 1; }
|
||||
NEW_USERNAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--password|-p)
|
||||
[[ $# -ge 2 ]] || { echo "Error: $1 needs a value" >&2; exit 1; }
|
||||
NEW_PASSWORD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--yes|-y)
|
||||
ASSUME_YES=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Error: unknown argument: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$NEW_USERNAME" && -z "$NEW_PASSWORD" ]]; then
|
||||
echo "Error: provide at least --username and/or --password" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: docker not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$("${COMPOSE[@]}" ps -q --status running "$SERVICE" 2>/dev/null || true)" ]]; then
|
||||
echo "Error: compose service '$SERVICE' is not running" >&2
|
||||
echo "Tip: docker compose --env-file .env.prod up -d" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_node() {
|
||||
local mode="$1"
|
||||
"${COMPOSE[@]}" exec -T \
|
||||
-e UPD_CURRENT="$CURRENT_USERNAME" \
|
||||
-e UPD_NEW_USERNAME="$NEW_USERNAME" \
|
||||
-e UPD_NEW_PASSWORD="$NEW_PASSWORD" \
|
||||
-e UPD_MODE="$mode" \
|
||||
"$SERVICE" node <<'NODE'
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const bcrypt = require('bcrypt');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
(async () => {
|
||||
const currentUsername = process.env.UPD_CURRENT;
|
||||
const newUsername = (process.env.UPD_NEW_USERNAME || '').trim();
|
||||
const newPassword = process.env.UPD_NEW_PASSWORD || '';
|
||||
const mode = process.env.UPD_MODE;
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username: currentUsername },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
balance: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.error(`Error: no user with username '${currentUsername}'`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (mode === 'info') {
|
||||
console.log(`id: ${user.id}`);
|
||||
console.log(`username: ${user.username}`);
|
||||
console.log(`role: ${user.role}`);
|
||||
console.log(`balance: ${user.balance.toString()}`);
|
||||
console.log(`createdAt: ${user.createdAt.toISOString()}`);
|
||||
if (newUsername) console.log(`new user: ${newUsername}`);
|
||||
if (newPassword) console.log(`password: (will be changed)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (mode !== 'update') {
|
||||
console.error(`Unknown mode: ${mode}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = {};
|
||||
|
||||
if (newUsername) {
|
||||
if (newUsername.length < 3 || newUsername.length > 32) {
|
||||
console.error('Error: username must be 3–32 characters');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(newUsername)) {
|
||||
console.error('Error: username may only contain letters, numbers, and underscores');
|
||||
process.exit(1);
|
||||
}
|
||||
if (newUsername !== user.username) {
|
||||
const taken = await prisma.user.findUnique({ where: { username: newUsername } });
|
||||
if (taken) {
|
||||
console.error(`Error: username '${newUsername}' already taken`);
|
||||
process.exit(1);
|
||||
}
|
||||
data.username = newUsername;
|
||||
}
|
||||
}
|
||||
|
||||
if (newPassword) {
|
||||
if (newPassword.length < 6) {
|
||||
console.error('Error: password must be at least 6 characters');
|
||||
process.exit(1);
|
||||
}
|
||||
data.passwordHash = await bcrypt.hash(newPassword, 10);
|
||||
}
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
console.log('Nothing to change (same username, no password).');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const updated = await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data,
|
||||
select: { id: true, username: true },
|
||||
});
|
||||
|
||||
const parts = [];
|
||||
if (data.username) parts.push(`username → ${updated.username}`);
|
||||
if (data.passwordHash) parts.push('password updated');
|
||||
console.log(`Updated user id=${updated.id}: ${parts.join(', ')}`);
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}).finally(() => prisma.$disconnect());
|
||||
NODE
|
||||
}
|
||||
|
||||
echo "==> Looking up user '$CURRENT_USERNAME' …"
|
||||
set +e
|
||||
INFO_OUT="$(run_node info 2>&1)"
|
||||
INFO_RC=$?
|
||||
set -e
|
||||
echo "$INFO_OUT"
|
||||
if [[ "$INFO_RC" -ne 0 ]]; then
|
||||
exit "$INFO_RC"
|
||||
fi
|
||||
|
||||
if [[ "$ASSUME_YES" -ne 1 ]]; then
|
||||
echo ""
|
||||
printf "Apply changes to '%s'? [y/N] " "$CURRENT_USERNAME"
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
y|Y|yes|YES) ;;
|
||||
*)
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "==> Updating …"
|
||||
run_node update
|
||||
echo "==> Done."
|
||||
Reference in New Issue
Block a user