Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions .tsc-baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"generatedAt": "2026-05-24T19:40:05.304Z",
"totalErrors": 508,
"generatedAt": "2026-05-25T14:23:22.196Z",
"totalErrors": 509,
"counts": {
"src/components/admin/products/BulkImportDialog.tsx": {
"TS2322": 1
Expand Down Expand Up @@ -328,9 +328,6 @@
"src/hooks/auth/useAccessSecurity.ts": {
"TS2345": 3
},
"src/hooks/auth/useAuthMFA.ts": {
"TS2339": 2
},
"src/hooks/collections/useCollections.ts": {
"TS2345": 1
},
Expand Down Expand Up @@ -442,9 +439,6 @@
"src/hooks/products/useSupplierFiscalData.ts": {
"TS2352": 2
},
"src/hooks/quotes/useAutoSaveQuote.ts": {
"TS2339": 2
},
"src/hooks/quotes/useQuoteComments.ts": {
"TS2322": 3,
"TS2353": 1
Expand Down Expand Up @@ -520,12 +514,8 @@
"src/lib/external-db/batch-import.ts": {
"TS2352": 2
},
"src/lib/external-db/bridge.ts": {
"TS2353": 2
},
"src/lib/external-db/invoke.ts": {
"TS2322": 1,
"TS2353": 3
"TS2322": 1
},
"src/lib/kit-builder/types.ts": {
"TS18048": 2
Expand Down Expand Up @@ -577,8 +567,7 @@
},
"src/pages/kit-builder/useKitBuilderQuote.ts": {
"TS2305": 1,
"TS2345": 2,
"TS2353": 1
"TS2345": 2
},
"src/pages/magic-up/MagicUpConfigPanel.tsx": {
"TS2322": 1,
Expand Down Expand Up @@ -660,6 +649,10 @@
},
"src/utils/productPdfExport.ts": {
"TS18048": 1
},
"src/integrations/supabase/types.ts": {
"TS2300": 10,
"TS2717": 1
}
}
}
4 changes: 1 addition & 3 deletions src/contexts/AuthContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ vi.mock('@/services/authService', async (importOriginal) => {
return {
authService: {
...actual.authService,
fetchAAL: vi
.fn()
.mockResolvedValue({ currentLevel: 'aal1', nextLevel: 'aal1', hasMFA: false }),
fetchAAL: vi.fn().mockResolvedValue({ currentAAL: 'aal1', nextAAL: 'aal1', hasMFA: false }),
fetchProfile: vi.fn().mockResolvedValue({ data: null, error: null }),
queryRoles: vi.fn().mockResolvedValue({ data: [], error: null }),
},
Expand Down
9 changes: 5 additions & 4 deletions src/hooks/auth/useAuthMFA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ export function useAuthMFA() {
const fetchAAL = useCallback(async () => {
try {
const data = await authService.fetchAAL();
setCurrentAAL(data.currentLevel);
setNextAAL(data.nextLevel);
setCurrentAAL(data.currentAAL);
setNextAAL(data.nextAAL);
setHasMFA(data.hasMFA);
} catch (e) {
if (import.meta.env.DEV) logger.warn('AAL fetch failed', e instanceof Error ? e.message : String(e));
if (import.meta.env.DEV)
logger.warn('AAL fetch failed', e instanceof Error ? e.message : String(e));
}
Comment on lines 16 to 19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Faça fail-closed quando fetchAAL falhar.

Se fetchAAL já tiver retornado aal2 antes, uma falha posterior mantém o estado antigo e o gate local continua tratando a sessão como MFA-validada. Para um fluxo de step-up, isso vira bypass por estado stale; no catch, limpe currentAAL, nextAAL e hasMFA.

Diff sugerido
     } catch (e) {
+      setCurrentAAL(null);
+      setNextAAL(null);
+      setHasMFA(false);
       if (import.meta.env.DEV)
         logger.warn('AAL fetch failed', e instanceof Error ? e.message : String(e));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (e) {
if (import.meta.env.DEV) logger.warn('AAL fetch failed', e instanceof Error ? e.message : String(e));
if (import.meta.env.DEV)
logger.warn('AAL fetch failed', e instanceof Error ? e.message : String(e));
}
} catch (e) {
setCurrentAAL(null);
setNextAAL(null);
setHasMFA(false);
if (import.meta.env.DEV)
logger.warn('AAL fetch failed', e instanceof Error ? e.message : String(e));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/auth/useAuthMFA.ts` around lines 16 - 19, O bloco catch do método
que chama fetchAAL está apenas logando o erro e deve falhar-fechado: ao capturar
qualquer exceção de fetchAAL limpe explicitamente o estado de autenticação
step-up removendo/zerando currentAAL, nextAAL e hasMFA (garantir que sejam
definidos como nulos/false conforme os tipos do hook) para evitar que um valor
antigo (por exemplo aal2) mantenha o gate local em estado MFA-validado; atualize
o catch existente (onde logger.warn é chamado) para também resetar esses campos
e, se houver, disparar quaisquer efeitos/updates necessários para propagar a
falha de MFA ao resto do hook/componente.

}, []);

Expand All @@ -29,6 +30,6 @@ export function useAuthMFA() {
nextAAL,
hasMFA,
fetchAAL,
clearMFA
clearMFA,
};
}
10 changes: 9 additions & 1 deletion src/hooks/intelligence/useMagicUpState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ export function useMagicUpState() {
setSelectedTechnique(null);
return;
}
// Guarda contra resposta fora-de-ordem: ao trocar de produto rapidamente, a
// resposta mais lenta (produto antigo) não pode sobrescrever o estado atual.
let cancelled = false;
(async () => {
setLoadingColors(true);
try {
Expand All @@ -318,6 +321,7 @@ export function useMagicUpState() {
limit: 100,
}),
]);
if (cancelled) return;
const images: ProductImage[] = (imagesResult.records || [])
.filter((img: Record<string, unknown>) => img.image_type !== 'box')
.map((img: Record<string, unknown>) => ({
Expand All @@ -341,12 +345,16 @@ export function useMagicUpState() {
});
setColors(Array.from(uniqueColors.values()));
} catch {
if (cancelled) return;
setColors([]);
setProductImages([]);
} finally {
setLoadingColors(false);
if (!cancelled) setLoadingColors(false);
}
})();
return () => {
cancelled = true;
};
}, [selectedProduct?.id]);

// ─── Print Areas from customization data ───────────────────────
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/products/useColorEnrichment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,14 @@ export function useColorEnrichment({
return productIds.filter((id) => !enrichedIds.has(id));
}, [productIds, hasFilter, filterKey]);

// Stable key: use count of new IDs + total count
// Chave por CONTEÚDO dos IDs (não só `.length`): dois conjuntos de IDs
// distintos com o mesmo tamanho colidiam na mesma entrada de cache e serviam
// enrichment do conjunto errado. `newProductIds` é memoizado em deps estáveis,
// então a chave não muda após o enrich (sem loop de refetch).
const queryEnabled = hasFilter && newProductIds.length > 0;

const query = useQuery({
queryKey: ['color-enrichment-batch', filterKey, newProductIds.length, productIds.length],
queryKey: ['color-enrichment-batch', filterKey, newProductIds.join(',')],
queryFn: async (): Promise<Map<string, ColorEnrichmentData>> => {
if (lastFilterKeyRef.current !== filterKey) {
enrichedIdsRef.current = new Set();
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/products/useProductImages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export function useProductImages(productId: string | null) {
*/
export function useProductImagesBatch(productIds: string[]) {
return useQuery({
queryKey: ['product-images-batch', productIds.sort().join(',')],
queryKey: ['product-images-batch', [...productIds].sort().join(',')],
queryFn: async () => {
if (productIds.length === 0) return new Map<string, ProductImage[]>();
return fetchProductImagesBatch(productIds);
Expand Down
14 changes: 11 additions & 3 deletions src/hooks/products/useSellerCarts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,20 @@ export function useSellerCarts() {
// Add item to cart
const addItem = useMutation({
mutationFn: async ({ cartId, item }: { cartId: string; item: AddToCartInput }) => {
const { data: existing } = await supabase
// Dedup pela identidade COMPLETA da variante (produto + cor). Antes casava
// só por product_id, o que (a) mesclava a 2ª cor na linha da 1ª — perdendo
// a variante — e (b) estourava o .maybeSingle() quando 2+ linhas do mesmo
// produto coexistiam. `.eq` não casa NULL no PostgREST: usar `.is` p/ nulos.
const colorName = item.color_name ?? null;
let lookup = supabase
.from('seller_cart_items')
.select('id, quantity')
.eq('cart_id', cartId)
.eq('product_id', item.product_id)
.maybeSingle();
.eq('product_id', item.product_id);
lookup =
colorName === null ? lookup.is('color_name', null) : lookup.eq('color_name', colorName);

Comment on lines +169 to +177
const { data: existing } = await lookup.limit(1).maybeSingle();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Erro de lookup não está sendo verificado.

A desestruturação ignora o campo error retornado por .maybeSingle(). Se a query falhar (erro de rede, timeout, constraint violation), existing será undefined e o código prosseguirá para inserção, potencialmente criando duplicatas ou mascarando falhas do banco.

🛡️ Fix proposto para tratar erro de lookup
-      const { data: existing } = await lookup.limit(1).maybeSingle();
+      const { data: existing, error: lookupError } = await lookup.limit(1).maybeSingle();
+      if (lookupError) throw lookupError;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: existing } = await lookup.limit(1).maybeSingle();
const { data: existing, error: lookupError } = await lookup.limit(1).maybeSingle();
if (lookupError) throw lookupError;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/products/useSellerCarts.ts` at line 178, A desestruturação atual em
useSellerCarts que faz const { data: existing } = await
lookup.limit(1).maybeSingle() ignora o campo error retornado por .maybeSingle();
modifique para desestruturar { data: existing, error } a partir de
lookup.limit(1).maybeSingle(), verifique se error existe e trate-o (lançando,
retornando um erro ou logando e abortando a operação) antes de prosseguir com a
inserção; garanta também que a lógica que decide criar um novo registro
diferencia entre existing === null (nenhum registro) e undefined (falha na
query) para evitar duplicatas ou mascaramento de falhas.


if (existing) {
const { error } = await supabase
Expand Down
16 changes: 12 additions & 4 deletions src/hooks/quotes/useAutoSaveQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ export function migratePayload<T>(
payload: unknown,
currentVersion: number = AUTOSAVE_SCHEMA_VERSION,
): AutoSavePayload<T> | null {
if (!payload) return null;
if (!payload || typeof payload !== 'object') return null;

const versioned = payload as { version?: number };
Comment on lines +29 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validação incompleta de unknown com type assertions.

O payload é validado apenas como objeto (typeof payload !== 'object'), mas depois usa type assertions (as { version?: number }, linha 38 as T, linha 55 as AutoSavePayload<T>) sem verificar se a estrutura realmente existe. Se o localStorage contiver dados corrompidos ou malformados, o código pode falhar em runtime.

Considere adicionar validações explícitas:

if (!payload || typeof payload !== 'object') return null;

const versioned = payload as { version?: number; data?: unknown };

// Validar presença de campos críticos
if (!('version' in versioned) && !('data' in versioned)) {
  logger.warn('[AutoSave] Invalid payload structure');
  return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/quotes/useAutoSaveQuote.ts` around lines 29 - 31, The payload read
in useAutoSaveQuote (variable payload) is only checked with typeof object then
blindly cast to { version?: number } and to T/AutoSavePayload<T>, which can
throw at runtime for malformed localStorage data; update the validation in the
top of the function to assert that payload is non-null, typeof payload ===
'object', and that required keys exist (e.g., 'version' and/or 'data' present)
before casting to versioned / AutoSavePayload<T>, log a warning via the module
logger when structure is invalid, and return null to avoid unsafe type
assertions in functions like useAutoSaveQuote and any code paths that rely on
the versioned/data shape.


// Se for um payload antigo sem versão (v1)
if (!payload.version) {
if (!versioned.version) {
logger.debug('[AutoSave] Migrating from v1 to v2');
return {
version: currentVersion,
Expand All @@ -40,7 +42,7 @@ export function migratePayload<T>(

// Se a versão do payload for maior que a atual, tratamos como inseguro
// e retornamos null para evitar corrupção de estado (o usuário perderá o rascunho, mas não quebrará o app)
if (payload.version > currentVersion) {
if (versioned.version > currentVersion) {
console.warn(
'[AutoSave] Future payload version detected, skipping restore to prevent state corruption',
);
Expand All @@ -64,10 +66,16 @@ export function useAutoSaveQuote<T>({
key = 'quote_builder_autosave',
}: AutoSaveOptions<T>) {
const lastSavedRef = useRef<string>('');
// Restaura UMA única vez por montagem. Sem este guard, callers que passam um
// `onRestore` inline (identidade nova a cada render) faziam o efeito re-rodar
// a cada render e re-aplicar o rascunho salvo POR CIMA das edições ao vivo do
// usuário (ex.: o 2º item adicionado era revertido para o estado salvo).
const hasRestoredRef = useRef(false);

// Efeito de carregamento inicial (Restaurar)
useEffect(() => {
if (!enabled) return;
if (!enabled || hasRestoredRef.current) return;
hasRestoredRef.current = true;

const saved = localStorage.getItem(key);
if (saved) {
Expand Down
11 changes: 9 additions & 2 deletions src/hooks/simulation/useTecnicasUnificadas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,18 @@ export function useCustomizationPricing() {
const calc = usePrecoCalculation();

return {
priceTables: [] as Array<{
// Derivado de `techniques` (SSOT: tabelas de preço ativas). Consumido por
// QuantityComparisonTable para casar técnica→tabela. Antes era `[]` fixo, o
// que fazia toda a tabela de comparação renderizar "N/D".
priceTables: calc.techniques.map((t) => ({
table_code: t.code,
customization_type_name: t.name,
price_by_color: t.priceByColor,
})) as Array<{
table_code: string;
customization_type_name: string;
price_by_color?: boolean | null;
}>, // Legado - não mais usado
}>,
techniques: calc.techniques,
standardQuantities: calc.standardQuantities,
isLoading: calc.isLoading,
Expand Down
20 changes: 16 additions & 4 deletions src/lib/access/access-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,22 @@ export const checkAccess = (
const isSupervisorOrAbove = safeRoles.some((r) =>
['dev', 'supervisor', 'admin', 'manager'].includes(r),
);
Comment on lines 25 to 27
if (requiredRole === 'supervisor' && !isSupervisorOrAbove) {
return { allowed: false, reason: 'insufficient_role' };
}
if (requiredRole === 'dev' && !safeRoles.includes('dev')) {
if (requiredRole === 'dev') {
if (!safeRoles.includes('dev')) {
return { allowed: false, reason: 'insufficient_role' };
}
} else if (
requiredRole === 'supervisor' ||
requiredRole === 'admin' ||
requiredRole === 'manager'
) {
// Papéis de gestão exigem supervisor-ou-acima.
if (!isSupervisorOrAbove) {
return { allowed: false, reason: 'insufficient_role' };
}
Comment on lines +32 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Alinhe a hierarquia de gestão com a regra do backend.

Nas Lines 32-40, admin/manager passam a depender de isSupervisorOrAbove, mas o helper equivalente em supabase/functions/_shared/auth.ts:84-91 usa outro conjunto de papéis (simulation entra e manager não). Isso cria drift de autorização entre cliente e servidor: o frontend pode liberar rota que o backend não reconhece, ou bloquear um papel que o backend aceita.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/access/access-policy.ts` around lines 32 - 40, The management-role
check in access-policy.ts (the branch handling requiredRole === 'supervisor' ||
'admin' || 'manager') is out of sync with the server-side helper in
supabase/functions/_shared/auth.ts — the server includes 'simulation' in the
supervisor-or-above set and treats 'manager' differently; update the frontend
logic so the same helper/role-set is used: either change the local
isSupervisorOrAbove implementation to match the server helper's exact role
membership (including 'simulation' and the same inclusion/exclusion of
'manager') or replace the inline condition in access-policy.ts to call the
shared helper/function used by the backend so both sides evaluate the same set
of roles. Ensure you reference and align the symbols isSupervisorOrAbove and the
server helper in supabase/functions/_shared/auth.ts.

} else if (!safeRoles.includes(requiredRole)) {
// Default-deny: qualquer outro papel exigido (ex.: 'agente') requer o papel
// exato. Antes, valores não tratados caíam em `allowed: true` (fail-open).
return { allowed: false, reason: 'insufficient_role' };
}
}
Expand Down
Loading