refactor(auth): remove hash hardcoded da migration, seeder gera em runtime

Antes: 010_seed_super_admin.sql tinha hash bcrypt fixo amarrado a um pepper
específico. Qualquer mudança no PASSWORD_PEPPER quebrava todos os logins
silenciosamente após reset do banco.

Agora:
- migration 010: insere superadmin com placeholder inválido + force_change_password.
  ON CONFLICT DO NOTHING preserva o hash se o seeder já rodou.
- seeder users.js: faz upsert de 'lol' com bcrypt(senha + env.PASSWORD_PEPPER)
  em runtime. Mudar o pepper e re-rodar o seeder é suficiente para atualizar
  as credenciais sem tocar em nenhuma migration.
- docs/AGENTS.md: atualiza gotcha #1 explicando o novo fluxo migrate → seed
- docs/DEVOPS.md: fix opção 1 do troubleshooting inclui re-deploy do seeder

Fluxo correto após reset do banco (coberto pelo start.sh opções 2, 6, 8):
  npm run migrate  →  superadmin criado, hash = placeholder
  npm run seed     →  hash recalculado com PEPPER do ambiente, status = active

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tiago Yamamoto 2026-02-22 12:05:54 -06:00
parent fcf960381c
commit 89358acc13
4 changed files with 81 additions and 33 deletions

View file

@ -2,13 +2,17 @@
-- Description: Inserts the default System Company and Super Admin user. -- Description: Inserts the default System Company and Super Admin user.
-- Uses unified tables (companies, users, user_roles) -- Uses unified tables (companies, users, user_roles)
-- --
-- ⚠️ PEPPER CRITICAL: This hash was generated with PASSWORD_PEPPER=gohorse-pepper -- ⚠️ SEM HASH HARDCODED — o hash é gerado em runtime pelo seeder-api.
-- The backend (Coolify env var PASSWORD_PEPPER) MUST be set to: gohorse-pepper -- Motivo: bcrypt(password + pepper) depende do valor de PASSWORD_PEPPER
-- If the pepper does not match, ALL logins will fail with "invalid credentials". -- que varia por ambiente. Hardcodar o hash aqui amarraria o deploy a um
-- pepper específico e quebraria logins silenciosamente se o pepper mudar.
-- --
-- Credentials: identifier=superadmin / password=Admin@2025! -- Fluxo correto após reset do banco:
-- Hash: bcrypt("Admin@2025!" + "gohorse-pepper", cost=10) -- 1. npm run migrate → cria o usuário com senha bloqueada (placeholder)
-- Generated with bcryptjs 2.4.x / golang.org/x/crypto/bcrypt — both compatible. -- 2. npm run seed → gera o hash correto e ativa o usuário
--
-- O status 'force_change_password' sinaliza que o hash ainda não foi
-- definido pelo seeder. O usuário NÃO consegue logar antes do seed.
-- 1. Insert System Company (for SuperAdmin context) -- 1. Insert System Company (for SuperAdmin context)
INSERT INTO companies (name, slug, type, document, email, description, verified, active) INSERT INTO companies (name, slug, type, document, email, description, verified, active)
@ -23,19 +27,18 @@ VALUES (
true true
) ON CONFLICT (slug) DO NOTHING; ) ON CONFLICT (slug) DO NOTHING;
-- 2. Insert Super Admin User -- 2. Insert Super Admin User (sem hash — seeder define o hash em runtime)
INSERT INTO users (identifier, password_hash, role, full_name, email, status, active) INSERT INTO users (identifier, password_hash, role, full_name, email, status, active)
VALUES ( VALUES (
'superadmin', 'superadmin',
'$2b$10$4759wJhnXnBpcwSnVZm9Eu.wTqGYVCHkxAU5a2NxhsFHU42nV3tzW', '$invalid-placeholder-run-seeder$',
'superadmin', 'superadmin',
'Super Administrator', 'Super Administrator',
'admin@gohorsejobs.com', 'admin@gohorsejobs.com',
'ACTIVE', 'force_change_password',
true true
) ON CONFLICT (identifier) DO UPDATE SET ) ON CONFLICT (identifier) DO NOTHING;
password_hash = EXCLUDED.password_hash, -- ON CONFLICT DO NOTHING: não sobrescreve se o seeder já definiu o hash.
status = 'ACTIVE';
-- 3. Assign superadmin role (if user_roles table exists) -- 3. Assign superadmin role (if user_roles table exists)
DO $$ DO $$

View file

