From 80262c4be38d5d2b6f3244230029fbab0391cbdc Mon Sep 17 00:00:00 2001 From: Antonio Andre Date: Tue, 28 Apr 2026 18:53:44 -0500 Subject: [PATCH] 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 + + + + +
+ + + + ` +})