diff --git a/backend/cmd/reset_password/main.go b/backend/cmd/reset_password/main.go index 809d915..c1e32e5 100644 --- a/backend/cmd/reset_password/main.go +++ b/backend/cmd/reset_password/main.go @@ -31,7 +31,7 @@ func main() { // Nova senha: senha123 newPassword := "senha123" - + // Hash da senha com bcrypt hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword+cfg.PasswordPepper), bcrypt.DefaultCost) if err != nil { diff --git a/backend/internal/http/handler/dto.go b/backend/internal/http/handler/dto.go index 345e4c6..000fd6f 100644 --- a/backend/internal/http/handler/dto.go +++ b/backend/internal/http/handler/dto.go @@ -29,13 +29,15 @@ type createUserRequest struct { } type registerAuthRequest struct { - CompanyID *uuid.UUID `json:"company_id,omitempty"` - Company *registerCompanyTarget `json:"company,omitempty"` - Role string `json:"role"` - Name string `json:"name"` - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` + CompanyID *uuid.UUID `json:"company_id,omitempty"` + Company *registerCompanyTarget `json:"company,omitempty"` + Role string `json:"role"` + Name string `json:"name"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + CompanyName string `json:"company_name,omitempty"` // Frontend sends this + CNPJ string `json:"cnpj,omitempty"` // Frontend sends this } type registerCompanyTarget struct { diff --git a/backend/internal/http/handler/handler.go b/backend/internal/http/handler/handler.go index 8f256dc..b53addb 100644 --- a/backend/internal/http/handler/handler.go +++ b/backend/internal/http/handler/handler.go @@ -60,6 +60,19 @@ func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { } } + // 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 @@ -73,7 +86,12 @@ func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { Email: req.Email, } - // If no company provided, create a placeholder one to satisfy DB constraints + // Default role to Seller if not provided + if user.Role == "" { + user.Role = "Seller" + } + + // 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{ diff --git a/backend/internal/http/handler/product_handler.go b/backend/internal/http/handler/product_handler.go index 66de026..84018e7 100644 --- a/backend/internal/http/handler/product_handler.go +++ b/backend/internal/http/handler/product_handler.go @@ -2,6 +2,7 @@ package handler import ( "errors" + "log" "net/http" "strconv" "time" @@ -33,9 +34,13 @@ func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) { return } + // Debug logging + log.Printf("🔍 [CreateProduct] Role: %s, CompanyID: %v", claims.Role, claims.CompanyID) + // If not Admin/Superadmin, force SellerID to be their CompanyID if claims.Role != "Admin" && claims.Role != "superadmin" { if claims.CompanyID == nil { + log.Printf("❌ [CreateProduct] CompanyID is nil for user %s with role %s", claims.UserID, claims.Role) writeError(w, http.StatusForbidden, errors.New("user has no company")) return } diff --git a/backend/internal/http/middleware/auth.go b/backend/internal/http/middleware/auth.go index ad1c61f..6aec586 100644 --- a/backend/internal/http/middleware/auth.go +++ b/backend/internal/http/middleware/auth.go @@ -3,6 +3,7 @@ package middleware import ( "context" "errors" + "log" "net/http" "strings" @@ -27,15 +28,21 @@ func RequireAuth(secret []byte, allowedRoles ...string) func(http.Handler) http. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { claims, err := parseToken(r, secret) if err != nil { + log.Printf("❌ [RequireAuth] Token parse error: %v", err) w.WriteHeader(http.StatusUnauthorized) return } + log.Printf("🔍 [RequireAuth] User Role: %s, Allowed Roles: %v", claims.Role, allowedRoles) + if len(allowedRoles) > 0 && !isRoleAllowed(claims.Role, allowedRoles) { + log.Printf("❌ [RequireAuth] Role %s not in allowed roles %v", claims.Role, allowedRoles) w.WriteHeader(http.StatusForbidden) return } + log.Printf("✅ [RequireAuth] Access granted for role: %s", claims.Role) + ctx := context.WithValue(r.Context(), claimsKey, *claims) ctx = context.WithValue(ctx, "company_id", claims.CompanyID) next.ServeHTTP(w, r.WithContext(ctx)) diff --git a/backend/internal/repository/postgres/postgres.go b/backend/internal/repository/postgres/postgres.go index eaadbc2..27e49ee 100644 --- a/backend/internal/repository/postgres/postgres.go +++ b/backend/internal/repository/postgres/postgres.go @@ -393,11 +393,16 @@ func (r *Repository) SearchProducts(ctx context.Context, filter domain.ProductSe // Calculate distance and build response results := make([]domain.ProductWithDistance, 0, len(rows)) for _, row := range rows { - dist := domain.HaversineDistance(filter.BuyerLat, filter.BuyerLng, row.Latitude, row.Longitude) - - // Filter by max distance if specified - if filter.MaxDistanceKm != nil && dist > *filter.MaxDistanceKm { - continue + var dist float64 + // If company has no location set (0,0), always include and show distance 0 + if row.Latitude == 0 && row.Longitude == 0 { + dist = 0 + } else { + dist = domain.HaversineDistance(filter.BuyerLat, filter.BuyerLng, row.Latitude, row.Longitude) + // Filter by max distance only for companies with valid coordinates + if filter.MaxDistanceKm != nil && dist > *filter.MaxDistanceKm { + continue + } } results = append(results, domain.ProductWithDistance{ diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index 1eefa06..e779611 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -66,8 +66,8 @@ func New(cfg config.Config) (*Server, error) { auth := middleware.RequireAuth([]byte(cfg.JWTSecret)) adminOnly := middleware.RequireAuth([]byte(cfg.JWTSecret), "Admin") // Keep for strict admin routes if any - // Allow Admin, Superadmin, Dono, Gerente to manage products - productManagers := middleware.RequireAuth([]byte(cfg.JWTSecret), "Admin", "superadmin", "Dono", "Gerente") + // Allow Admin, Superadmin, Dono, Gerente, and Seller to manage products + productManagers := middleware.RequireAuth([]byte(cfg.JWTSecret), "Admin", "superadmin", "Dono", "Gerente", "Seller") // Companies (Empresas) mux.Handle("POST /api/v1/companies", chain(http.HandlerFunc(h.CreateCompany), middleware.Logger, middleware.Gzip)) diff --git a/frontend/src/pages/auth/Register.tsx b/frontend/src/pages/auth/Register.tsx index 2f16da7..1d9d82e 100644 --- a/frontend/src/pages/auth/Register.tsx +++ b/frontend/src/pages/auth/Register.tsx @@ -1,8 +1,10 @@ import { FormEvent, useState } from 'react' import { useNavigate, Link } from 'react-router-dom' import axios from 'axios' +import { useAuth, UserRole } from '@/context/AuthContext' import { authService } from '@/services/auth' import { formatCNPJ, validateCNPJ } from '@/utils/cnpj' +import { decodeJwtPayload } from '@/utils/jwt' import logoImg from '@/assets/logo.png' interface RegistrationFormData { @@ -17,6 +19,7 @@ interface RegistrationFormData { export function RegisterPage() { const navigate = useNavigate() + const { login } = useAuth() const [formData, setFormData] = useState({ email: '', username: '', @@ -87,6 +90,17 @@ export function RegisterPage() { return Object.keys(errors).length === 0 } + const resolveRole = (role?: string): UserRole => { + switch (role?.toLowerCase()) { + case 'admin': return 'admin' + case 'dono': return 'owner' + case 'colaborador': return 'employee' + case 'entregador': return 'delivery' + case 'customer': return 'customer' + case 'seller': default: return 'seller' + } + } + const onSubmit = async (event: FormEvent) => { event.preventDefault() @@ -99,7 +113,7 @@ export function RegisterPage() { setErrorMessage(null) try { - await authService.register({ + const response = await authService.register({ email: formData.email, username: formData.username, password: formData.password, @@ -108,8 +122,15 @@ export function RegisterPage() { cnpj: formData.cnpj.replace(/\D/g, ''), }) - // Show success and redirect to login - navigate('/login?registered=true') + // Auto login after successful registration + const { token } = response + if (!token) throw new Error('Resposta de registro inválida.') + + const payload = decodeJwtPayload<{ role?: string, sub: string, company_id?: string }>(token) + const role = resolveRole(payload?.role) + + // Login automatically and redirect to dashboard + login(token, role, formData.name, payload?.sub || '', payload?.company_id, formData.email, formData.username) } catch (error) { const fallback = 'Não foi possível criar a conta. Tente novamente.' if (axios.isAxiosError(error)) { diff --git a/frontend/src/pages/dashboard/seller/Inventory.tsx b/frontend/src/pages/dashboard/seller/Inventory.tsx index 426cd91..f457e6f 100644 --- a/frontend/src/pages/dashboard/seller/Inventory.tsx +++ b/frontend/src/pages/dashboard/seller/Inventory.tsx @@ -8,11 +8,12 @@ import { formatCents } from '@/utils/format' interface InventoryItem { product_id: string seller_id: string - name: string + nome: string + ean_code: string batch: string expires_at: string - quantity: number - price_cents: number + stock_quantity: number + sale_price_cents: number } export function InventoryPage() { @@ -153,27 +154,27 @@ export function InventoryPage() { - - - - + + + + {inventory.map((item) => ( - - + + + + - -
ProdutoLoteValidadeQuantidadeNomeEAN PreçoEstoqueValidade Ações
{item.name}{item.batch}{item.nome || '—'}{item.ean_code || '—'} + {formatCents(item.sale_price_cents)} + {item.stock_quantity} {new Date(item.expires_at).toLocaleDateString('pt-BR')} {isExpiringSoon(item.expires_at) && ⚠️} {item.quantity} - {formatCents(item.price_cents)} -