feat: add password reset utility script
Usage: ./scripts/reset-password.sh <email> <new-password> Generates bcrypt hash via bcryptjs in the backend container, updates the database, and verifies the hash matches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
150
scripts/reset-password.sh
Executable file
150
scripts/reset-password.sh
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# ---------------------------------------------------------------------------
|
||||
# reset-password.sh — Reset a user's password in HOA LedgerIQ
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/reset-password.sh <email> <new-password>
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/reset-password.sh admin@hoaledgeriq.com MyNewPassword123
|
||||
# ./scripts/reset-password.sh admin@sunrisevalley.org SecurePass!
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- Defaults ----
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DB_USER="${POSTGRES_USER:-hoafinance}"
|
||||
DB_NAME="${POSTGRES_DB:-hoafinance}"
|
||||
COMPOSE_CMD="docker compose"
|
||||
|
||||
# If running with the SSL override, detect it
|
||||
if [ -f "$PROJECT_DIR/docker-compose.ssl.yml" ] && \
|
||||
docker compose -f "$PROJECT_DIR/docker-compose.yml" \
|
||||
-f "$PROJECT_DIR/docker-compose.ssl.yml" ps --quiet 2>/dev/null | head -1 | grep -q .; then
|
||||
COMPOSE_CMD="docker compose -f $PROJECT_DIR/docker-compose.yml -f $PROJECT_DIR/docker-compose.ssl.yml"
|
||||
fi
|
||||
|
||||
# ---- Colors ----
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
die() { err "$@"; exit 1; }
|
||||
|
||||
# ---- Helpers ----
|
||||
|
||||
ensure_containers_running() {
|
||||
if ! $COMPOSE_CMD ps postgres 2>/dev/null | grep -q "running\|Up"; then
|
||||
die "PostgreSQL container is not running. Start it with: docker compose up -d postgres"
|
||||
fi
|
||||
if ! $COMPOSE_CMD ps backend 2>/dev/null | grep -q "running\|Up"; then
|
||||
die "Backend container is not running. Start it with: docker compose up -d backend"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- CLI ----
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
HOA LedgerIQ Password Reset
|
||||
|
||||
Usage:
|
||||
$(basename "$0") <email> <new-password>
|
||||
|
||||
Examples:
|
||||
$(basename "$0") admin@hoaledgeriq.com MyNewPassword123
|
||||
$(basename "$0") admin@sunrisevalley.org SecurePass!
|
||||
|
||||
This script:
|
||||
1. Verifies the user exists in the database
|
||||
2. Generates a bcrypt hash using bcryptjs (same library the app uses)
|
||||
3. Updates the password in the database
|
||||
4. Verifies the new hash works
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse args
|
||||
case "${1:-}" in
|
||||
-h|--help|help|"") usage ;;
|
||||
esac
|
||||
|
||||
[ $# -lt 2 ] && die "Usage: $(basename "$0") <email> <new-password>"
|
||||
|
||||
EMAIL="$1"
|
||||
NEW_PASSWORD="$2"
|
||||
|
||||
# Load .env if present
|
||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_DIR/.env"
|
||||
set +a
|
||||
DB_USER="${POSTGRES_USER:-hoafinance}"
|
||||
DB_NAME="${POSTGRES_DB:-hoafinance}"
|
||||
fi
|
||||
|
||||
# Ensure containers are running
|
||||
info "Checking containers ..."
|
||||
ensure_containers_running
|
||||
|
||||
# Verify user exists
|
||||
info "Looking up user: ${EMAIL} ..."
|
||||
USER_RECORD=$($COMPOSE_CMD exec -T postgres psql -U "$DB_USER" -d "$DB_NAME" \
|
||||
-t -A -c "SELECT id, email, first_name, last_name, is_superadmin FROM shared.users WHERE email = '${EMAIL}';" 2>/dev/null)
|
||||
|
||||
if [ -z "$USER_RECORD" ]; then
|
||||
die "No user found with email: ${EMAIL}"
|
||||
fi
|
||||
|
||||
# Parse user info for display
|
||||
IFS='|' read -r USER_ID USER_EMAIL FIRST_NAME LAST_NAME IS_SUPER <<< "$USER_RECORD"
|
||||
info "Found user: ${FIRST_NAME} ${LAST_NAME} (${USER_EMAIL})"
|
||||
if [ "$IS_SUPER" = "t" ]; then
|
||||
warn "This is a superadmin account"
|
||||
fi
|
||||
|
||||
# Generate bcrypt hash using bcryptjs inside the backend container
|
||||
info "Generating bcrypt hash ..."
|
||||
HASH=$($COMPOSE_CMD exec -T backend node -e "
|
||||
const bcrypt = require('bcryptjs');
|
||||
bcrypt.hash(process.argv[1], 12).then(h => process.stdout.write(h));
|
||||
" "$NEW_PASSWORD" 2>/dev/null)
|
||||
|
||||
if [ -z "$HASH" ] || [ ${#HASH} -lt 50 ]; then
|
||||
die "Failed to generate bcrypt hash. Is the backend container running?"
|
||||
fi
|
||||
|
||||
# Update the password using a heredoc to avoid shell escaping issues with $ in hashes
|
||||
info "Updating password ..."
|
||||
UPDATE_RESULT=$($COMPOSE_CMD exec -T postgres psql -U "$DB_USER" -d "$DB_NAME" -t -A <<EOSQL
|
||||
UPDATE shared.users SET password_hash = '${HASH}', updated_at = NOW() WHERE email = '${EMAIL}';
|
||||
EOSQL
|
||||
)
|
||||
|
||||
if [[ "$UPDATE_RESULT" != *"UPDATE 1"* ]]; then
|
||||
die "Password update failed. Result: ${UPDATE_RESULT}"
|
||||
fi
|
||||
|
||||
# Verify the new hash works
|
||||
info "Verifying new password ..."
|
||||
VERIFY=$($COMPOSE_CMD exec -T backend node -e "
|
||||
const bcrypt = require('bcryptjs');
|
||||
bcrypt.compare(process.argv[1], process.argv[2]).then(r => process.stdout.write(String(r)));
|
||||
" "$NEW_PASSWORD" "$HASH" 2>/dev/null)
|
||||
|
||||
if [ "$VERIFY" != "true" ]; then
|
||||
die "Verification failed — the hash does not match the password. Something went wrong."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
ok "Password reset successful!"
|
||||
echo ""
|
||||
info " User: ${FIRST_NAME} ${LAST_NAME} (${USER_EMAIL})"
|
||||
info " Login: ${EMAIL}"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user