Runbook: Sign-in fails with "invalid credentials"¶
Symptom¶
Attempting to sign in as local@example.com (or any seeded user) returns "Invalid credentials" despite using the correct password. The sign-in E2E test fails at the POST /auth/login step.
Likely cause¶
The user's password column in Postgres contains a truncated or corrupted bcrypt hash. This happens when the seed script runs with an incompatible bcrypt version or when a partial migration corrupts the row. The telltale sign: the password field is shorter than the expected 60 characters (a valid bcrypt hash is always exactly 60 chars: $2b$10$...).
Diagnosis¶
# Check the password hash length
sudo docker exec ebit-db psql -U ebit -d ebit -c \
"SELECT email, length(password) as len, substring(password, 1, 10) as prefix FROM \"User\" WHERE email = 'local@example.com';"
# Expected output for a healthy hash:
# email | len | prefix
# -----------------------+-----+------------
# local@example.com | 60 | $2b$10$xxx
# If len < 60 (especially len = 2), the hash is corrupted.
Fix¶
Option 1: Re-seed the database¶
cd ebit-api
npm run db:reset # drops + recreates + seeds
# This sets DEBUG_SEED_LOCAL=true which creates local@example.com:password
Option 2: Patch the password directly¶
# Generate a fresh bcrypt hash for "password" (cost factor 10)
node -e "const bcrypt = require('bcryptjs'); bcrypt.hash('password', 10, (e, h) => console.log(h))"
# Output: $2a$10$<hash>
# Update the user's password
sudo docker exec ebit-db psql -U ebit -d ebit -c \
"UPDATE \"User\" SET password = '\$2a\$10\$<paste_full_hash_here>' WHERE email = 'local@example.com';"
Option 3: Clear login-attempt lockout¶
If the password hash is fine but you're locked out from too many failed attempts:
# Check for login-attempt rate-limit keys in Redis
redis-cli -a cache KEYS "*login*local@example.com*"
# Delete the lockout key
redis-cli -a cache DEL "user:login-attempts:local@example.com"
Verification¶
# Test sign-in via curl
curl -s -X POST http://localhost:4000/auth/login \
-H "Content-Type: application/json" \
-H "x-captcha-token: pass" \
-d '{"email":"local@example.com","password":"password"}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('OK' if 'accessToken' in d else f'FAIL: {d}')"
Prevention¶
- Always use
npm run db:seed(not manual INSERTs) to create test users — the seed script uses the project's bcrypt configuration - If switching Node.js versions, re-run
npm installto rebuild native bcrypt bindings - The
DEBUG_SEED_LOCAL=trueenv var indocker-compose.yml:132ensures the demo user is created with every fresh seed