#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}" USERNAME="" PASSWORD="" EMAIL="" if [[ -f "$ENV_FILE" ]]; then set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a fi EMAIL="${ACORE_ACCOUNT_DEFAULT_EMAIL:-}" GMLEVEL="${ACORE_ACCOUNT_DEFAULT_GMLEVEL:-3}" REALM_ID="${ACORE_ACCOUNT_DEFAULT_REALM_ID:--1}" EXPANSION="${ACORE_ACCOUNT_DEFAULT_EXPANSION:-2}" usage() { cat < [options] Creates or updates an AzerothCore account in the dockerized auth database. Options: --email Account email --gmlevel <0-3> GM level, default: 3 --realm Realm ID for account_access, default: -1 --expansion <0-2> Expansion flag, default: 2 -h, --help Show this help Examples: ./create-account.sh admin strongpass ./create-account.sh player1 secret --gmlevel 0 EOF } log() { printf '[create-account] %s\n' "$*" } require_value() { local flag="$1" local value="${2:-}" if [[ -z "$value" ]]; then printf 'Option %s requires a value\n' "$flag" >&2 exit 1 fi } if (($# == 1)) && [[ "$1" == "-h" || "$1" == "--help" ]]; then usage exit 0 fi if (($# < 2)); then usage >&2 exit 1 fi USERNAME="$1" PASSWORD="$2" shift 2 while (($#)); do case "$1" in --email) require_value "$1" "${2:-}" EMAIL="$2" shift 2 ;; --gmlevel) require_value "$1" "${2:-}" GMLEVEL="$2" shift 2 ;; --realm) require_value "$1" "${2:-}" REALM_ID="$2" shift 2 ;; --expansion) require_value "$1" "${2:-}" EXPANSION="$2" shift 2 ;; -h|--help) usage exit 0 ;; *) printf 'Unknown option: %s\n\n' "$1" >&2 usage >&2 exit 1 ;; esac done if ! [[ "$GMLEVEL" =~ ^[0-3]$ ]]; then printf 'gmlevel must be 0, 1, 2, or 3\n' >&2 exit 1 fi if ! [[ "$REALM_ID" =~ ^-?[0-9]+$ ]]; then printf 'realm must be an integer\n' >&2 exit 1 fi if ! [[ "$EXPANSION" =~ ^[0-2]$ ]]; then printf 'expansion must be 0, 1, or 2\n' >&2 exit 1 fi cd "$ROOT_DIR" if ! docker compose ps --status running --services | grep -qx 'ac-database'; then printf 'ac-database is not running. Start the server first with ./start-server.sh\n' >&2 exit 1 fi export ACORE_USERNAME="$USERNAME" export ACORE_PASSWORD="$PASSWORD" export ACORE_EMAIL="$EMAIL" export ACORE_GMLEVEL="$GMLEVEL" export ACORE_REALM_ID="$REALM_ID" export ACORE_EXPANSION="$EXPANSION" SQL="$(python3 <<'PY' import hashlib import os import secrets N = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16) G = 7 username = os.environ["ACORE_USERNAME"] password = os.environ["ACORE_PASSWORD"] email = os.environ.get("ACORE_EMAIL", "") gmlevel = int(os.environ["ACORE_GMLEVEL"]) realm_id = int(os.environ["ACORE_REALM_ID"]) expansion = int(os.environ["ACORE_EXPANSION"]) def upper_only_latin(value: str) -> str: chars = [] for ch in value: code = ord(ch) if 97 <= code <= 122: chars.append(chr(code - 32)) else: chars.append(ch) return "".join(chars) def sql_quote(value: str) -> str: return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'" username = upper_only_latin(username) password = upper_only_latin(password) email = upper_only_latin(email) if not username: raise SystemExit("username must not be empty") if not password: raise SystemExit("password must not be empty") if len(username) > 32: raise SystemExit("username is too long (max 32 characters)") if len(password) > 32: raise SystemExit("password is too long (max 32 characters)") if len(email) > 255: raise SystemExit("email is too long (max 255 characters)") salt = secrets.token_bytes(32) inner = hashlib.sha1(f"{username}:{password}".encode("utf-8")).digest() x = hashlib.sha1(salt + inner).digest() verifier_int = pow(G, int.from_bytes(x, "big"), N) verifier = verifier_int.to_bytes(32, "little") email_insert = sql_quote(email) if email: email_update = "reg_mail = VALUES(reg_mail), email = VALUES(email)" else: email_update = "reg_mail = reg_mail, email = email" print("START TRANSACTION;") print("SET @account_id := (SELECT id FROM account WHERE username = %s LIMIT 1);" % sql_quote(username)) print( "INSERT INTO account (username, salt, verifier, expansion, reg_mail, email, joindate) " "VALUES (%s, UNHEX('%s'), UNHEX('%s'), %d, %s, %s, NOW()) " "ON DUPLICATE KEY UPDATE salt = VALUES(salt), verifier = VALUES(verifier), expansion = VALUES(expansion), " "%s;" % ( sql_quote(username), salt.hex().upper(), verifier.hex().upper(), expansion, email_insert, email_insert, email_update, ) ) print("SET @account_id := COALESCE(@account_id, LAST_INSERT_ID());") print( "INSERT IGNORE INTO realmcharacters (realmid, acctid, numchars) " "SELECT realmlist.id, @account_id, 0 FROM realmlist;" ) print( "INSERT INTO account_access (id, gmlevel, RealmID, comment) " "VALUES (@account_id, %d, %d, 'managed by create-account.sh') " "ON DUPLICATE KEY UPDATE gmlevel = VALUES(gmlevel), comment = VALUES(comment);" % (gmlevel, realm_id) ) print( "SELECT id, username, expansion FROM account WHERE id = @account_id;" ) print( "SELECT id, gmlevel, RealmID FROM account_access WHERE id = @account_id ORDER BY RealmID;" ) print("COMMIT;") PY )" log "writing account ${USERNAME} (gmlevel=${GMLEVEL}, realm=${REALM_ID}, expansion=${EXPANSION})" docker compose exec -T ac-database bash -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" acore_auth' <<<"$SQL" log "account ready"