7c20b518d4
Repositorio estava ha ~5 sessoes sem commit. Consolida tudo desde d088a89.
Ver commit.md na raiz para descricao completa por sessao.
# Numeros
- A# auditoria abertos: 0/30
- V# verificacoes abertos: 5/52 (todos adiados com plano)
- T# testes escritos: 10/10
- Vitest: 192/192
- SQL integration: 33/33
- E2E (Playwright, novo): 5/5
- Migrations: 17 (10 novas Sessao 6)
- Areas auditadas: 7 (+documentos com 10 V#)
# Highlights Sessao 6 (hoje)
- V#34/V#41 Opcao B2: tenant_features com plano + override (RPC SECURITY DEFINER, tela /saas/tenant-features)
- A#20 rev2 self-hosted: defesa em 5 camadas (honeypot + rate limit + math captcha condicional + paranoid mode + dashboard /saas/security)
- Documentos hardening (V#43-V#49): tenant scoping em storage policies (vazamento entre clinicas eliminado), RPC validate_share_token, signatures policy granular
- SaaS Twilio Config (/saas/twilio-config): UI editavel para SID/webhook/cotacao; AUTH_TOKEN permanece em env var
- T#9 + T#10: useAgendaEvents.spec.js + Playwright E2E (descobriu bug no front que foi corrigido)
# Sessoes anteriores (1-5) consolidadas
- Sessao 1: auth/router/session, normalizeRole extraido
- Sessao 2: agenda - composables/services consolidados
- Sessao 3: pacientes - tenant_id em todas queries
- Sessao 4: security review pagina publica - 14/15 vulnerabilidades corrigidas
- Sessao 5: SaaS - P0 (A#30: 7 tabelas com RLS off corrigidas)
# .gitignore ajustado
- supabase/* + !supabase/functions/ (mantem 10 edge functions, ignora .temp/migrations gerados pelo CLI)
- database-novo/backups/ (regeneravel via db.cjs backup)
- test-results/ + playwright-report/
- .claude/settings.local.json (config local com senha de dev removida do tracking)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.3 KiB
Vue
97 lines
3.3 KiB
Vue
<!--
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI — MathCaptchaChallenge (A#20 rev2)
|
|
|--------------------------------------------------------------------------
|
|
| Componente de captcha matemático invocado SOB DEMANDA quando a edge
|
|
| function retorna 403 captcha-required ou na primeira tentativa se o IP
|
|
| já está marcado como suspeito.
|
|
|
|
|
| Uso:
|
|
| <MathCaptchaChallenge
|
|
| v-model:id="captchaId"
|
|
| v-model:answer="captchaAnswer"
|
|
| :function-url="..."
|
|
| />
|
|
|--------------------------------------------------------------------------
|
|
-->
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue';
|
|
import InputNumber from 'primevue/inputnumber';
|
|
import Button from 'primevue/button';
|
|
import { supabase } from '@/lib/supabase/client';
|
|
|
|
const props = defineProps({
|
|
id: { type: String, default: '' },
|
|
answer: { type: [Number, null], default: null },
|
|
autoLoad: { type: Boolean, default: true }
|
|
});
|
|
|
|
const emit = defineEmits(['update:id', 'update:answer']);
|
|
|
|
const challenge = ref({ id: '', question: '' });
|
|
const loading = ref(false);
|
|
const error = ref('');
|
|
const localAnswer = ref(props.answer);
|
|
|
|
async function loadChallenge() {
|
|
loading.value = true;
|
|
error.value = '';
|
|
try {
|
|
const { data, error: fnErr } = await supabase.functions.invoke('submit-patient-intake/captcha-challenge', {
|
|
method: 'POST',
|
|
body: {}
|
|
});
|
|
if (fnErr) throw fnErr;
|
|
const ch = data?.challenge || data;
|
|
challenge.value = { id: ch?.id || '', question: ch?.question || '' };
|
|
emit('update:id', challenge.value.id);
|
|
localAnswer.value = null;
|
|
emit('update:answer', null);
|
|
} catch (e) {
|
|
error.value = e?.message || 'Não foi possível carregar a verificação.';
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function onAnswerChange(v) {
|
|
localAnswer.value = v;
|
|
emit('update:answer', v);
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (props.autoLoad) loadChallenge();
|
|
});
|
|
|
|
defineExpose({ loadChallenge });
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rounded-xl border border-amber-400/30 bg-amber-400/5 p-4">
|
|
<div class="flex items-center justify-between gap-2 mb-2">
|
|
<div class="text-sm font-semibold text-slate-100">
|
|
<i class="pi pi-shield mr-2 text-amber-300" />
|
|
Verificação rápida
|
|
</div>
|
|
<Button icon="pi pi-refresh" text size="small" :loading="loading" @click="loadChallenge" v-tooltip.top="'Outra pergunta'" />
|
|
</div>
|
|
|
|
<p v-if="!challenge.question && !loading" class="text-xs text-slate-300">Carregando…</p>
|
|
<p v-if="error" class="text-xs text-rose-300">{{ error }}</p>
|
|
|
|
<div v-if="challenge.question" class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
<span class="text-base text-slate-100 font-medium">{{ challenge.question }}</span>
|
|
<InputNumber
|
|
:modelValue="localAnswer"
|
|
@update:modelValue="onAnswerChange"
|
|
placeholder="?"
|
|
class="!w-32"
|
|
:useGrouping="false"
|
|
inputClass="text-center"
|
|
/>
|
|
</div>
|
|
|
|
<p class="mt-2 text-[11px] text-slate-400">Confirma que você é humano. Sem cookies, sem rastreio externo.</p>
|
|
</div>
|
|
</template>
|