#!/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 # ./scripts/xdocker-delete-user.sh --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 [--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."