feat: add apps/api (NestJS + Fastify) with authentication endpoints
- POST /auth/login, /auth/refresh, /auth/logout, /auth/select-tenant, /auth/change-password, GET /auth/tenants — wired to packages/auth - JwtAuthGuard + DomainExceptionFilter (401/403 without leaking internals) - LoginRateLimitGuard: Redis-backed 5/min per IP and per email (agente.md secao 149), safe across multiple API instances - helmet + restrictive cors (deny-by-default) + global rate limit - GET /health, /health/live, /health/ready checking Postgres and Redis - changePassword() added to packages/auth for the mustChangePassword flow - fixed REDIS_HOST/POSTGRES_HOST docker-compose-only hostnames not resolving from the host process; added REDIS_URL for host-side use - verified end-to-end with curl: login, wrong password / unknown email (same generic error), authenticated route, missing token, refresh rotation, logout revocation, and the 429 rate limit kicking in after 5 attempts
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { getPrismaClient, withUserContext } from "@b2bcall/database";
|
||||
import { recordAuditEvent } from "./audit";
|
||||
import { verifyPassword } from "./password";
|
||||
import { hashPassword, verifyPassword } from "./password";
|
||||
import {
|
||||
REFRESH_TOKEN_TTL_MS,
|
||||
generateRefreshToken,
|
||||
@@ -185,3 +185,35 @@ export async function setActiveTenant(
|
||||
|
||||
return signAccessToken({ sub: userId, sessionId: session.id, tenantId: session.activeTenantId! });
|
||||
}
|
||||
|
||||
/**
|
||||
* Troca de senha (usada tanto voluntariamente quanto para satisfazer
|
||||
* `mustChangePassword`, agente.md secao 199 — força troca no primeiro login).
|
||||
* Revoga todas as outras sessões do usuário; a atual continua válida.
|
||||
*/
|
||||
export async function changePassword(
|
||||
userId: string,
|
||||
currentSessionId: string,
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
): Promise<void> {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
||||
|
||||
const currentOk = await verifyPassword(user.passwordHash, currentPassword);
|
||||
if (!currentOk) {
|
||||
throw new InvalidCredentialsError();
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { passwordHash: await hashPassword(newPassword), mustChangePassword: false },
|
||||
});
|
||||
|
||||
await prisma.session.updateMany({
|
||||
where: { userId, id: { not: currentSessionId }, revokedAt: null },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
|
||||
await recordAuditEvent(prisma, { action: "PASSWORD_CHANGE", userId });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user