From 4680035e027c58d046f9cfd3d77ea190bcf5d9b7 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Fri, 19 Dec 2025 17:54:16 -0300 Subject: [PATCH] Add auth docs, user CRUD, and password pepper --- backend/cmd/api/main.go | 3 + backend/docs/docs.go | 827 ++++++++++++++++++++++- backend/docs/swagger.json | 827 ++++++++++++++++++++++- backend/docs/swagger.yaml | 533 ++++++++++++++- backend/internal/config/config.go | 2 + backend/internal/http/handler/handler.go | 89 ++- backend/internal/server/server.go | 2 +- backend/internal/usecase/usecase.go | 25 +- 8 files changed, 2282 insertions(+), 26 deletions(-) diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 7255717..fd17a29 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -21,6 +21,9 @@ import ( // @Schemes http // @contact.name Engenharia SaveInMed // @contact.email devops@saveinmed.com +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization func main() { cfg := config.Load() diff --git a/backend/docs/docs.go b/backend/docs/docs.go index a3ae9f7..f398f63 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -250,6 +250,532 @@ const docTemplate = `{ } } } + }, + "/api/v1/auth/login": { + "post": { + "description": "Autentica usuário e retorna token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Login", + "parameters": [ + { + "description": "Credenciais", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.loginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "Cria um usuário e opcionalmente uma empresa, retornando token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Cadastro de usuário", + "parameters": [ + { + "description": "Dados do usuário e empresa", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.registerAuthRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/payments/webhook": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pagamentos" + ], + "summary": "Recebe notificações do Mercado Pago", + "parameters": [ + { + "description": "Evento do gateway", + "name": "notification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.PaymentWebhookEvent" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.PaymentSplitResult" + } + } + } + } + }, + "/api/v1/shipments": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Logistica" + ], + "summary": "Gera guia de postagem/transporte", + "parameters": [ + { + "description": "Dados de envio", + "name": "shipment", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.createShipmentRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Shipment" + } + } + } + } + }, + "/api/v1/shipments/{order_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Logistica" + ], + "summary": "Rastreia entrega", + "parameters": [ + { + "type": "string", + "description": "Order ID", + "name": "order_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Shipment" + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Usuários" + ], + "summary": "Listar usuários", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Filtro por empresa", + "name": "company_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.UserPage" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Usuários" + ], + "summary": "Criar usuário", + "parameters": [ + { + "description": "Novo usuário", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.createUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Usuários" + ], + "summary": "Obter usuário", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Usuários" + ], + "summary": "Atualizar usuário", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Campos para atualização", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.updateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "tags": [ + "Usuários" + ], + "summary": "Excluir usuário", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } } }, "definitions": { @@ -268,11 +794,14 @@ const docTemplate = `{ "id": { "type": "string" }, - "role": { - "description": "pharmacy, distributor, admin", + "is_verified": { + "type": "boolean" + }, + "license_number": { "type": "string" }, - "sanitary_license": { + "role": { + "description": "pharmacy, distributor, admin", "type": "string" }, "updated_at": { @@ -301,6 +830,9 @@ const docTemplate = `{ "seller_id": { "type": "string" }, + "shipping": { + "$ref": "#/definitions/domain.ShippingAddress" + }, "status": { "$ref": "#/definitions/domain.OrderStatus" }, @@ -376,6 +908,52 @@ const docTemplate = `{ } } }, + "domain.PaymentSplitResult": { + "type": "object", + "properties": { + "marketplace_fee": { + "type": "integer" + }, + "order_id": { + "type": "string" + }, + "payment_id": { + "type": "string" + }, + "seller_receivable": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "total_paid_amount": { + "type": "integer" + } + } + }, + "domain.PaymentWebhookEvent": { + "type": "object", + "properties": { + "marketplace_fee": { + "type": "integer" + }, + "order_id": { + "type": "string" + }, + "payment_id": { + "type": "string" + }, + "seller_amount": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "total_paid_amount": { + "type": "integer" + } + } + }, "domain.Product": { "type": "object", "properties": { @@ -411,6 +989,124 @@ const docTemplate = `{ } } }, + "domain.Shipment": { + "type": "object", + "properties": { + "carrier": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "external_tracking": { + "type": "string" + }, + "id": { + "type": "string" + }, + "order_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tracking_code": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ShippingAddress": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "complement": { + "type": "string" + }, + "country": { + "type": "string" + }, + "district": { + "type": "string" + }, + "number": { + "type": "string" + }, + "recipient_name": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + }, + "zip_code": { + "type": "string" + } + } + }, + "domain.User": { + "type": "object", + "properties": { + "company_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "role": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.UserPage": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.User" + } + } + } + }, + "handler.authResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, "handler.createOrderRequest": { "type": "object", "properties": { @@ -425,6 +1121,80 @@ const docTemplate = `{ }, "seller_id": { "type": "string" + }, + "shipping": { + "$ref": "#/definitions/domain.ShippingAddress" + } + } + }, + "handler.createShipmentRequest": { + "type": "object", + "properties": { + "carrier": { + "type": "string" + }, + "external_tracking": { + "type": "string" + }, + "order_id": { + "type": "string" + }, + "tracking_code": { + "type": "string" + } + } + }, + "handler.createUserRequest": { + "type": "object", + "properties": { + "company_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "handler.loginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "handler.registerAuthRequest": { + "type": "object", + "properties": { + "company": { + "$ref": "#/definitions/handler.registerCompanyTarget" + }, + "company_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" } } }, @@ -437,10 +1207,30 @@ const docTemplate = `{ "corporate_name": { "type": "string" }, - "role": { + "license_number": { "type": "string" }, - "sanitary_license": { + "role": { + "type": "string" + } + } + }, + "handler.registerCompanyTarget": { + "type": "object", + "properties": { + "cnpj": { + "type": "string" + }, + "corporate_name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "license_number": { + "type": "string" + }, + "role": { "type": "string" } } @@ -478,6 +1268,33 @@ const docTemplate = `{ "type": "string" } } + }, + "handler.updateUserRequest": { + "type": "object", + "properties": { + "company_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" } } }` diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 26e570b..d7ba94a 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -246,6 +246,532 @@ } } } + }, + "/api/v1/auth/login": { + "post": { + "description": "Autentica usuário e retorna token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Login", + "parameters": [ + { + "description": "Credenciais", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.loginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "Cria um usuário e opcionalmente uma empresa, retornando token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Cadastro de usuário", + "parameters": [ + { + "description": "Dados do usuário e empresa", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.registerAuthRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/payments/webhook": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pagamentos" + ], + "summary": "Recebe notificações do Mercado Pago", + "parameters": [ + { + "description": "Evento do gateway", + "name": "notification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.PaymentWebhookEvent" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.PaymentSplitResult" + } + } + } + } + }, + "/api/v1/shipments": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Logistica" + ], + "summary": "Gera guia de postagem/transporte", + "parameters": [ + { + "description": "Dados de envio", + "name": "shipment", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.createShipmentRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Shipment" + } + } + } + } + }, + "/api/v1/shipments/{order_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Logistica" + ], + "summary": "Rastreia entrega", + "parameters": [ + { + "type": "string", + "description": "Order ID", + "name": "order_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Shipment" + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Usuários" + ], + "summary": "Listar usuários", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Filtro por empresa", + "name": "company_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.UserPage" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Usuários" + ], + "summary": "Criar usuário", + "parameters": [ + { + "description": "Novo usuário", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.createUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Usuários" + ], + "summary": "Obter usuário", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Usuários" + ], + "summary": "Atualizar usuário", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Campos para atualização", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.updateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "tags": [ + "Usuários" + ], + "summary": "Excluir usuário", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } } }, "definitions": { @@ -264,11 +790,14 @@ "id": { "type": "string" }, - "role": { - "description": "pharmacy, distributor, admin", + "is_verified": { + "type": "boolean" + }, + "license_number": { "type": "string" }, - "sanitary_license": { + "role": { + "description": "pharmacy, distributor, admin", "type": "string" }, "updated_at": { @@ -297,6 +826,9 @@ "seller_id": { "type": "string" }, + "shipping": { + "$ref": "#/definitions/domain.ShippingAddress" + }, "status": { "$ref": "#/definitions/domain.OrderStatus" }, @@ -372,6 +904,52 @@ } } }, + "domain.PaymentSplitResult": { + "type": "object", + "properties": { + "marketplace_fee": { + "type": "integer" + }, + "order_id": { + "type": "string" + }, + "payment_id": { + "type": "string" + }, + "seller_receivable": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "total_paid_amount": { + "type": "integer" + } + } + }, + "domain.PaymentWebhookEvent": { + "type": "object", + "properties": { + "marketplace_fee": { + "type": "integer" + }, + "order_id": { + "type": "string" + }, + "payment_id": { + "type": "string" + }, + "seller_amount": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "total_paid_amount": { + "type": "integer" + } + } + }, "domain.Product": { "type": "object", "properties": { @@ -407,6 +985,124 @@ } } }, + "domain.Shipment": { + "type": "object", + "properties": { + "carrier": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "external_tracking": { + "type": "string" + }, + "id": { + "type": "string" + }, + "order_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tracking_code": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ShippingAddress": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "complement": { + "type": "string" + }, + "country": { + "type": "string" + }, + "district": { + "type": "string" + }, + "number": { + "type": "string" + }, + "recipient_name": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + }, + "zip_code": { + "type": "string" + } + } + }, + "domain.User": { + "type": "object", + "properties": { + "company_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "role": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.UserPage": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.User" + } + } + } + }, + "handler.authResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, "handler.createOrderRequest": { "type": "object", "properties": { @@ -421,6 +1117,80 @@ }, "seller_id": { "type": "string" + }, + "shipping": { + "$ref": "#/definitions/domain.ShippingAddress" + } + } + }, + "handler.createShipmentRequest": { + "type": "object", + "properties": { + "carrier": { + "type": "string" + }, + "external_tracking": { + "type": "string" + }, + "order_id": { + "type": "string" + }, + "tracking_code": { + "type": "string" + } + } + }, + "handler.createUserRequest": { + "type": "object", + "properties": { + "company_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "handler.loginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "handler.registerAuthRequest": { + "type": "object", + "properties": { + "company": { + "$ref": "#/definitions/handler.registerCompanyTarget" + }, + "company_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" } } }, @@ -433,10 +1203,30 @@ "corporate_name": { "type": "string" }, - "role": { + "license_number": { "type": "string" }, - "sanitary_license": { + "role": { + "type": "string" + } + } + }, + "handler.registerCompanyTarget": { + "type": "object", + "properties": { + "cnpj": { + "type": "string" + }, + "corporate_name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "license_number": { + "type": "string" + }, + "role": { "type": "string" } } @@ -474,6 +1264,33 @@ "type": "string" } } + }, + "handler.updateUserRequest": { + "type": "object", + "properties": { + "company_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" } } } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 684829f..a58e6b4 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -10,11 +10,13 @@ definitions: type: string id: type: string + is_verified: + type: boolean + license_number: + type: string role: description: pharmacy, distributor, admin type: string - sanitary_license: - type: string updated_at: type: string type: object @@ -32,6 +34,8 @@ definitions: type: array seller_id: type: string + shipping: + $ref: '#/definitions/domain.ShippingAddress' status: $ref: '#/definitions/domain.OrderStatus' total_cents: @@ -83,6 +87,36 @@ definitions: seller_receivable: type: integer type: object + domain.PaymentSplitResult: + properties: + marketplace_fee: + type: integer + order_id: + type: string + payment_id: + type: string + seller_receivable: + type: integer + status: + type: string + total_paid_amount: + type: integer + type: object + domain.PaymentWebhookEvent: + properties: + marketplace_fee: + type: integer + order_id: + type: string + payment_id: + type: string + seller_amount: + type: integer + status: + type: string + total_paid_amount: + type: integer + type: object domain.Product: properties: batch: @@ -106,6 +140,83 @@ definitions: updated_at: type: string type: object + domain.Shipment: + properties: + carrier: + type: string + created_at: + type: string + external_tracking: + type: string + id: + type: string + order_id: + type: string + status: + type: string + tracking_code: + type: string + updated_at: + type: string + type: object + domain.ShippingAddress: + properties: + city: + type: string + complement: + type: string + country: + type: string + district: + type: string + number: + type: string + recipient_name: + type: string + state: + type: string + street: + type: string + zip_code: + type: string + type: object + domain.User: + properties: + company_id: + type: string + created_at: + type: string + email: + type: string + id: + type: string + name: + type: string + role: + type: string + updated_at: + type: string + type: object + domain.UserPage: + properties: + page: + type: integer + page_size: + type: integer + total: + type: integer + users: + items: + $ref: '#/definitions/domain.User' + type: array + type: object + handler.authResponse: + properties: + expires_at: + type: string + token: + type: string + type: object handler.createOrderRequest: properties: buyer_id: @@ -116,6 +227,54 @@ definitions: type: array seller_id: type: string + shipping: + $ref: '#/definitions/domain.ShippingAddress' + type: object + handler.createShipmentRequest: + properties: + carrier: + type: string + external_tracking: + type: string + order_id: + type: string + tracking_code: + type: string + type: object + handler.createUserRequest: + properties: + company_id: + type: string + email: + type: string + name: + type: string + password: + type: string + role: + type: string + type: object + handler.loginRequest: + properties: + email: + type: string + password: + type: string + type: object + handler.registerAuthRequest: + properties: + company: + $ref: '#/definitions/handler.registerCompanyTarget' + company_id: + type: string + email: + type: string + name: + type: string + password: + type: string + role: + type: string type: object handler.registerCompanyRequest: properties: @@ -123,9 +282,22 @@ definitions: type: string corporate_name: type: string + license_number: + type: string role: type: string - sanitary_license: + type: object + handler.registerCompanyTarget: + properties: + cnpj: + type: string + corporate_name: + type: string + id: + type: string + license_number: + type: string + role: type: string type: object handler.registerProductRequest: @@ -150,6 +322,19 @@ definitions: status: type: string type: object + handler.updateUserRequest: + properties: + company_id: + type: string + email: + type: string + name: + type: string + password: + type: string + role: + type: string + type: object info: contact: email: devops@saveinmed.com @@ -310,6 +495,348 @@ paths: summary: Cadastro de produto com rastreabilidade de lote tags: - Produtos + /api/v1/auth/login: + post: + consumes: + - application/json + description: Autentica usuário e retorna token JWT. + parameters: + - description: Credenciais + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.loginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.authResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Login + tags: + - Autenticação + /api/v1/auth/register: + post: + consumes: + - application/json + description: Cria um usuário e opcionalmente uma empresa, retornando token JWT. + parameters: + - description: Dados do usuário e empresa + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.registerAuthRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/handler.authResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Cadastro de usuário + tags: + - Autenticação + /api/v1/payments/webhook: + post: + consumes: + - application/json + parameters: + - description: Evento do gateway + in: body + name: notification + required: true + schema: + $ref: '#/definitions/domain.PaymentWebhookEvent' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.PaymentSplitResult' + summary: Recebe notificações do Mercado Pago + tags: + - Pagamentos + /api/v1/shipments: + post: + consumes: + - application/json + parameters: + - description: Dados de envio + in: body + name: shipment + required: true + schema: + $ref: '#/definitions/handler.createShipmentRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Shipment' + summary: Gera guia de postagem/transporte + tags: + - Logistica + /api/v1/shipments/{order_id}: + get: + parameters: + - description: Order ID + in: path + name: order_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Shipment' + summary: Rastreia entrega + tags: + - Logistica + /api/v1/users: + get: + parameters: + - description: Página + in: query + name: page + type: integer + - description: Tamanho da página + in: query + name: page_size + type: integer + - description: Filtro por empresa + in: query + name: company_id + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.UserPage' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Listar usuários + tags: + - Usuários + post: + consumes: + - application/json + parameters: + - description: Novo usuário + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.createUserRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.User' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "403": + description: Forbidden + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Criar usuário + tags: + - Usuários + /api/v1/users/{id}: + delete: + parameters: + - description: User ID + in: path + name: id + required: true + type: string + responses: + "204": + description: No Content + schema: + type: string + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "403": + description: Forbidden + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Excluir usuário + tags: + - Usuários + get: + parameters: + - description: User ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.User' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "403": + description: Forbidden + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Obter usuário + tags: + - Usuários + put: + consumes: + - application/json + parameters: + - description: User ID + in: path + name: id + required: true + type: string + - description: Campos para atualização + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.updateUserRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.User' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "403": + description: Forbidden + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Atualizar usuário + tags: + - Usuários schemes: - http +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 5cc1892..a82d712 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -20,6 +20,7 @@ type Config struct { MarketplaceCommission float64 JWTSecret string JWTExpiresIn time.Duration + PasswordPepper string CORSOrigins []string } @@ -37,6 +38,7 @@ func Load() Config { MarketplaceCommission: getEnvFloat("MARKETPLACE_COMMISSION", 2.5), JWTSecret: getEnv("JWT_SECRET", "dev-secret"), JWTExpiresIn: getEnvDuration("JWT_EXPIRES_IN", 24*time.Hour), + PasswordPepper: getEnv("PASSWORD_PEPPER", ""), CORSOrigins: getEnvStringSlice("CORS_ORIGINS", []string{"*"}), } diff --git a/backend/internal/http/handler/handler.go b/backend/internal/http/handler/handler.go index 8fe262d..2864ec5 100644 --- a/backend/internal/http/handler/handler.go +++ b/backend/internal/http/handler/handler.go @@ -27,7 +27,17 @@ func New(svc *usecase.Service) *Handler { return &Handler{svc: svc} } -// Register handles sign-up creating a company when requested. +// Register godoc +// @Summary Cadastro de usuário +// @Description Cria um usuário e opcionalmente uma empresa, retornando token JWT. +// @Tags Autenticação +// @Accept json +// @Produce json +// @Param payload body registerAuthRequest true "Dados do usuário e empresa" +// @Success 201 {object} authResponse +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/auth/register [post] func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { var req registerAuthRequest if err := decodeJSON(r.Context(), r, &req); err != nil { @@ -77,7 +87,17 @@ func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, authResponse{Token: token, ExpiresAt: exp}) } -// Login validates credentials and emits a JWT token. +// Login godoc +// @Summary Login +// @Description Autentica usuário e retorna token JWT. +// @Tags Autenticação +// @Accept json +// @Produce json +// @Param payload body loginRequest true "Credenciais" +// @Success 200 {object} authResponse +// @Failure 400 {object} map[string]string +// @Failure 401 {object} map[string]string +// @Router /api/v1/auth/login [post] func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { var req loginRequest if err := decodeJSON(r.Context(), r, &req); err != nil { @@ -636,7 +656,18 @@ func (h *Handler) GetAdminDashboard(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, dashboard) } -// CreateUser handles the creation of platform users. +// CreateUser godoc +// @Summary Criar usuário +// @Tags Usuários +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param payload body createUserRequest true "Novo usuário" +// @Success 201 {object} domain.User +// @Failure 400 {object} map[string]string +// @Failure 403 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/users [post] func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { @@ -676,7 +707,18 @@ func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, user) } -// ListUsers supports pagination and optional company filter. +// ListUsers godoc +// @Summary Listar usuários +// @Tags Usuários +// @Security BearerAuth +// @Produce json +// @Param page query int false "Página" +// @Param page_size query int false "Tamanho da página" +// @Param company_id query string false "Filtro por empresa" +// @Success 200 {object} domain.UserPage +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/users [get] func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { @@ -713,7 +755,17 @@ func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, pageResult) } -// GetUser returns a single user by ID. +// GetUser godoc +// @Summary Obter usuário +// @Tags Usuários +// @Security BearerAuth +// @Produce json +// @Param id path string true "User ID" +// @Success 200 {object} domain.User +// @Failure 400 {object} map[string]string +// @Failure 403 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /api/v1/users/{id} [get] func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { @@ -743,7 +795,20 @@ func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, user) } -// UpdateUser updates profile fields or password. +// UpdateUser godoc +// @Summary Atualizar usuário +// @Tags Usuários +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Param payload body updateUserRequest true "Campos para atualização" +// @Success 200 {object} domain.User +// @Failure 400 {object} map[string]string +// @Failure 403 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/users/{id} [put] func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { @@ -802,7 +867,17 @@ func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, user) } -// DeleteUser removes a user by ID. +// DeleteUser godoc +// @Summary Excluir usuário +// @Tags Usuários +// @Security BearerAuth +// @Param id path string true "User ID" +// @Success 204 {string} string "No Content" +// @Failure 400 {object} map[string]string +// @Failure 403 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/users/{id} [delete] func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index 4cf1859..ca2818e 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -37,7 +37,7 @@ func New(cfg config.Config) (*Server, error) { repo := postgres.New(db) gateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission) - svc := usecase.NewService(repo, gateway, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn) + svc := usecase.NewService(repo, gateway, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper) h := handler.New(svc) mux := http.NewServeMux() diff --git a/backend/internal/usecase/usecase.go b/backend/internal/usecase/usecase.go index 95b4a75..afabaff 100644 --- a/backend/internal/usecase/usecase.go +++ b/backend/internal/usecase/usecase.go @@ -61,11 +61,19 @@ type Service struct { jwtSecret []byte tokenTTL time.Duration marketplaceCommission float64 + passwordPepper string } // NewService wires use cases together. -func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration) *Service { - return &Service{repo: repo, pay: pay, jwtSecret: []byte(jwtSecret), tokenTTL: tokenTTL, marketplaceCommission: commissionPct} +func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration, passwordPepper string) *Service { + return &Service{ + repo: repo, + pay: pay, + jwtSecret: []byte(jwtSecret), + tokenTTL: tokenTTL, + marketplaceCommission: commissionPct, + passwordPepper: passwordPepper, + } } func (s *Service) RegisterCompany(ctx context.Context, company *domain.Company) error { @@ -171,7 +179,7 @@ func (s *Service) HandlePaymentWebhook(ctx context.Context, event domain.Payment } func (s *Service) CreateUser(ctx context.Context, user *domain.User, password string) error { - hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + hashed, err := bcrypt.GenerateFromPassword([]byte(s.pepperPassword(password)), bcrypt.DefaultCost) if err != nil { return err } @@ -207,7 +215,7 @@ func (s *Service) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, erro func (s *Service) UpdateUser(ctx context.Context, user *domain.User, newPassword string) error { if newPassword != "" { - hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + hashed, err := bcrypt.GenerateFromPassword([]byte(s.pepperPassword(newPassword)), bcrypt.DefaultCost) if err != nil { return err } @@ -378,7 +386,7 @@ func (s *Service) Authenticate(ctx context.Context, email, password string) (str return "", time.Time{}, err } - if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(s.pepperPassword(password))); err != nil { return "", time.Time{}, errors.New("invalid credentials") } @@ -399,6 +407,13 @@ func (s *Service) Authenticate(ctx context.Context, email, password string) (str return signed, expiresAt, nil } +func (s *Service) pepperPassword(password string) string { + if s.passwordPepper == "" { + return password + } + return password + s.passwordPepper +} + // VerifyCompany marks a company as verified. func (s *Service) VerifyCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) { company, err := s.repo.GetCompany(ctx, id)