Admin-Rescue: PW-Reset direkt in DB + Rate-Limit-Reset

Use Case: Admin sperrt sich aus (admin@admin.com ist keine echte
Mailadresse, Passwort-vergessen-Flow kann keine Mail liefern) oder
Brute-Force-Lockout will sich nicht von selbst auflösen.

backend/prisma/reset-admin-password.ts:
- Findet User per Email, hasht neues PW mit bcrypt cost 12
- Schreibt direkt in user.password, setzt tokenInvalidatedAt=now()
  (kickt alle bestehenden Sessions), löscht Reset-Tokens
- Eigenes PW: Komplexitäts-Check 25 Zeichen
- Kein PW-Argument: 28-char Zufallspasswort (alle 4 Klassen
  garantiert), wird einmal in stdout ausgegeben

scripts/admin-rescue.sh:
- password <email> [pw]  → docker exec npx tsx … reset-admin-password
- unlock                  → docker restart opencrm-app (leert
                            In-Memory-Rate-Limit-Store)
- all <email> [pw]        → beides

Live-verifiziert: random-Modus, schwaches PW → klare Fehlerliste,
langes eigenes PW → akzeptiert, unbekannter User → exit 2, bash -n
syntax-check ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 15:47:02 +02:00
parent 3e1fc3eab2
commit 3dda83314a
3 changed files with 198 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
#
# Notfall-Wrapper für ausgesperrte Admins.
#
# 1. password Setzt das Passwort eines Mitarbeiter-Users direkt in der
# DB (umgeht jegliche Auth). Wird im laufenden Backend-
# Container über npx tsx ausgeführt, damit bcrypt + Prisma-
# Schema garantiert konsistent sind.
# 2. unlock Restartet den Backend-Container → leert den In-Memory-
# Rate-Limit-Store komplett (alle gesperrten IPs).
# 3. all Beides: PW setzen UND Rate-Limit leeren.
#
# Aufruf (im Repo-Root):
# ./scripts/admin-rescue.sh password <email> [neues-passwort]
# ./scripts/admin-rescue.sh unlock
# ./scripts/admin-rescue.sh all <email> [neues-passwort]
#
# Ohne neues-passwort wird ein 28-Zeichen-Zufallspasswort generiert und
# einmalig in stdout ausgegeben.
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
REPO_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
cd "$REPO_ROOT"
CONTAINER="opencrm-app"
usage() {
echo "Aufruf:"
echo " $0 password <email> [neues-passwort] Admin-Passwort direkt in DB setzen"
echo " $0 unlock Rate-Limit-Store leeren (Container-Restart)"
echo " $0 all <email> [neues-passwort] beides"
exit 1
}
require_container_running() {
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}\$"; then
echo "Container '${CONTAINER}' läuft nicht. Erst docker compose up -d." >&2
exit 2
fi
}
reset_password() {
local email="$1"
local pw="$2"
require_container_running
if [ -z "$email" ]; then
echo "Email fehlt." >&2
usage
fi
echo "Setze Passwort für ${email} im Container ${CONTAINER}"
if [ -n "$pw" ]; then
docker exec -it "$CONTAINER" npx tsx prisma/reset-admin-password.ts "$email" "$pw"
else
docker exec -it "$CONTAINER" npx tsx prisma/reset-admin-password.ts "$email"
fi
}
unlock_rate_limit() {
require_container_running
echo "Restarte ${CONTAINER} → Rate-Limit-Store wird geleert…"
docker restart "$CONTAINER" >/dev/null
echo "Fertig. Container hochgefahren."
}
case "${1:-}" in
password)
shift
reset_password "$@"
;;
unlock)
unlock_rate_limit
;;
all)
shift
reset_password "$@"
unlock_rate_limit
;;
*)
usage
;;
esac