From 80262c4be38d5d2b6f3244230029fbab0391cbdc Mon Sep 17 00:00:00 2001 From: Antonio Andre Date: Tue, 28 Apr 2026 18:53:44 -0500 Subject: [PATCH 1/2] feat(docs): inclui documentacao de endpoints auth com Swagger UI --- public/openapi.json | 602 ++++++++++++++++++++++++++++++++++++++ server/routes/docs.get.ts | 31 ++ 2 files changed, 633 insertions(+) create mode 100644 public/openapi.json create mode 100644 server/routes/docs.get.ts diff --git a/public/openapi.json b/public/openapi.json new file mode 100644 index 0000000..50a9118 --- /dev/null +++ b/public/openapi.json @@ -0,0 +1,602 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Sistema Auth API", + "description": "Serviço centralizado de autenticação com JWT RS256, refresh token rotation e recuperação de senha.", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://sistema-distribuido-trabalho-faculd.vercel.app", + "description": "Produção" + }, + { + "url": "http://localhost:3000", + "description": "Desenvolvimento local" + } + ], + "tags": [ + { + "name": "Auth", + "description": "Registro, login, refresh e recuperação de senha" + }, + { + "name": "Profile", + "description": "Dados do usuário autenticado" + }, + { + "name": "Dashboard", + "description": "Endpoint protegido de exemplo (orquestração A→B)" + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Access token JWT assinado com RS256. Obtido via POST /api/auth/login." + } + }, + "schemas": { + "UserResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + }, + "email": { + "type": "string", + "format": "email", + "example": "usuario@exemplo.com" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2026-04-28T12:00:00.000Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "example": "2026-04-28T12:00:00.000Z" + } + }, + "required": [ + "id", + "email", + "created_at", + "updated_at" + ] + }, + "TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string", + "description": "JWT RS256 com validade de 15 minutos (900s).", + "example": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImF1dGgta2V5LTEifQ..." + }, + "refresh_token": { + "type": "string", + "description": "Token opaco base64url com validade de 7 dias. Rotacionado a cada uso.", + "example": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." + }, + "token_type": { + "type": "string", + "example": "Bearer" + }, + "expires_in": { + "type": "integer", + "description": "Validade do access_token em segundos.", + "example": 900 + } + }, + "required": [ + "access_token", + "refresh_token", + "token_type", + "expires_in" + ] + }, + "ErrorResponse": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "example": 400 + }, + "message": { + "type": "string", + "example": "Email é obrigatório" + } + }, + "required": [ + "statusCode", + "message" + ] + } + } + }, + "paths": { + "/api/auth/register": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Registrar novo usuário", + "description": "Cria uma conta nova. A senha é armazenada como hash scrypt — nunca em plaintext.", + "operationId": "authRegister", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Normalizado para lowercase internamente.", + "example": "usuario@exemplo.com" + }, + "password": { + "type": "string", + "minLength": 6, + "description": "Mínimo 6 caracteres.", + "example": "senha123" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Usuário criado com sucesso.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse" + } + } + } + }, + "400": { + "description": "Dados inválidos (campo faltando ou senha muito curta).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "statusCode": 400, + "message": "Senha deve ter no mínimo 6 caracteres" + } + } + } + }, + "409": { + "description": "Email já cadastrado.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "statusCode": 409, + "message": "Email já cadastrado" + } + } + } + } + } + } + }, + "/api/auth/login": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Login", + "description": "Autentica o usuário e retorna um par de tokens (access + refresh).", + "operationId": "authLogin", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "usuario@exemplo.com" + }, + "password": { + "type": "string", + "example": "senha123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Autenticado com sucesso.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResponse" + } + } + } + }, + "400": { + "description": "Campos obrigatórios ausentes.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Credenciais inválidas.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "statusCode": 401, + "message": "Credenciais inválidas" + } + } + } + } + } + } + }, + "/api/auth/refresh": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Renovar tokens", + "description": "Troca um refresh token válido por um novo par de tokens. O token antigo é revogado imediatamente (rotação).", + "operationId": "authRefresh", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "refresh_token" + ], + "properties": { + "refresh_token": { + "type": "string", + "description": "Refresh token obtido no login ou na última renovação.", + "example": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Novos tokens emitidos.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenResponse" + } + } + } + }, + "400": { + "description": "refresh_token ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Token inválido, expirado ou já revogado.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "statusCode": 401, + "message": "Refresh token inválido ou expirado" + } + } + } + } + } + } + }, + "/api/auth/forgot-password": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Solicitar reset de senha", + "description": "Gera um token de reset para o email informado. A resposta é sempre genérica para não revelar se o email existe (proteção contra enumeração). Em produção, o token seria enviado por email; aqui é retornado no body para fins de teste.", + "operationId": "authForgotPassword", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "usuario@exemplo.com" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Resposta genérica (independente de o email existir ou não).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Se o email existir, instruções de recuperação foram geradas" + }, + "recovery": { + "type": "object", + "description": "Presente apenas em ambiente de desenvolvimento para facilitar testes.", + "properties": { + "reset_token": { + "type": "string", + "example": "abc123..." + }, + "reset_url": { + "type": "string", + "format": "uri", + "example": "http://localhost:3000/reset-password?token=abc123..." + }, + "expires_in": { + "type": "integer", + "example": 900 + } + } + } + } + } + } + } + }, + "400": { + "description": "Campo email ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/auth/reset-password": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Redefinir senha", + "description": "Aplica a nova senha usando o token de reset. O token é de uso único — após utilizado, é marcado como consumido.", + "operationId": "authResetPassword", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "token", + "new_password" + ], + "properties": { + "token": { + "type": "string", + "description": "Token recebido via forgot-password.", + "example": "abc123..." + }, + "new_password": { + "type": "string", + "minLength": 6, + "description": "Nova senha, mínimo 6 caracteres.", + "example": "novaSenha456" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Senha atualizada com sucesso.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Senha atualizada com sucesso" + } + } + } + } + } + }, + "400": { + "description": "Dados inválidos (campo faltando ou senha curta demais).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Token inválido, expirado ou já utilizado.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "statusCode": 401, + "message": "Token de reset inválido ou expirado" + } + } + } + } + } + } + }, + "/profile/me": { + "get": { + "tags": [ + "Profile" + ], + "summary": "Perfil do usuário autenticado", + "description": "Retorna os dados do usuário identificado pelo access token JWT.", + "operationId": "profileMe", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Dados do usuário.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse" + } + } + } + }, + "401": { + "description": "Token ausente, inválido ou expirado.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "statusCode": 401, + "message": "Token inválido ou expirado" + } + } + } + }, + "404": { + "description": "Usuário não encontrado (token válido mas conta removida).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/dashboard": { + "get": { + "tags": [ + "Dashboard" + ], + "summary": "Dashboard (orquestração A→B)", + "description": "Endpoint protegido que demonstra orquestração entre serviços: chama /profile/me internamente e agrega os resultados de duas APIs (A e B), verificando que o `sub` do JWT é consistente entre elas.", + "operationId": "dashboard", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Dados agregados do dashboard.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sub_from_api_a": { + "type": "string", + "format": "uuid", + "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + }, + "sub_from_api_b": { + "type": "string", + "format": "uuid", + "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + }, + "same_subject": { + "type": "boolean", + "example": true + }, + "profile": { + "$ref": "#/components/schemas/UserResponse" + } + } + } + } + } + }, + "401": { + "description": "Token ausente, inválido ou expirado.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/server/routes/docs.get.ts b/server/routes/docs.get.ts new file mode 100644 index 0000000..19fd291 --- /dev/null +++ b/server/routes/docs.get.ts @@ -0,0 +1,31 @@ +export default defineEventHandler((event) => { + setHeader(event, 'Content-Type', 'text/html; charset=utf-8') + + return ` + + + + + Sistema Auth — API Docs + + + + +
+ + + + ` +}) From 5b53211727b156fd94ab8c7a244863cd1b97ad04 Mon Sep 17 00:00:00 2001 From: Antonio Andre Date: Tue, 28 Apr 2026 19:05:22 -0500 Subject: [PATCH 2/2] docs: atualiza README com url de producao e doc Swagger --- README.md | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7b88bfd..28020d2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,13 @@ MVP de autenticação central para serviços distribuídos. - Prisma + Postgres - JOSE (JWT) -## Setup +## Produção + +**Base URL:** `https://sistema-distribuido-trabalho-faculd.vercel.app` + +**Documentação interativa (Swagger):** [`/docs`](https://sistema-distribuido-trabalho-faculd.vercel.app/docs) + +## Setup local 1. Instale dependências: @@ -56,7 +62,7 @@ npm run dev ## Usuários de seed - `student@example.com` / `123456` -s + ## Estrutura da tabela `User` A tabela `User` possui apenas: @@ -69,13 +75,15 @@ A tabela `User` possui apenas: ## Endpoints -- `POST /auth/login` -- `POST /auth/refresh` -- `POST /auth/register` -- `POST /auth/forgot-password` -- `POST /auth/reset-password` -- `GET /profile/me` (protegida) -- `GET /dashboard` (protegida, chama `/profile/me`) +| Método | Caminho | Auth | Descrição | +|--------|---------|------|-----------| +| POST | `/api/auth/register` | Não | Cria novo usuário | +| POST | `/api/auth/login` | Não | Autentica e retorna tokens | +| POST | `/api/auth/refresh` | Não | Rotaciona refresh token | +| POST | `/api/auth/forgot-password` | Não | Solicita reset de senha | +| POST | `/api/auth/reset-password` | Não | Aplica nova senha com token | +| GET | `/profile/me` | Sim (Bearer) | Retorna usuário autenticado | +| GET | `/dashboard` | Sim (Bearer) | Orquestração A→B | ## Guia para serviços consumidores @@ -98,7 +106,7 @@ A identidade confiável do usuário é sempre o `sub`. ### 1) Cadastro ```bash -curl -X POST http://localhost:3000/auth/register \ +curl -X POST https://sistema-distribuido-trabalho-faculd.vercel.app/api/auth/register \ -H 'Content-Type: application/json' \ -d '{"email":"novo.usuario@example.com","password":"senha123"}' ``` @@ -108,7 +116,7 @@ Fluxo recomendado no cliente: `register -> login`. ### 2) Login ```bash -curl -X POST http://localhost:3000/auth/login \ +curl -X POST https://sistema-distribuido-trabalho-faculd.vercel.app/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"novo.usuario@example.com","password":"senha123"}' ``` @@ -116,17 +124,17 @@ curl -X POST http://localhost:3000/auth/login \ ### 3) Forgot password (sem SMTP, modo didático) ```bash -curl -X POST http://localhost:3000/auth/forgot-password \ +curl -X POST https://sistema-distribuido-trabalho-faculd.vercel.app/api/auth/forgot-password \ -H 'Content-Type: application/json' \ -d '{"email":"novo.usuario@example.com"}' ``` -Observação: neste MVP didático a resposta já traz `recovery.reset_token` e `recovery.reset_url`. +Observação: neste MVP a resposta já traz `recovery.reset_token` e `recovery.reset_url`. ### 4) Reset password ```bash -curl -X POST http://localhost:3000/auth/reset-password \ +curl -X POST https://sistema-distribuido-trabalho-faculd.vercel.app/api/auth/reset-password \ -H 'Content-Type: application/json' \ -d '{"token":"","new_password":"novaSenha123"}' ``` @@ -134,7 +142,7 @@ curl -X POST http://localhost:3000/auth/reset-password \ ### 5) Login com nova senha ```bash -curl -X POST http://localhost:3000/auth/login \ +curl -X POST https://sistema-distribuido-trabalho-faculd.vercel.app/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"novo.usuario@example.com","password":"novaSenha123"}' ``` @@ -142,21 +150,21 @@ curl -X POST http://localhost:3000/auth/login \ ### 6) Rota protegida ```bash -curl http://localhost:3000/profile/me \ +curl https://sistema-distribuido-trabalho-faculd.vercel.app/profile/me \ -H "Authorization: Bearer " ``` ### 7) Chamada entre serviços (A -> B) ```bash -curl http://localhost:3000/dashboard \ +curl https://sistema-distribuido-trabalho-faculd.vercel.app/dashboard \ -H "Authorization: Bearer " ``` ### 8) Refresh ```bash -curl -X POST http://localhost:3000/auth/refresh \ +curl -X POST https://sistema-distribuido-trabalho-faculd.vercel.app/api/auth/refresh \ -H 'Content-Type: application/json' \ -d '{"refresh_token":""}' ```