Auth completo (Argon2id + JWT/refresh rotativo), permissões, Companies e OpenAPI
- Auth (apps/api/src/auth/): Argon2id com parâmetros versionados
(ADR-0005), access token JWT de 15min, refresh token opaco de 256 bits
com rotação e detecção de replay (reuso de token já rotacionado revoga
toda a cadeia de sessões do usuário). Endpoints /api/v1/auth/
{login,refresh,logout,me}.
- Permissões resource+action+scope (ADR-0004) via PermissionGuard,
resolvidas do banco a cada request (nunca cacheadas no JWT) — mudança
de papel tem efeito imediato. super_admin com bypass hardcoded,
espelhando a trava já existente no banco.
- Companies (migration + CRUD): leitura aberta a qualquer autenticado,
escrita restrita a companies:manage; validação de CNPJ via
packages/domain-shared (dígito verificador).
- SeedService: cria o primeiro super_admin a partir de
EDEN_SUPERADMIN_EMAIL/PASSWORD só quando `users` está vazia.
- OpenAPI/Swagger em /api/docs; ValidationPipe global com
whitelist+forbidNonWhitelisted (proteção contra mass-assignment).
- packages/database: repositórios (roles/users/sessions/audit/companies)
reutilizáveis por api e worker.
Validado manualmente contra a stack real (não só typecheck): login com
senha errada → 401 genérico; refresh rotaciona e invalida o token
anterior; reuso do token antigo → replay detectado, cadeia revogada;
logout idempotente; audit log com a sequência completa de eventos;
CNPJ inválido rejeitado; mass-assignment rejeitado.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
80
packages/database/src/repositories/users.ts
Normal file
80
packages/database/src/repositories/users.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { query } from "../pool.js";
|
||||
import type { User } from "./types.js";
|
||||
|
||||
export async function countUsers(): Promise<number> {
|
||||
const { rows } = await query<{ count: string }>("SELECT count(*) FROM users");
|
||||
return Number(rows[0]?.count ?? 0);
|
||||
}
|
||||
|
||||
export async function findUserByEmail(email: string): Promise<User | null> {
|
||||
const { rows } = await query<User>(
|
||||
"SELECT * FROM users WHERE lower(email) = lower($1) AND is_active = true",
|
||||
[email],
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findUserById(id: string): Promise<User | null> {
|
||||
const { rows } = await query<User>("SELECT * FROM users WHERE id = $1", [id]);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export interface CreateUserInput {
|
||||
email: string;
|
||||
passwordHash: string;
|
||||
passwordAlgoVersion: number;
|
||||
roleKey: string;
|
||||
fullName?: string;
|
||||
createdBy?: string | null;
|
||||
}
|
||||
|
||||
export async function createUser(input: CreateUserInput): Promise<User> {
|
||||
const { rows } = await query<User>(
|
||||
`INSERT INTO users (email, password_hash, password_algo_version, role_key, full_name, created_by)
|
||||
VALUES (lower($1), $2, $3, $4, $5, $6)
|
||||
RETURNING *`,
|
||||
[
|
||||
input.email,
|
||||
input.passwordHash,
|
||||
input.passwordAlgoVersion,
|
||||
input.roleKey,
|
||||
input.fullName ?? null,
|
||||
input.createdBy ?? null,
|
||||
],
|
||||
);
|
||||
const user = rows[0];
|
||||
if (!user) throw new Error("failed to create user");
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function updateUserPassword(
|
||||
userId: string,
|
||||
passwordHash: string,
|
||||
passwordAlgoVersion: number,
|
||||
): Promise<void> {
|
||||
await query("UPDATE users SET password_hash = $1, password_algo_version = $2 WHERE id = $3", [
|
||||
passwordHash,
|
||||
passwordAlgoVersion,
|
||||
userId,
|
||||
]);
|
||||
}
|
||||
|
||||
export async function grantUserApplication(
|
||||
userId: string,
|
||||
applicationKey: "core" | "reseller" | "subscriber",
|
||||
): Promise<void> {
|
||||
await query(
|
||||
`INSERT INTO user_applications (user_id, application_key)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id, application_key) DO NOTHING`,
|
||||
[userId, applicationKey],
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUserApplications(userId: string): Promise<string[]> {
|
||||
const { rows } = await query<{ application_key: string }>(
|
||||
"SELECT application_key FROM user_applications WHERE user_id = $1",
|
||||
[userId],
|
||||
);
|
||||
return rows.map((r) => r.application_key);
|
||||
}
|
||||
Reference in New Issue
Block a user