Files
CaseGambling/scripts/xdocker-update-user.sh
gpatruno 2614d679c8 update
2026-07-18 16:26:08 +02:00

216 lines
5.7 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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: 332 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 332 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."