saveinmed/backend-old/internal/http/handler/handler.go
NANDO9322 35f86c8e26 feat: Melhorias na Gestão de Usuários, Correções de Frete e UI
Frontend:
- Refatoração completa do [GestaoUsuarioModal](cci:1://file:///c:/Projetos/saveinmed/saveinmed-frontend/src/components/GestaoUsuarioModal.tsx:17:0-711:2) para melhor visibilidade e UX.
- Correção de erro (crash) ao carregar endereços vazios.
- Nova interface de Configuração de Frete com abas para Entrega e Retirada.
- Correção na busca de dados completos da empresa (CEP, etc).
- Ajuste na chave de autenticação (`access_token`) no serviço de endereços.

Backend:
- Correção do erro 500 em [GetShippingSettings](cci:1://file:///c:/Projetos/saveinmed/backend-old/internal/repository/postgres/postgres.go:1398:0-1405:1) (tratamento de `no rows`).
- Ajustes nos handlers de endereço para suportar Admin/EntityID corretamente.
- Migrações de banco de dados para configurações de envio e coordenadas.
- Atualização da documentação Swagger e testes.
2026-01-27 21:27:03 -03:00

429 lines
12 KiB
Go

package handler
import (
"context"
"database/sql"
"errors"
"net/http"
"strconv"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/gofrs/uuid/v5"
"github.com/saveinmed/backend-go/internal/domain"
"github.com/saveinmed/backend-go/internal/http/middleware"
"github.com/saveinmed/backend-go/internal/usecase"
)
var jsonAPI = jsoniter.ConfigCompatibleWithStandardLibrary
type Handler struct {
svc *usecase.Service
buyerFeeRate float64 // Rate to inflate prices for buyers (e.g., 0.12 = 12%)
}
func New(svc *usecase.Service, buyerFeeRate float64) *Handler {
return &Handler{svc: svc, buyerFeeRate: buyerFeeRate}
}
// 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 {
writeError(w, http.StatusBadRequest, err)
return
}
var company *domain.Company
if req.Company != nil {
company = &domain.Company{
ID: req.Company.ID,
Category: req.Company.Category,
CNPJ: req.Company.CNPJ,
CorporateName: req.Company.CorporateName,
LicenseNumber: req.Company.LicenseNumber,
Latitude: req.Company.Latitude,
Longitude: req.Company.Longitude,
City: req.Company.City,
State: req.Company.State,
}
}
var companyID uuid.UUID
if req.CompanyID != nil {
companyID = *req.CompanyID
}
user := &domain.User{
CompanyID: companyID,
Role: req.Role,
Name: req.Name,
Username: req.Username,
Email: req.Email,
}
// If no company provided, create a placeholder one to satisfy DB constraints
if user.CompanyID == uuid.Nil && company == nil {
timestamp := time.Now().UnixNano()
company = &domain.Company{
// ID left as Nil so usecase creates it
Category: "farmacia",
CNPJ: "TMP-" + strconv.FormatInt(timestamp, 10), // Temporary CNPJ
CorporateName: "Empresa de " + req.Name,
LicenseNumber: "PENDING",
IsVerified: false,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
}
if err := h.svc.RegisterAccount(r.Context(), company, user, req.Password); err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
token, exp, err := h.svc.Authenticate(r.Context(), user.Username, req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusCreated, authResponse{Token: token, ExpiresAt: exp})
}
// Login godoc
// @Summary Autenticação de usuário
// @Description Realiza login e retorna token JWT.
// @Description **Credenciais Padrão (Master):**
// @Description Email: `andre.fr93@gmail.com`
// @Description Senha: `teste1234`
// @Tags Autenticação
// @Accept json
// @Produce json
// @Param login body loginRequest true "Credenciais"
// @Success 200 {object} authResponse
// @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 {
writeError(w, http.StatusBadRequest, err)
return
}
identifier := req.Email
if identifier == "" {
identifier = req.Username
}
token, exp, err := h.svc.Authenticate(r.Context(), identifier, req.Password)
if err != nil {
writeError(w, http.StatusUnauthorized, err)
return
}
writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp})
}
// GetMe godoc
// @Summary Obter dados do usuário logado
// @Tags Autenticação
// @Security BearerAuth
// @Produce json
// @Success 200 {object} domain.User
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/me [get]
func (h *Handler) GetMe(w http.ResponseWriter, r *http.Request) {
requester, err := getRequester(r)
if err != nil {
writeError(w, http.StatusUnauthorized, err)
return
}
user, err := h.svc.GetUser(r.Context(), requester.ID)
if err != nil {
writeError(w, http.StatusNotFound, err)
return
}
var companyName string
var isSuperAdmin bool
if user.CompanyID != uuid.Nil {
if c, err := h.svc.GetCompany(r.Context(), user.CompanyID); err == nil && c != nil {
companyName = c.CorporateName
if c.Category == "admin" {
isSuperAdmin = true
}
}
}
response := struct {
*domain.User
CompanyName string `json:"company_name"`
SuperAdmin bool `json:"superadmin"`
EmpresasDados []string `json:"empresasDados"` // Frontend expects this array
}{
User: user,
CompanyName: companyName,
SuperAdmin: isSuperAdmin,
EmpresasDados: []string{user.CompanyID.String()},
}
writeJSON(w, http.StatusOK, response)
}
// RegisterCustomer godoc
// @Summary Cadastro de cliente
// @Description Cria um usuário do tipo cliente 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/customer [post]
func (h *Handler) RegisterCustomer(w http.ResponseWriter, r *http.Request) {
var req registerAuthRequest
if err := decodeJSON(r.Context(), r, &req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
req.Role = "Customer"
h.registerWithPayload(w, r, req)
}
// RegisterTenant godoc
// @Summary Cadastro de tenant
// @Description Cria um usuário do tipo tenant 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/tenant [post]
func (h *Handler) RegisterTenant(w http.ResponseWriter, r *http.Request) {
var req registerAuthRequest
if err := decodeJSON(r.Context(), r, &req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
req.Role = "Seller"
h.registerWithPayload(w, r, req)
}
// RefreshToken godoc
// @Summary Atualizar token
// @Description Gera um novo JWT a partir de um token válido.
// @Tags Autenticação
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Success 200 {object} authResponse
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/refresh-token [post]
func (h *Handler) RefreshToken(w http.ResponseWriter, r *http.Request) {
tokenStr, err := parseBearerToken(r)
if err != nil {
writeError(w, http.StatusUnauthorized, err)
return
}
token, exp, err := h.svc.RefreshToken(r.Context(), tokenStr)
if err != nil {
writeError(w, http.StatusUnauthorized, err)
return
}
writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp})
}
// Logout godoc
// @Summary Logout
// @Description Endpoint para logout (invalidação client-side).
// @Tags Autenticação
// @Success 204 {string} string "No Content"
// @Router /api/v1/auth/logout [post]
func (h *Handler) Logout(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// ForgotPassword godoc
// @Summary Solicitar redefinição de senha
// @Description Gera um token de redefinição de senha.
// @Tags Autenticação
// @Accept json
// @Produce json
// @Param payload body forgotPasswordRequest true "Email do usuário"
// @Success 202 {object} resetTokenResponse
// @Failure 400 {object} map[string]string
// @Router /api/v1/auth/password/forgot [post]
func (h *Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
var req forgotPasswordRequest
if err := decodeJSON(r.Context(), r, &req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
if req.Email == "" {
writeError(w, http.StatusBadRequest, errors.New("email is required"))
return
}
token, exp, err := h.svc.CreatePasswordResetToken(r.Context(), req.Email)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
writeJSON(w, http.StatusAccepted, resetTokenResponse{
Message: "Se existir uma conta, enviaremos instruções de redefinição.",
})
return
}
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusAccepted, resetTokenResponse{
Message: "Token de redefinição gerado.",
ResetToken: token,
ExpiresAt: &exp,
})
}
// ResetPassword godoc
// @Summary Redefinir senha
// @Description Atualiza a senha usando o token de redefinição.
// @Tags Autenticação
// @Accept json
// @Produce json
// @Param payload body resetPasswordRequest true "Token e nova senha"
// @Success 200 {object} messageResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/password/reset [post]
func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
var req resetPasswordRequest
if err := decodeJSON(r.Context(), r, &req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
if req.Token == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, errors.New("token and password are required"))
return
}
if err := h.svc.ResetPassword(r.Context(), req.Token, req.Password); err != nil {
writeError(w, http.StatusUnauthorized, err)
return
}
writeJSON(w, http.StatusOK, messageResponse{Message: "Senha atualizada com sucesso."})
}
// VerifyEmail godoc
// @Summary Verificar email
// @Description Marca o email como verificado usando um token JWT.
// @Tags Autenticação
// @Accept json
// @Produce json
// @Param payload body verifyEmailRequest true "Token de verificação"
// @Success 200 {object} messageResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/verify-email [post]
func (h *Handler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
var req verifyEmailRequest
if err := decodeJSON(r.Context(), r, &req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
if req.Token == "" {
writeError(w, http.StatusBadRequest, errors.New("token is required"))
return
}
if _, err := h.svc.VerifyEmail(r.Context(), req.Token); err != nil {
writeError(w, http.StatusUnauthorized, err)
return
}
writeJSON(w, http.StatusOK, messageResponse{Message: "E-mail verificado com sucesso."})
}
func (h *Handler) registerWithPayload(w http.ResponseWriter, r *http.Request, req registerAuthRequest) {
var company *domain.Company
if req.Company != nil {
company = &domain.Company{
ID: req.Company.ID,
Category: req.Company.Category,
CNPJ: req.Company.CNPJ,
CorporateName: req.Company.CorporateName,
LicenseNumber: req.Company.LicenseNumber,
Latitude: req.Company.Latitude,
Longitude: req.Company.Longitude,
City: req.Company.City,
State: req.Company.State,
}
}
var companyID uuid.UUID
if req.CompanyID != nil {
companyID = *req.CompanyID
}
user := &domain.User{
CompanyID: companyID,
Role: req.Role,
Name: req.Name,
Email: req.Email,
}
if user.CompanyID == uuid.Nil && company == nil {
writeError(w, http.StatusBadRequest, errors.New("company_id or company payload is required"))
return
}
if err := h.svc.RegisterAccount(r.Context(), company, user, req.Password); err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
token, exp, err := h.svc.Authenticate(r.Context(), user.Email, req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusCreated, authResponse{Token: token, ExpiresAt: exp})
}
func (h *Handler) getUserFromContext(ctx context.Context) (*domain.User, error) {
claims, ok := middleware.GetClaims(ctx)
if !ok {
return nil, errors.New("unauthorized")
}
var cid uuid.UUID
if claims.CompanyID != nil {
cid = *claims.CompanyID
}
return &domain.User{
ID: claims.UserID,
Role: claims.Role,
CompanyID: cid,
}, nil
}