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.
429 lines
12 KiB
Go
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
|
|
}
|