Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions backend/src/middleware/requireRole.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import test from "node:test";
import assert from "node:assert/strict";
import express from "express";
import { once } from "node:events";
import { requireRole } from "./requireRole.js";
import { UserRole } from "../modules/auth/auth.types.js";

test("escrita de projetos exige sessão e perfil PO ou administrador", async () => {
let role: UserRole | undefined;
const app = express();
app.use((req, _res, next) => {
if (role) req.auth = { id: "user", nome: "Pessoa", email: "test@example.com", role };
next();
});
app.post("/projects", requireRole("admin", "po"), (_req, res) => { res.sendStatus(201); });
const server = app.listen(0, "127.0.0.1");
await once(server, "listening");
try {
const address = server.address();
assert.ok(address && typeof address !== "string");
for (const [profile, expected] of [[undefined, 401], ["dev", 403], ["po", 201], ["admin", 201]] as const) {
role = profile;
const result = await fetch(`http://127.0.0.1:${address.port}/projects`, { method: "POST" });
assert.equal(result.status, expected, `perfil ${profile}`);
}
} finally {
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
}
});
16 changes: 16 additions & 0 deletions backend/src/middleware/requireRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextFunction, Request, Response } from "express";
import { UserRole } from "../modules/auth/auth.types.js";

export function requireRole(...roles: UserRole[]) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.auth) {
res.status(401).json({ error: "Autenticação necessária.", code: "UNAUTHORIZED" });
return;
}
if (!roles.includes(req.auth.role)) {
res.status(403).json({ error: "Seu perfil não permite alterar projetos.", code: "FORBIDDEN" });
return;
}
next();
};
}
10 changes: 6 additions & 4 deletions backend/src/modules/projects/projects.routes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Router } from "express";
import { projectsController } from "./projects.controller.js";
import { requireRole } from "../../middleware/requireRole.js";

export const projectsRouter = Router();

projectsRouter.post("/", projectsController.create);
const canWrite = requireRole("admin", "po");
projectsRouter.post("/", canWrite, projectsController.create);
projectsRouter.get("/", projectsController.list);
projectsRouter.get("/:id", projectsController.getById);
projectsRouter.put("/:id", projectsController.update);
projectsRouter.patch("/:id", projectsController.update);
projectsRouter.patch("/:id/archive", projectsController.archive);
projectsRouter.put("/:id", canWrite, projectsController.update);
projectsRouter.patch("/:id", canWrite, projectsController.update);
projectsRouter.patch("/:id/archive", canWrite, projectsController.archive);
11 changes: 10 additions & 1 deletion docs/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ paths:
post:
summary: Cadastra um novo projeto (PBI-01.1.1 / S1-03)
operationId: createProject
description: Escrita permitida apenas aos perfis po e admin; dev recebe 403.
security:
- cookieAuth: []
requestBody:
Expand All @@ -113,6 +114,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ProjectResponse'
'403':
description: Perfil sem permissão de escrita
'400':
$ref: '#/components/responses/ValidationError'
'401':
Expand Down Expand Up @@ -207,6 +210,7 @@ paths:
put:
summary: Atualiza dados de um projeto (S1-03 / S1-08)
operationId: updateProject
description: Escrita permitida apenas aos perfis po e admin; dev recebe 403. PATCH no mesmo caminho é um alias desta operação.
security:
- cookieAuth: []
parameters:
Expand All @@ -229,6 +233,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ProjectResponse'
'403':
description: Perfil sem permissão de escrita
'400':
$ref: '#/components/responses/ValidationError'
'401':
Expand All @@ -250,6 +256,7 @@ paths:
patch:
summary: Arquiva um projeto (S1-03 / S1-09)
operationId: archiveProject
description: Escrita permitida apenas aos perfis po e admin; dev recebe 403.
security:
- cookieAuth: []
parameters:
Expand All @@ -275,6 +282,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ProjectResponse'
'403':
description: Perfil sem permissão de escrita
'401':
$ref: '#/components/responses/Unauthorized'
'404':
Expand Down Expand Up @@ -609,4 +618,4 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: '#/components/schemas/Error'
32 changes: 32 additions & 0 deletions frontend/AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# S1-02 — Autenticação no frontend

Login, logout, restauração de sessão e proteção das páginas internas integrados
à S1-01. A sessão usa cookie HttpOnly, sem credenciais no armazenamento do navegador.

## Contrato implementado

- `POST /api/v1/auth/login`: `{ email, password }`; retorna `{ user: { id, nome, email, role } }`.
- `GET /api/v1/auth/me`: restaura a sessão com o mesmo formato de usuário.
- `POST /api/v1/auth/logout`: revoga a sessão; retorna 204.

O adaptador do frontend converte `nome` para o campo visual `name` e valida o papel.
401 exige login; 403 informa acesso indisponível; 429 informa limite de tentativas.
Erros de rede e respostas inválidas bloqueiam o conteúdo com opção de tentar novamente.
A saída só é confirmada após sucesso ou 401 do servidor.

Rotas protegidas: `/`, `/requirements` e `/rag`. Destinos de retorno são validados
em `src/auth/navigation.ts`, preservando filtros e fragmentos de páginas conhecidas.
A sessão é revalidada ao navegar, usar o histórico e recuperar foco, sem polling.
O cliente HTTP compartilhado informa expiração ao receber 401 em recursos privados.

## Validação

No frontend: `npm test` e `npm run build`.
No backend: `npm ci`, `npm run build` e `npm test`.
Os testes do frontend usam o contrato da S1-01; os testes HTTP do backend cobrem
cookies, autenticação e consulta da sessão, com repositórios em memória.

Para execução com PostgreSQL, configure os arquivos de ambiente conforme o README,
aplique as migrações (inclusive `004_identity_domain.sql`) e utilize um usuário
ativo com hash de senha compatível com a S1-01. O proxy Vite encaminha `/api` à
porta 3001; o frontend inicia na porta 5173.
Loading
Loading