# 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
130 lines
4.8 KiB
TypeScript
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);
|
|
});
|