Backend: - Implementa API de Endereços (`POST /enderecos`) e migration da tabela `addresses`. - Adiciona bloqueio de login para usuários de empresas não verificadas (status `pending`). - Criação automática do usuário Master (`seedAdmin`) com empresa verificada. - Adiciona aliases de rota em PT-BR (`/api/v1/empresas` GET/PATCH, `/api/v1/usuarios` PATCH) para compatibilidade com o frontend. - Atualiza DTOs para suportar campos em português no registro de empresas e atualização de usuários. - Endpoint `/auth/me` agora retorna `company_name` e flag `superadmin`. - Ajusta filtro de repositório para listar empresas por status de verificação. Frontend: - Nova página `/usuarios-pendentes` com layout padrão e funcionalidade de aprovação. - Atualiza [Header](cci:1://file:///c:/Projetos/saveinmed/saveinmed-frontend/src/components/Header.tsx:29:0-337:2) para exibir o nome da empresa do usuário logado. - Serviço `empresaApiService`: correções de mapeamento (`corporate_name` -> `razao_social`) e novos métodos. - Tipagem atualizada para incluir campos de empresa no [UserData](cci:2://file:///c:/Projetos/saveinmed/saveinmed-frontend/src/types/auth.ts:15:0-30:1). Fixes: - Correção de erro 405 (Method Not Allowed) nas rotas de atualização. - Correção de erro 404 na listagem de pendentes. - Resolução de campos vazios na listagem de empresas.
279 lines
7.1 KiB
Go
279 lines
7.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gofrs/uuid/v5"
|
|
"github.com/saveinmed/backend-go/internal/domain"
|
|
)
|
|
|
|
// 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 {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var req createUserRequest
|
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
if strings.EqualFold(requester.Role, "Seller") {
|
|
if requester.CompanyID == nil {
|
|
writeError(w, http.StatusBadRequest, errors.New("seller must include X-Company-ID header"))
|
|
return
|
|
}
|
|
if req.CompanyID != *requester.CompanyID {
|
|
writeError(w, http.StatusForbidden, errors.New("seller can only manage their own company users"))
|
|
return
|
|
}
|
|
}
|
|
|
|
user := &domain.User{
|
|
CompanyID: req.CompanyID,
|
|
Role: req.Role,
|
|
Name: req.Name,
|
|
Username: req.Username,
|
|
Email: req.Email,
|
|
}
|
|
|
|
if err := h.svc.CreateUser(r.Context(), user, req.Password); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, user)
|
|
}
|
|
|
|
// 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 {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
page, pageSize := parsePagination(r)
|
|
|
|
var companyFilter *uuid.UUID
|
|
if cid := r.URL.Query().Get("company_id"); cid != "" {
|
|
id, err := uuid.FromString(cid)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, errors.New("invalid company_id"))
|
|
return
|
|
}
|
|
companyFilter = &id
|
|
}
|
|
|
|
// Non-admin users can only see users from their own company
|
|
if !strings.EqualFold(requester.Role, "Admin") {
|
|
if requester.CompanyID == nil {
|
|
writeError(w, http.StatusBadRequest, errors.New("user must have a company associated"))
|
|
return
|
|
}
|
|
companyFilter = requester.CompanyID
|
|
}
|
|
|
|
pageResult, err := h.svc.ListUsers(r.Context(), domain.UserFilter{CompanyID: companyFilter}, page, pageSize)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, pageResult)
|
|
}
|
|
|
|
// 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 {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
id, err := parseUUIDFromPath(r.URL.Path)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
user, err := h.svc.GetUser(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
|
|
if strings.EqualFold(requester.Role, "Seller") {
|
|
if requester.CompanyID == nil || user.CompanyID != *requester.CompanyID {
|
|
writeError(w, http.StatusForbidden, errors.New("seller can only view users from their company"))
|
|
return
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
// 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 {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
id, err := parseUUIDFromPath(r.URL.Path)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var req updateUserRequest
|
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
user, err := h.svc.GetUser(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
|
|
if strings.EqualFold(requester.Role, "Seller") {
|
|
if requester.CompanyID == nil || user.CompanyID != *requester.CompanyID {
|
|
writeError(w, http.StatusForbidden, errors.New("seller can only update their company users"))
|
|
return
|
|
}
|
|
}
|
|
|
|
if req.CompanyID != nil {
|
|
user.CompanyID = *req.CompanyID
|
|
}
|
|
// Map frontend's array of company IDs to the single CompanyID
|
|
if len(req.EmpresasDados) > 0 {
|
|
// Use the first company ID from the list
|
|
if id, err := uuid.FromString(req.EmpresasDados[0]); err == nil {
|
|
user.CompanyID = id
|
|
}
|
|
}
|
|
|
|
if req.Role != nil {
|
|
user.Role = *req.Role
|
|
}
|
|
if req.Name != nil {
|
|
user.Name = *req.Name
|
|
}
|
|
if req.Username != nil {
|
|
user.Username = *req.Username
|
|
}
|
|
if req.Email != nil {
|
|
user.Email = *req.Email
|
|
}
|
|
|
|
newPassword := ""
|
|
if req.Password != nil {
|
|
newPassword = *req.Password
|
|
}
|
|
|
|
if err := h.svc.UpdateUser(r.Context(), user, newPassword); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, user)
|
|
}
|
|
|
|
// 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 {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
id, err := parseUUIDFromPath(r.URL.Path)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
user, err := h.svc.GetUser(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
|
|
if strings.EqualFold(requester.Role, "Seller") {
|
|
if requester.CompanyID == nil || user.CompanyID != *requester.CompanyID {
|
|
writeError(w, http.StatusForbidden, errors.New("seller can only delete their company users"))
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := h.svc.DeleteUser(r.Context(), id); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|