fix: correcoes de acesso e marketplace
- Corrige algoritmo de validacao CNPJ (pesos completos 12/13 digitos) - Auto-login apos cadastro de usuario redirecionando para /seller - Registro: role padrao Seller quando campo vazio, mapeamento company_name/cnpj - Adiciona role Seller ao middleware productManagers (fix 403 em criacao de produto) - Inventario: usa campos corretos da API (nome, ean_code, sale_price_cents, stock_quantity) - Marketplace: raio padrao nacional (5000km), empresas sem coordenadas sempre visiveis - dto.go: adiciona CompanyName e CNPJ ao registerAuthRequest
This commit is contained in:
parent
c1c1dd83a5
commit
eb9dc8be1d
11 changed files with 108 additions and 48 deletions
|
|
@ -36,6 +36,8 @@ type registerAuthRequest struct {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -393,12 +393,17 @@ 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
|
||||
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{
|
||||
Product: row.Product,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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<RegistrationFormData>({
|
||||
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)) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="text-white" style={{ backgroundColor: '#0F4C81' }}>
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase text-white">Produto</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase text-white">Lote</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase text-white">Validade</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase text-white">Quantidade</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase text-white">Nome</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase text-white">EAN</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase text-white">Preço</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase text-white">Estoque</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase text-white">Validade</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-semibold uppercase text-white">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{inventory.map((item) => (
|
||||
<tr key={`${item.product_id}-${item.batch}`} className={isExpiringSoon(item.expires_at) ? 'bg-yellow-50' : ''}>
|
||||
<td className="px-4 py-3 text-sm font-medium text-gray-800">{item.name}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{item.batch}</td>
|
||||
<td className="px-4 py-3 text-sm font-medium text-gray-800">{item.nome || '—'}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{item.ean_code || '—'}</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-medicalBlue">
|
||||
{formatCents(item.sale_price_cents)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-800">{item.stock_quantity}</td>
|
||||
<td className={`px-4 py-3 text-sm ${isExpiringSoon(item.expires_at) ? 'font-semibold text-orange-600' : 'text-gray-600'}`}>
|
||||
{new Date(item.expires_at).toLocaleDateString('pt-BR')}
|
||||
{isExpiringSoon(item.expires_at) && <span className="ml-2 text-xs">⚠️</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-semibold text-gray-800">{item.quantity}</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-medicalBlue">
|
||||
{formatCents(item.price_cents)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<div className="flex justify-center gap-2">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const ProductSearch = () => {
|
|||
const [lat, setLat] = useState<number>(-16.3285)
|
||||
const [lng, setLng] = useState<number>(-48.9534)
|
||||
const [search, setSearch] = useState('')
|
||||
const [maxDistance, setMaxDistance] = useState<number>(10)
|
||||
const [maxDistance, setMaxDistance] = useState<number>(5000)
|
||||
const [products, setProducts] = useState<ProductWithDistance[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [total, setTotal] = useState(0)
|
||||
|
|
@ -134,7 +134,7 @@ const ProductSearch = () => {
|
|||
setMinPrice('')
|
||||
setMaxPrice('')
|
||||
setMinExpiryDays(0)
|
||||
setMaxDistance(10)
|
||||
setMaxDistance(5000)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -160,12 +160,13 @@ const ProductSearch = () => {
|
|||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Raio de busca: <span className="text-primary font-bold">{maxDistance} km</span>
|
||||
Raio de busca: <span className="text-primary font-bold">{maxDistance >= 5000 ? 'Nacional' : `${maxDistance} km`}</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="50"
|
||||
max="5000"
|
||||
step="10"
|
||||
value={maxDistance}
|
||||
onChange={(e) => setMaxDistance(Number(e.target.value))}
|
||||
className="w-full mt-2"
|
||||
|
|
|
|||
|
|
@ -16,31 +16,31 @@ export function validateCNPJ(cnpj: string): boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
// Validate first check digit
|
||||
// Validate first check digit (position 12)
|
||||
// Weights: 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2
|
||||
const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
|
||||
let sum = 0
|
||||
let position = 5
|
||||
for (let i = 0; i < 8; i++) {
|
||||
sum += parseInt(cleaned[i]) * position
|
||||
position -= 1
|
||||
for (let i = 0; i < 12; i++) {
|
||||
sum += parseInt(cleaned[i]) * weights1[i]
|
||||
}
|
||||
let remainder = sum % 11
|
||||
const firstDigit = remainder < 2 ? 0 : 11 - remainder
|
||||
|
||||
if (parseInt(cleaned[8]) !== firstDigit) {
|
||||
if (parseInt(cleaned[12]) !== firstDigit) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate second check digit
|
||||
// Validate second check digit (position 13)
|
||||
// Weights: 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2
|
||||
const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
|
||||
sum = 0
|
||||
position = 9
|
||||
for (let i = 0; i < 9; i++) {
|
||||
sum += parseInt(cleaned[i]) * position
|
||||
position -= 1
|
||||
for (let i = 0; i < 13; i++) {
|
||||
sum += parseInt(cleaned[i]) * weights2[i]
|
||||
}
|
||||
remainder = sum % 11
|
||||
const secondDigit = remainder < 2 ? 0 : 11 - remainder
|
||||
|
||||
if (parseInt(cleaned[9]) !== secondDigit) {
|
||||
if (parseInt(cleaned[13]) !== secondDigit) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue