-
Notifications
You must be signed in to change notification settings - Fork 0
fix(qa): correção de 11 bugs reais — MFA bypass, fail-open RBAC, carrinho, autosave + 7 #332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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(); | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Erro de lookup não está sendo verificado. A desestruturação ignora o campo 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| if (existing) { | ||||||||
| const { error } = await supabase | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validação incompleta de O 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 |
||
|
|
||
| // 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, | ||
|
|
@@ -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', | ||
| ); | ||
|
|
@@ -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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alinhe a hierarquia de gestão com a regra do backend. Nas Lines 32-40, 🤖 Prompt for AI Agents |
||
| } 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' }; | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Faça fail-closed quando
fetchAALfalhar.Se
fetchAALjá tiver retornadoaal2antes, 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; nocatch, limpecurrentAAL,nextAALehasMFA.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
🤖 Prompt for AI Agents