FieldOps/scripts/auth-smoke.ts
Pedro Gomes 1bc837e606 MAI CALL - auth v0.2
# O que mudou
1 Schema: failedAttempts + lockedUntil em User; migration auth_v0_2_lockout aplicada; crypto.ts com hashSecret/verifySecret (Node scrypt nativo, zero deps)
2 packages/api/src/auth.ts — authenticateCredential com lockout de 5 tentativas
3 Seed reescrito: admin hashed admin1234, operadores hashed 1111/2222/3333
4 Porta das traseiras fechada: AUTH_DEV_AUTOLOGIN ignorado quando NODE_ENV=production, em ambas as apps
5 operator-pwa: Credentials provider usa PIN + allowedRoles:['OPERATOR']; cookies fieldops-op.*
6 Picker em 2 estados: lista → teclado PIN (botões grandes, dots de progresso, mensagem de erro sem dar pistas)
7 admin-web: Auth.js completo (auth.config, auth.ts, route handler, middleware, /login page, AUTH_SECRET no env) com cookies fieldops-admin.*
8 scripts/auth-smoke.ts (11/11 ✓); .env.example e README atualizados
2026-05-30 11:54:38 +01:00

130 lines
4.8 KiB
TypeScript

/**
* Auth smoke test — verifies hashSecret/verifySecret and authenticateCredential.
* Run: pnpm tsx scripts/auth-smoke.ts
* Requires: Docker Postgres running + pnpm db:seed already done.
*/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { config as loadEnv } from 'dotenv';
const here = path.dirname(fileURLToPath(import.meta.url));
loadEnv({ path: path.resolve(here, '../.env') });
import { hashSecret, verifySecret, prisma } from '../packages/db/src/index.js';
import { authenticateCredential } from '../packages/api/src/index.js';
let passed = 0;
let failed = 0;
function ok(label: string) {
console.log(`${label}`);
passed++;
}
function fail(label: string, detail?: unknown) {
console.error(`${label}`, detail ?? '');
failed++;
}
async function main() {
// ── 1. Crypto unit tests (no DB) ─────────────────────────────────────────────
console.log('\n[1] Crypto unit tests');
const hash = await hashSecret('1234');
if (await verifySecret('1234', hash)) ok('verifySecret correct input → true');
else fail('verifySecret correct input → true');
if (!(await verifySecret('9999', hash))) ok('verifySecret wrong input → false');
else fail('verifySecret wrong input → false');
if (!(await verifySecret('x', null))) ok('verifySecret null stored → false');
else fail('verifySecret null stored → false');
if (!(await verifySecret('x', 'malformed'))) ok('verifySecret malformed stored → false');
else fail('verifySecret malformed stored → false');
// ── 2. authenticateCredential — success cases ─────────────────────────────────
console.log('\n[2] authenticateCredential — success cases');
const op1 = await authenticateCredential({
email: 'op1@demo.local',
secret: '1111',
allowedRoles: ['OPERATOR'],
});
if (op1 && op1.role === 'OPERATOR') ok('op1 + correct PIN → user returned');
else fail('op1 + correct PIN → user returned', op1);
const admin = await authenticateCredential({
email: 'admin@demo.local',
secret: 'admin1234',
allowedRoles: ['ADMIN', 'SUPERVISOR'],
});
if (admin && admin.role === 'ADMIN') ok('admin + correct password → user returned');
else fail('admin + correct password → user returned', admin);
// ── 3. authenticateCredential — failure cases ─────────────────────────────────
console.log('\n[3] authenticateCredential — failure cases');
const wrongPin = await authenticateCredential({
email: 'op2@demo.local',
secret: '0000',
allowedRoles: ['OPERATOR'],
});
if (!wrongPin) ok('op2 + wrong PIN → null');
else fail('op2 + wrong PIN → null');
const wrongRole = await authenticateCredential({
email: 'admin@demo.local',
secret: 'admin1234',
allowedRoles: ['OPERATOR'],
});
if (!wrongRole) ok('admin trying to log in as OPERATOR → null');
else fail('admin trying to log in as OPERATOR → null');
const noUser = await authenticateCredential({
email: 'ghost@demo.local',
secret: '1111',
allowedRoles: ['OPERATOR'],
});
if (!noUser) ok('unknown email → null');
else fail('unknown email → null');
// ── 4. Lockout after 5 wrong PINs ─────────────────────────────────────────────
console.log('\n[4] Lockout after 5 failed attempts (op3@demo.local)');
// Reset first in case a prior run left the account locked
await prisma.user.updateMany({
where: { email: 'op3@demo.local' },
data: { failedAttempts: 0, lockedUntil: null },
});
for (let i = 1; i <= 5; i++) {
await authenticateCredential({ email: 'op3@demo.local', secret: '0000', allowedRoles: ['OPERATOR'] });
}
const afterLockout = await authenticateCredential({
email: 'op3@demo.local',
secret: '3333', // correct PIN
allowedRoles: ['OPERATOR'],
});
if (!afterLockout) ok('6th attempt (correct PIN) still blocked by lockout → null');
else fail('6th attempt (correct PIN) still blocked by lockout → null');
// Reset op3 so the DB isn't left in a broken state
await prisma.user.updateMany({
where: { email: 'op3@demo.local' },
data: { failedAttempts: 0, lockedUntil: null },
});
ok('op3 lockout reset — DB clean');
// ── Summary ───────────────────────────────────────────────────────────────────
await prisma.$disconnect();
console.log(`\nResults: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});