182 lines
4.8 KiB
Bash
Executable File
182 lines
4.8 KiB
Bash
Executable File
#!/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."
|