@ -306,17 +306,28 @@ git push pipe dev
## ⚠️ Known Gotchas ## ⚠️ Known Gotchas
### 1. PASSWORD_PEPPER must be `gohorse-pepper` everywhere ### 1. PASSWORD_PEPPER e o hash do superadmin são gerados em runtime
All migration seeds (`010_seed_super_admin.sql`) and the seeder-api use `gohorse-pepper` as the bcrypt pepper. A migration `010_seed_super_admin.sql` **não hardcoda hash**. Ela cria o superadmin com um
The Coolify/production env var `PASSWORD_PEPPER` **must match** or every login returns `AUTH_INVALID_CREDENTIALS`. placeholder inválido e `force_change_password`. O **seeder-api** é quem gera o hash correto
em runtime lendo `process.env.PASSWORD_PEPPER`.
| Location | Value | Fluxo correto após reset do banco:
```
npm run migrate → superadmin criado, hash = placeholder (não faz login)
npm run seed → hash recalculado com pepper do ambiente, status = active
```
O `start.sh` opções 2, 6 e 8 já fazem isso automaticamente (migrate → seed).
| Location | `PASSWORD_PEPPER` |
|----------|-------| |----------|-------|
| `backend/.env` (local) | `gohorse-pepper` | | `backend/.env` (local) | `gohorse-pepper` |
| `seeder-api/.env` | `gohorse-pepper` | | `seeder-api/.env` | `gohorse-pepper` |
| Coolify DEV (`iw4sow8s0kkg4cccsk08gsoo`) | `gohorse-pepper` | | Coolify DEV (`iw4sow8s0kkg4cccsk08gsoo`) | `gohorse-pepper` |
| Migration `010_seed_super_admin.sql` hash | computed with `gohorse-pepper` |
Se `PASSWORD_PEPPER` mudar, basta rodar `npm run seed` novamente — o hash é recalculado
automaticamente. Nenhuma migration precisa ser alterada.
If you suspect a mismatch, see the full fix procedure in [DEVOPS.md](DEVOPS.md#troubleshooting-login-retorna-invalid-credentials). If you suspect a mismatch, see the full fix procedure in [DEVOPS.md](DEVOPS.md#troubleshooting-login-retorna-invalid-credentials).

View file

@ -181,38 +181,52 @@ docker run --rm --network coolify -v /tmp/login.json:/tmp/login.json \
-H 'Content-Type: application/json' -d @/tmp/login.json -H 'Content-Type: application/json' -d @/tmp/login.json
``` ```
**Fix — opção 1: corrigir o pepper no Coolify (preferível):** **Fix — opção 1 (preferível): corrigir o pepper no Coolify e re-rodar o seeder:**
```bash ```bash
TOKEN=$(cat ~/.ssh/coolify-redbull-token) TOKEN=$(cat ~/.ssh/coolify-redbull-token)
# 1. Atualizar PASSWORD_PEPPER
curl -s -X PATCH \ curl -s -X PATCH \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"key":"PASSWORD_PEPPER","value":"gohorse-pepper"}' \ -d '{"key":"PASSWORD_PEPPER","value":"gohorse-pepper"}' \
"https://redbull.rede5.com.br/api/v1/applications/iw4sow8s0kkg4cccsk08gsoo/envs" "https://redbull.rede5.com.br/api/v1/applications/iw4sow8s0kkg4cccsk08gsoo/envs"
# Reiniciar o backend # 2. Reiniciar o backend (para ele pegar o novo pepper)
curl -s -H "Authorization: Bearer $TOKEN" \ curl -s -H "Authorization: Bearer $TOKEN" \
"https://redbull.rede5.com.br/api/v1/applications/iw4sow8s0kkg4cccsk08gsoo/restart" "https://redbull.rede5.com.br/api/v1/applications/iw4sow8s0kkg4cccsk08gsoo/restart"
# 3. Re-rodar o seeder (ele regrava o hash com o pepper correto automaticamente)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://redbull.rede5.com.br/api/v1/deploy?uuid=q4w48gos8cgssso00o8w8gck"
``` ```
**Fix — opção 2: regravar o hash no banco (se o pepper mudou intencionalmente):** > O seeder (`seedUsers()`) sempre faz upsert do superadmin/lol com `bcrypt(senha + PEPPER)`.
```bash > Mudar o pepper e re-rodar o seeder é suficiente — nenhuma migration precisa ser tocada.
# Gerar novo hash com o pepper correto (ex: dentro de container node):
docker run --rm node:20-alpine sh -c \
'cd /tmp && npm init -y > /dev/null && npm install bcryptjs > /dev/null && \
node -e "console.log(require(\"./node_modules/bcryptjs\").hashSync(\"Admin@2025!\"+process.env.PEPPER,10))" \
PEPPER=gohorse-pepper'
# Aplicar no banco (usar arquivo para preservar os $ do hash): **Fix — opção 2 (emergência, sem seeder): regravar hash direto no banco:**
```bash
ssh redbull
# Gerar novo hash com node (usando arquivo para evitar expansão de $ pelo shell):
mkdir -p /tmp/hashgen && cat > /tmp/hashgen/gen.js <<'EOF'
const b = require("./node_modules/bcryptjs");
console.log(b.hashSync("Admin@2025!" + process.env.PEPPER, 10));
EOF
docker run --rm -v /tmp/hashgen:/app -w /app -e PEPPER=gohorse-pepper \
node:20-alpine sh -c "npm install bcryptjs -s && node gen.js"
# Aplicar no banco (SEMPRE usar -f, nunca -c, para preservar os $ do hash):
cat > /tmp/fix_hash.sql <<'EOF' cat > /tmp/fix_hash.sql <<'EOF'
UPDATE users SET password_hash = '<hash_gerado>' WHERE identifier IN ('lol','superadmin'); UPDATE users SET password_hash = '<hash_gerado_acima>', status = 'active'
WHERE identifier IN ('lol', 'superadmin');
EOF EOF
docker cp /tmp/fix_hash.sql bgws48os8wgwk08o48wg8k80:/tmp/fix_hash.sql docker cp /tmp/fix_hash.sql bgws48os8wgwk08o48wg8k80:/tmp/fix_hash.sql
docker exec bgws48os8wgwk08o48wg8k80 psql -U gohorsejobs -d gohorsejobs -f /tmp/fix_hash.sql docker exec bgws48os8wgwk08o48wg8k80 psql -U gohorsejobs -d gohorsejobs -f /tmp/fix_hash.sql
``` ```
> **Nota:** Sempre use um arquivo (ou `docker cp` + `-f`) para executar SQL com hashes bcrypt. > ⚠️ **Nunca passe hash bcrypt via `-c '...'`** na linha de comando — o shell expande os `$`
> Passar o hash via `-c '...'` na linha de comando faz o shell interpretar os `$` como variáveis. > e corrompe o hash silenciosamente. Use sempre um arquivo e `-f`.
### Deploy via API ### Deploy via API

