Add auth docs, user CRUD, and password pepper

This commit is contained in:
Tiago Yamamoto 2025-12-19 17:54:16 -03:00
parent 916225f19e
commit 4680035e02
8 changed files with 2282 additions and 26 deletions

View file

@ -21,6 +21,9 @@ import (
// @Schemes http // @Schemes http
// @contact.name Engenharia SaveInMed // @contact.name Engenharia SaveInMed
// @contact.email devops@saveinmed.com // @contact.email devops@saveinmed.com
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() { func main() {
cfg := config.Load() cfg := config.Load()

View file

@ -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": { "definitions": {
@ -268,11 +794,14 @@ const docTemplate = `{
"id": { "id": {
"type": "string" "type": "string"
}, },
"role": { "is_verified": {
"description": "pharmacy, distributor, admin", "type": "boolean"
},
"license_number": {
"type": "string" "type": "string"
}, },
"sanitary_license": { "role": {
"description": "pharmacy, distributor, admin",
"type": "string" "type": "string"
}, },
"updated_at": { "updated_at": {
@ -301,6 +830,9 @@ const docTemplate = `{
"seller_id": { "seller_id": {
"type": "string" "type": "string"
}, },
"shipping": {
"$ref": "#/definitions/domain.ShippingAddress"
},
"status": { "status": {
"$ref": "#/definitions/domain.OrderStatus" "$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": { "domain.Product": {
"type": "object", "type": "object",
"properties": { "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": { "handler.createOrderRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -425,6 +1121,80 @@ const docTemplate = `{
}, },
"seller_id": { "seller_id": {
"type": "string" "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": { "corporate_name": {
"type": "string" "type": "string"
}, },
"role": { "license_number": {
"type": "string" "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" "type": "string"
} }
} }
@ -478,6 +1268,33 @@ const docTemplate = `{
"type": "string" "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"
} }
} }
}` }`

View file

@ -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": { "definitions": {
@ -264,11 +790,14 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"role": { "is_verified": {
"description": "pharmacy, distributor, admin", "type": "boolean"
},
"license_number": {
"type": "string" "type": "string"
}, },
"sanitary_license": { "role": {
"description": "pharmacy, distributor, admin",
"type": "string" "type": "string"
}, },
"updated_at": { "updated_at": {
@ -297,6 +826,9 @@
"seller_id": { "seller_id": {
"type": "string" "type": "string"
}, },
"shipping": {
"$ref": "#/definitions/domain.ShippingAddress"
},
"status": { "status": {
"$ref": "#/definitions/domain.OrderStatus" "$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": { "domain.Product": {
"type": "object", "type": "object",
"properties": { "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": { "handler.createOrderRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -421,6 +1117,80 @@
}, },
"seller_id": { "seller_id": {
"type": "string" "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": { "corporate_name": {
"type": "string" "type": "string"
}, },
"role": { "license_number": {
"type": "string" "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" "type": "string"
} }
} }
@ -474,6 +1264,33 @@
"type": "string" "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"
} }
} }
} }

View file

@ -10,11 +10,13 @@ definitions:
type: string type: string
id: id:
type: string type: string
is_verified:
type: boolean
license_number:
type: string
role: role:
description: pharmacy, distributor, admin description: pharmacy, distributor, admin
type: string type: string
sanitary_license:
type: string
updated_at: updated_at:
type: string type: string
type: object type: object
@ -32,6 +34,8 @@ definitions:
type: array type: array
seller_id: seller_id:
type: string type: string
shipping:
$ref: '#/definitions/domain.ShippingAddress'
status: status:
$ref: '#/definitions/domain.OrderStatus' $ref: '#/definitions/domain.OrderStatus'
total_cents: total_cents:
@ -83,6 +87,36 @@ definitions:
seller_receivable: seller_receivable:
type: integer type: integer
type: object 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: domain.Product:
properties: properties:
batch: batch:
@ -106,6 +140,83 @@ definitions:
updated_at: updated_at:
type: string type: string
type: object 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: handler.createOrderRequest:
properties: properties:
buyer_id: buyer_id:
@ -116,6 +227,54 @@ definitions:
type: array type: array
seller_id: seller_id:
type: string 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 type: object
handler.registerCompanyRequest: handler.registerCompanyRequest:
properties: properties:
@ -123,9 +282,22 @@ definitions:
type: string type: string
corporate_name: corporate_name:
type: string type: string
license_number:
type: string
role: role:
type: string 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: string
type: object type: object
handler.registerProductRequest: handler.registerProductRequest:
@ -150,6 +322,19 @@ definitions:
status: status:
type: string type: string
type: object type: object
handler.updateUserRequest:
properties:
company_id:
type: string
email:
type: string
name:
type: string
password:
type: string
role:
type: string
type: object
info: info:
contact: contact:
email: devops@saveinmed.com email: devops@saveinmed.com
@ -310,6 +495,348 @@ paths:
summary: Cadastro de produto com rastreabilidade de lote summary: Cadastro de produto com rastreabilidade de lote
tags: tags:
- Produtos - 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: schemes:
- http - http
securityDefinitions:
BearerAuth:
in: header
name: Authorization
type: apiKey
swagger: "2.0" swagger: "2.0"

View file

@ -20,6 +20,7 @@ type Config struct {
MarketplaceCommission float64 MarketplaceCommission float64
JWTSecret string JWTSecret string
JWTExpiresIn time.Duration JWTExpiresIn time.Duration
PasswordPepper string
CORSOrigins []string CORSOrigins []string
} }
@ -37,6 +38,7 @@ func Load() Config {
MarketplaceCommission: getEnvFloat("MARKETPLACE_COMMISSION", 2.5), MarketplaceCommission: getEnvFloat("MARKETPLACE_COMMISSION", 2.5),
JWTSecret: getEnv("JWT_SECRET", "dev-secret"), JWTSecret: getEnv("JWT_SECRET", "dev-secret"),
JWTExpiresIn: getEnvDuration("JWT_EXPIRES_IN", 24*time.Hour), JWTExpiresIn: getEnvDuration("JWT_EXPIRES_IN", 24*time.Hour),
PasswordPepper: getEnv("PASSWORD_PEPPER", ""),
CORSOrigins: getEnvStringSlice("CORS_ORIGINS", []string{"*"}), CORSOrigins: getEnvStringSlice("CORS_ORIGINS", []string{"*"}),
} }

View file

@ -27,7 +27,17 @@ func New(svc *usecase.Service) *Handler {
return &Handler{svc: svc} 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) { func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
var req registerAuthRequest var req registerAuthRequest
if err := decodeJSON(r.Context(), r, &req); err != nil { 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}) 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) { func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
var req loginRequest var req loginRequest
if err := decodeJSON(r.Context(), r, &req); err != nil { 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) 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) { func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
requester, err := getRequester(r) requester, err := getRequester(r)
if err != nil { if err != nil {
@ -676,7 +707,18 @@ func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, user) 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) { func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) {
requester, err := getRequester(r) requester, err := getRequester(r)
if err != nil { if err != nil {
@ -713,7 +755,17 @@ func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, pageResult) 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) { func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
requester, err := getRequester(r) requester, err := getRequester(r)
if err != nil { if err != nil {
@ -743,7 +795,20 @@ func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, user) 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) { func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
requester, err := getRequester(r) requester, err := getRequester(r)
if err != nil { if err != nil {
@ -802,7 +867,17 @@ func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, user) 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) { func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
requester, err := getRequester(r) requester, err := getRequester(r)
if err != nil { if err != nil {

View file

@ -37,7 +37,7 @@ func New(cfg config.Config) (*Server, error) {
repo := postgres.New(db) repo := postgres.New(db)
gateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission) 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) h := handler.New(svc)
mux := http.NewServeMux() mux := http.NewServeMux()

View file

@ -61,11 +61,19 @@ type Service struct {
jwtSecret []byte jwtSecret []byte
tokenTTL time.Duration tokenTTL time.Duration
marketplaceCommission float64 marketplaceCommission float64
passwordPepper string
} }
// NewService wires use cases together. // NewService wires use cases together.
func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration) *Service { 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} 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 { 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 { 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 { if err != nil {
return err 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 { func (s *Service) UpdateUser(ctx context.Context, user *domain.User, newPassword string) error {
if newPassword != "" { if newPassword != "" {
hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) hashed, err := bcrypt.GenerateFromPassword([]byte(s.pepperPassword(newPassword)), bcrypt.DefaultCost)
if err != nil { if err != nil {
return err return err
} }
@ -378,7 +386,7 @@ func (s *Service) Authenticate(ctx context.Context, email, password string) (str
return "", time.Time{}, err 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") 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 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. // VerifyCompany marks a company as verified.
func (s *Service) VerifyCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) { func (s *Service) VerifyCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
company, err := s.repo.GetCompany(ctx, id) company, err := s.repo.GetCompany(ctx, id)