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