saveinmed/backend-old/internal/server/server.go
NANDO9322 0a0c344022 feat(geral): implementa fluxo de aprovação, api de endereços e acesso master
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.
2026-01-21 17:20:06 -03:00

268 lines
15 KiB
Go

package server
import (
"context"
"log"
"net/http"
"time"
"github.com/gofrs/uuid/v5"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
httpSwagger "github.com/swaggo/http-swagger"
"github.com/saveinmed/backend-go/internal/config"
"github.com/saveinmed/backend-go/internal/domain"
"github.com/saveinmed/backend-go/internal/http/handler"
"github.com/saveinmed/backend-go/internal/http/middleware"
"github.com/saveinmed/backend-go/internal/notifications"
"github.com/saveinmed/backend-go/internal/payments"
"github.com/saveinmed/backend-go/internal/repository/postgres"
"github.com/saveinmed/backend-go/internal/usecase"
)
// Server wires the infrastructure and exposes HTTP handlers.
type Server struct {
cfg config.Config
db *sqlx.DB
mux *http.ServeMux
svc *usecase.Service
}
func New(cfg config.Config) (*Server, error) {
db, err := sqlx.Open("pgx", cfg.DatabaseURL)
if err != nil {
return nil, err
}
repoInstance := postgres.New(db)
paymentGateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission)
// Services
notifySvc := notifications.NewLoggerNotificationService()
svc := usecase.NewService(repoInstance, paymentGateway, notifySvc, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper)
h := handler.New(svc, cfg.BuyerFeeRate)
mux := http.NewServeMux()
// Root endpoint
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := `{"message":"💊 SaveInMed API is running!","docs":"/docs","health":"/health","version":"1.0.0"}`
_, _ = w.Write([]byte(response))
})
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
auth := middleware.RequireAuth([]byte(cfg.JWTSecret))
adminOnly := middleware.RequireAuth([]byte(cfg.JWTSecret), "Admin")
// Companies (Empresas)
mux.Handle("POST /api/v1/companies", chain(http.HandlerFunc(h.CreateCompany), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/empresas", chain(http.HandlerFunc(h.CreateCompany), middleware.Logger, middleware.Gzip)) // Alias for frontend
mux.Handle("GET /api/v1/companies", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip))
mux.Handle("GET /api/v1/empresas", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip)) // Alias for frontend
mux.Handle("GET /api/v1/companies/{id}", chain(http.HandlerFunc(h.GetCompany), middleware.Logger, middleware.Gzip))
mux.Handle("PATCH /api/v1/companies/{id}", chain(http.HandlerFunc(h.UpdateCompany), middleware.Logger, middleware.Gzip))
mux.Handle("PATCH /api/v1/empresas/{id}", chain(http.HandlerFunc(h.UpdateCompany), middleware.Logger, middleware.Gzip)) // Alias for frontend
mux.Handle("DELETE /api/v1/companies/{id}", chain(http.HandlerFunc(h.DeleteCompany), middleware.Logger, middleware.Gzip))
mux.Handle("PATCH /api/v1/companies/{id}/verify", chain(http.HandlerFunc(h.VerifyCompany), middleware.Logger, middleware.Gzip, adminOnly))
mux.Handle("GET /api/v1/companies/me", chain(http.HandlerFunc(h.GetMyCompany), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/companies/{id}/rating", chain(http.HandlerFunc(h.GetCompanyRating), middleware.Logger, middleware.Gzip))
// KYC
mux.Handle("POST /api/v1/companies/documents", chain(http.HandlerFunc(h.UploadDocument), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/companies/documents", chain(http.HandlerFunc(h.GetDocuments), middleware.Logger, middleware.Gzip, auth))
// Financials
mux.Handle("GET /api/v1/finance/ledger", chain(http.HandlerFunc(h.GetLedger), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/finance/balance", chain(http.HandlerFunc(h.GetBalance), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/finance/withdrawals", chain(http.HandlerFunc(h.RequestWithdrawal), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/finance/withdrawals", chain(http.HandlerFunc(h.ListWithdrawals), middleware.Logger, middleware.Gzip, auth))
// Team
mux.Handle("GET /api/v1/team", chain(http.HandlerFunc(h.ListTeam), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/team", chain(http.HandlerFunc(h.InviteMember), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/products", chain(http.HandlerFunc(h.CreateProduct), middleware.Logger, middleware.Gzip))
mux.Handle("GET /api/v1/products", chain(http.HandlerFunc(h.ListProducts), middleware.Logger, middleware.Gzip))
mux.Handle("GET /api/v1/products/search", chain(http.HandlerFunc(h.SearchProducts), middleware.Logger, middleware.Gzip, middleware.OptionalAuth([]byte(cfg.JWTSecret))))
mux.Handle("GET /api/v1/products/{id}", chain(http.HandlerFunc(h.GetProduct), middleware.Logger, middleware.Gzip))
mux.Handle("GET /api/v1/marketplace/records", chain(http.HandlerFunc(h.ListMarketplaceRecords), middleware.Logger, middleware.Gzip))
mux.Handle("PATCH /api/v1/products/{id}", chain(http.HandlerFunc(h.UpdateProduct), middleware.Logger, middleware.Gzip))
mux.Handle("DELETE /api/v1/products/{id}", chain(http.HandlerFunc(h.DeleteProduct), middleware.Logger, middleware.Gzip))
mux.Handle("GET /api/v1/inventory", chain(http.HandlerFunc(h.ListInventory), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/inventory/adjust", chain(http.HandlerFunc(h.AdjustInventory), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/orders", chain(http.HandlerFunc(h.CreateOrder), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/orders", chain(http.HandlerFunc(h.ListOrders), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/orders/{id}", chain(http.HandlerFunc(h.GetOrder), middleware.Logger, middleware.Gzip, auth))
mux.Handle("PATCH /api/v1/orders/{id}/status", chain(http.HandlerFunc(h.UpdateOrderStatus), middleware.Logger, middleware.Gzip, auth))
mux.Handle("DELETE /api/v1/orders/{id}", chain(http.HandlerFunc(h.DeleteOrder), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/orders/{id}/payment", chain(http.HandlerFunc(h.CreatePaymentPreference), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/shipments", chain(http.HandlerFunc(h.CreateShipment), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/shipments", chain(http.HandlerFunc(h.ListShipments), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/shipments/", chain(http.HandlerFunc(h.GetShipmentByOrderID), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/payments/webhook", chain(http.HandlerFunc(h.HandlePaymentWebhook), middleware.Logger, middleware.Gzip)) // Generic/Mercado Pago
mux.Handle("POST /api/v1/payments/webhook/stripe", chain(http.HandlerFunc(h.HandleStripeWebhook), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/payments/webhook/asaas", chain(http.HandlerFunc(h.HandleAsaasWebhook), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/reviews", chain(http.HandlerFunc(h.CreateReview), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/reviews", chain(http.HandlerFunc(h.ListReviews), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/dashboard", chain(http.HandlerFunc(h.GetDashboard), middleware.Logger, middleware.Gzip, auth))
// Payment Config (Admin)
mux.Handle("GET /api/v1/admin/payment-gateways/{provider}", chain(http.HandlerFunc(h.GetPaymentGatewayConfig), middleware.Logger, middleware.Gzip, adminOnly))
mux.Handle("PUT /api/v1/admin/payment-gateways/{provider}", chain(http.HandlerFunc(h.UpdatePaymentGatewayConfig), middleware.Logger, middleware.Gzip, adminOnly))
mux.Handle("POST /api/v1/admin/payment-gateways/{provider}/test", chain(http.HandlerFunc(h.TestPaymentGateway), middleware.Logger, middleware.Gzip, adminOnly))
// Payment Config (Seller)
mux.Handle("GET /api/v1/sellers/{id}/payment-config", chain(http.HandlerFunc(h.GetSellerPaymentConfig), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/sellers/{id}/onboarding", chain(http.HandlerFunc(h.OnboardSeller), middleware.Logger, middleware.Gzip, auth))
// Credit Lines (Boleto a Prazo)
mux.Handle("POST /api/v1/companies/{company_id}/credit/check", chain(http.HandlerFunc(h.CheckCreditLine), middleware.Logger, middleware.Gzip, auth))
mux.Handle("PUT /api/v1/companies/{company_id}/credit/limit", chain(http.HandlerFunc(h.SetCreditLimit), middleware.Logger, middleware.Gzip, adminOnly))
mux.Handle("POST /api/v1/auth/register", chain(http.HandlerFunc(h.Register), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/register/customer", chain(http.HandlerFunc(h.RegisterCustomer), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/register/tenant", chain(http.HandlerFunc(h.RegisterTenant), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/login", chain(http.HandlerFunc(h.Login), middleware.Logger, middleware.Gzip))
mux.Handle("GET /api/v1/auth/me", chain(http.HandlerFunc(h.GetMe), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/auth/logout", chain(http.HandlerFunc(h.Logout), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/password/forgot", chain(http.HandlerFunc(h.ForgotPassword), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/password/reset", chain(http.HandlerFunc(h.ResetPassword), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/refresh-token", chain(http.HandlerFunc(h.RefreshToken), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/verify-email", chain(http.HandlerFunc(h.VerifyEmail), middleware.Logger, middleware.Gzip))
// Address
mux.Handle("POST /api/v1/enderecos", chain(http.HandlerFunc(h.CreateAddress), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/push/register", chain(http.HandlerFunc(h.RegisterPushToken), middleware.Logger, middleware.Gzip, auth))
mux.Handle("DELETE /api/v1/push/unregister", chain(http.HandlerFunc(h.UnregisterPushToken), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/push/test", chain(http.HandlerFunc(h.TestPushNotification), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/users", chain(http.HandlerFunc(h.CreateUser), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/users", chain(http.HandlerFunc(h.ListUsers), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/users/", chain(http.HandlerFunc(h.GetUser), middleware.Logger, middleware.Gzip, auth))
mux.Handle("PUT /api/v1/users/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth))
mux.Handle("PATCH /api/v1/users/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth)) // Add PATCH support
mux.Handle("PATCH /api/v1/usuarios/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth)) // Alias for frontend
mux.Handle("DELETE /api/v1/users/", chain(http.HandlerFunc(h.DeleteUser), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/cart", chain(http.HandlerFunc(h.AddToCart), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/cart", chain(http.HandlerFunc(h.GetCart), middleware.Logger, middleware.Gzip, auth))
mux.Handle("DELETE /api/v1/cart/", chain(http.HandlerFunc(h.DeleteCartItem), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/shipping/settings/{vendor_id}", chain(http.HandlerFunc(h.GetShippingSettings), middleware.Logger, middleware.Gzip, auth))
mux.Handle("PUT /api/v1/shipping/settings/{vendor_id}", chain(http.HandlerFunc(h.UpsertShippingSettings), middleware.Logger, middleware.Gzip, auth))
mux.Handle("POST /api/v1/shipping/calculate", chain(http.HandlerFunc(h.CalculateShipping), middleware.Logger, middleware.Gzip))
mux.Handle("GET /docs/", httpSwagger.Handler(httpSwagger.URL("/docs/doc.json")))
return &Server{cfg: cfg, db: db, mux: mux, svc: svc}, nil
}
// Start runs the HTTP server and ensures the database is reachable.
func (s *Server) Start(ctx context.Context) error {
if err := s.db.PingContext(ctx); err != nil {
return err
}
repo := postgres.New(s.db)
if err := repo.ApplyMigrations(ctx); err != nil {
return err
}
// Seed Admin
if s.cfg.AdminEmail != "" && s.cfg.AdminPassword != "" {
// Checks if admin already exists
_, err := repo.GetUserByEmail(ctx, s.cfg.AdminEmail)
if err != nil {
// If not found, create
log.Printf("Seeding admin user: %s", s.cfg.AdminEmail)
// 1. Create/Get Admin Company
adminCNPJ := "00000000000000"
company := &domain.Company{
ID: uuid.Nil,
CNPJ: adminCNPJ,
CorporateName: "SaveInMed Admin",
Category: "admin",
LicenseNumber: "ADMIN",
IsVerified: true,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
// We need to check if company exists by CNPJ normally, but repo doesn't expose GetByCNPJ easily?
// Let's rely on RegisterAccount handling it or check if we can query.
// Actually RegisterAccount in Service handles creation if ID is Nil, but keys off ID.
// We can try to create and ignore conflict, or use a known ID?
// Let's use RegisterAccount logic.
// Because RegisterAccount expects us to pass a company, and tries to Get by ID if ID is set, or Create if not.
// But duplicate CNPJ will fail at DB level.
// Let's assume on fresh boot it doesn't exist.
// Or better: Use svc.RegisterAccount. But wait, svc.RegisterAccount logic:
/*
if company != nil {
if company.ID == uuid.Nil {
// create
} else {
// get
}
}
*/
// If we re-run, GetUserByEmail would have found the user, so we skip.
// The only edge case is if User was deleted but Company remains.
// In that case, CreateCompany will fail on CNPJ constraint.
err := s.svc.RegisterAccount(ctx, company, &domain.User{
Role: "Admin",
Name: s.cfg.AdminName,
Username: s.cfg.AdminUsername,
Email: s.cfg.AdminEmail,
}, s.cfg.AdminPassword)
if err != nil {
// If error is duplicate key on company, maybe we should fetch the company and try creating user only?
// For now, let's log error but not fail startup hard, or fail hard to signal issue.
log.Printf("Failed to seed admin: %v", err)
} else {
// FORCE VERIFY the admin company
if _, err := s.svc.VerifyCompany(ctx, company.ID); err != nil {
log.Printf("Failed to verify admin company: %v", err)
}
log.Printf("Admin user created successfully")
}
} else {
log.Printf("Admin user %s already exists", s.cfg.AdminEmail)
}
}
corsConfig := middleware.CORSConfig{AllowedOrigins: s.cfg.CORSOrigins}
srv := &http.Server{
Addr: s.cfg.Addr(),
Handler: middleware.SecurityHeaders(middleware.CORSWithConfig(corsConfig)(s.mux)),
ReadHeaderTimeout: 5 * time.Second,
}
log.Printf("starting %s on %s", s.cfg.AppName, s.cfg.Addr())
return srv.ListenAndServe()
}
func chain(h http.Handler, middlewareFns ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewareFns) - 1; i >= 0; i-- {
h = middlewareFns[i](h)
}
return h
}