View file

@ -7,7 +7,6 @@ const PASSWORD_PEPPER = process.env.PASSWORD_PEPPER || '';
export async function seedUsers() { export async function seedUsers() {
console.log('👤 Seeding users (Unified Architecture)...'); console.log('👤 Seeding users (Unified Architecture)...');
console.log(' SuperAdmin is created via backend migration (010_seed_super_admin.sql)');
try { try {
// Fetch companies to map users (now using companies table, not core_companies) // Fetch companies to map users (now using companies table, not core_companies)
@ -19,8 +18,29 @@ export async function seedUsers() {
const systemResult = await pool.query("SELECT id FROM companies WHERE slug = 'gohorse-system'"); const systemResult = await pool.query("SELECT id FROM companies WHERE slug = 'gohorse-system'");
const systemTenantId = systemResult.rows[0]?.id || null; const systemTenantId = systemResult.rows[0]?.id || null;
// NOTE: SuperAdmin is now created via migration 010_seed_super_admin.sql // 0. Seed SuperAdmin (lol) — hash gerado em runtime com o pepper do ambiente.
// No longer created here to avoid PASSWORD_PEPPER mismatch issues // A migration 010 cria o usuário com hash placeholder. O seeder é quem
// define o hash correto. Assim, trocar PASSWORD_PEPPER + rodar o seeder
// é suficiente para atualizar as credenciais sem tocar em migrations.
if (!PASSWORD_PEPPER) {
console.warn(' ⚠️ PASSWORD_PEPPER não definido — superadmin ficará sem senha válida.');
}
const superAdminHash = await bcrypt.hash('Admin@2025!' + PASSWORD_PEPPER, 10);
const superAdminResult = await pool.query(`
INSERT INTO users (identifier, password_hash, role, full_name, email, name, status, active)
VALUES ('lol', $1, 'superadmin', 'Dr. Horse Expert', 'lol@gohorsejobs.com', 'Dr. Horse Expert', 'active', true)
ON CONFLICT (identifier) DO UPDATE SET
password_hash = EXCLUDED.password_hash,
status = 'active',
updated_at = NOW()
RETURNING id
`, [superAdminHash]);
const superAdminId = superAdminResult.rows[0].id;
await pool.query(`
INSERT INTO user_roles (user_id, role) VALUES ($1, 'superadmin')
ON CONFLICT (user_id, role) DO NOTHING
`, [superAdminId]);
console.log(' ✓ SuperAdmin seeded: lol (hash gerado com pepper do ambiente)');
// 1. Create Company Admins // 1. Create Company Admins
const admins = [ const admins = [