416 lines
12 KiB
Go
416 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"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 _ = json.Marshal // dummy to keep import if needed elsewhere
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
// If company_name and cnpj are sent directly (from frontend), create company object
|
|
if company == nil && req.CompanyName != "" && req.CNPJ != "" {
|
|
company = &domain.Company{
|
|
Category: "farmacia",
|
|
CNPJ: req.CNPJ,
|
|
CorporateName: req.CompanyName,
|
|
LicenseNumber: "PENDING",
|
|
IsVerified: false,
|
|
CreatedAt: time.Now().UTC(),
|
|
UpdatedAt: time.Now().UTC(),
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
// Default role to owner if not provided
|
|
if user.Role == "" {
|
|
user.Role = domain.RoleOwner
|
|
}
|
|
user.Role = domain.NormalizeRole(user.Role)
|
|
|
|
// If no company provided at all, 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})
|
|
}
|
|
|
|
// 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
|
|
if user.CompanyID != uuid.Nil {
|
|
if c, err := h.svc.GetCompany(r.Context(), user.CompanyID); err == nil && c != nil {
|
|
companyName = c.CorporateName
|
|
}
|
|
}
|
|
|
|
enderecos := make([]string, 0)
|
|
empresasDados := make([]string, 0)
|
|
if user.CompanyID != uuid.Nil {
|
|
empresasDados = append(empresasDados, user.CompanyID.String())
|
|
}
|
|
|
|
response := struct {
|
|
ID uuid.UUID `json:"id"`
|
|
CompanyID uuid.UUID `json:"company_id"`
|
|
Role string `json:"role"`
|
|
Nivel string `json:"nivel"`
|
|
Name string `json:"name"`
|
|
Nome string `json:"nome"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
EmailVerified bool `json:"email_verified"`
|
|
NomeSocial string `json:"nome-social"`
|
|
CPF string `json:"cpf"`
|
|
RegistroCompleto bool `json:"registro-completo"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CompanyName string `json:"company_name"`
|
|
SuperAdmin bool `json:"superadmin"`
|
|
Enderecos []string `json:"enderecos"`
|
|
EmpresasDados []string `json:"empresasDados"`
|
|
}{
|
|
ID: user.ID,
|
|
CompanyID: user.CompanyID,
|
|
Role: domain.NormalizeRole(user.Role),
|
|
Nivel: domain.NormalizeRole(user.Role),
|
|
Name: user.Name,
|
|
Nome: user.Name,
|
|
Username: user.Username,
|
|
Email: user.Email,
|
|
EmailVerified: user.EmailVerified,
|
|
NomeSocial: user.NomeSocial,
|
|
CPF: user.CPF,
|
|
RegistroCompleto: true,
|
|
CreatedAt: user.CreatedAt,
|
|
UpdatedAt: user.UpdatedAt,
|
|
CompanyName: companyName,
|
|
SuperAdmin: false,
|
|
Enderecos: enderecos,
|
|
EmpresasDados: empresasDados,
|
|
}
|
|
|
|
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 = domain.RoleOwner
|
|
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 = domain.RoleOwner
|
|
h.registerWithPayload(w, r, req)
|
|
}
|
|
|
|
// 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: domain.NormalizeRole(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: domain.NormalizeRole(claims.Role),
|
|
CompanyID: cid,
|
|
}, nil
|
|
}
|