Repository navigation
feat: pagamento no BFF, JWT só em cookie HttpOnly e corte de escopo para a Fase 1 - #10
Merged
Hidden character warning
The head ref may contain hidden characters: "Vers\u00e3o-1.0"
Merged
Conversation
O Next resolvia preco na Stripe e no Mercado Pago, criava a cobranca com os
SDKs dos gateways, consultava se o pagamento aconteceu e conferia titularidade
contra a sessao. Isso violava a regra do CLAUDE.md ("thin proxies to the Java
BFF"), mantinha dois catalogos de preco independentes e deixava as chaves de
gateway num ambiente que nao precisa delas.
As cinco rotas de pagamento viram proxy fino. Preco, status, titularidade e
tier passam a sair inteiramente do Java, e os webhooks das gateways apontam
direto para ele -- o salto Next->Java, que nao tinha credencial para
autenticar, deixa de existir.
- bffPayments.ts concentra o encaminhamento: BACKEND_API_URL, Bearer do aluno
e tratamento de erro num lugar so. Nada de erro do BFF chega ao navegador;
acima de 2xx a resposta vira mensagem generica do proprio status.
- sanitizeCheckoutBody remonta o corpo por allowlist: so planId e payer
(first_name, last_name, identification e, no boleto, address). Valor, status
e e-mail do pagador nao saem do navegador -- e-mail vem da sessao no Java, o
que e o que torna a conferencia de titularidade possivel.
- O payer do boleto foi unificado com o de cartao e PIX: um contrato so.
- activate/route.ts grava o JWT novo no cookie user_data HttpOnly e remove o
campo token do corpo -- o tier muda sem o aluno relogar, e o navegador nunca
recebe o token.
- normalizeTier absorve a grafia do claim tipo, que ainda nao foi acordada com
o time Java. Fail safe: desconhecido vira FREE.
- Removidos stripe, mercadopago e @mercadopago/sdk-react. Ficam apenas
@stripe/stripe-js e @stripe/react-stripe-js, que sao o Payment Element --
o cartao vai direto do navegador para a gateway.
Vitest configurado (nao havia test runner). Os scripts k6 foram para
src/test/K6/ para nao se misturarem aos unitarios.
BREAKING CHANGE: enquanto o BFF Java nao expuser GET /plans, os tres
endpoints de checkout e POST /subscriptions/activate, o checkout responde 503.
O contrato exigido esta na primeira secao do CHANGES.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ttpOnly
O JWT do aluno era gravado no localStorage alem do cookie user_data. Eram duas
copias do mesmo token: uma que o navegador protege (HttpOnly, JS nao le) e uma
que qualquer script na pagina le numa linha -- dependencia npm comprometida,
extensao, XSS. Com o token, o portador se faz passar pelo aluno no BFF.
Contraria a regra explicita do CLAUDE.md.
A copia existia porque o cliente precisava de duas coisas, e so uma era real:
montar Authorization: Bearer (redundante -- o cookie acompanha sozinho todo
fetch same-origin) e ler o tier de dentro do JWT para desenhar a tela (essa
precisava de substituto).
Executado na ordem servidor -> cliente -> escrita; inverter derrubaria
autenticacao em tela que funciona.
- Cinco rotas passam a aceitar o cookie via readUserToken (universities,
flash-cards, user/stats, Nota-corte, subscribe). O subscribe era o pior dos
cinco: recebia o JWT no corpo da requisicao, e um token que o cliente
escolhe qual mandar e um token que ele pode trocar pelo de outro aluno.
Etapa aditiva -- o header continua sendo aceito.
- GET /api/user/me (nova) devolve so os claims de exibicao
{ id, nome, email, tier, newsletter }. Nunca token, exp, iat nem payload
cru. Um teste trava a lista exata de chaves da resposta.
- sync-user para de devolver o JWT no corpo (responde { ok, id, tipo }) e
para de vazar details/error do backend ao navegador.
- Oito chamadas do cliente param de montar Authorization. Server Components
que leem o cookie com cookies() ficaram intactos: servidor lendo o proprio
cookie e o caminho certo.
- useUserTier busca /api/user/me. O setInterval de 2s morreu -- era leitura de
memoria, viraria uma requisicao a cada dois segundos por aba aberta. Restam
os gatilhos user_synced (mesma aba) e storage (outras abas).
- As tres escritas no localStorage sumiram, e uma limpeza incondicional no
carregamento apaga a copia legada: parar de escrever nao apaga o que ja
foi escrito.
- Quatro console.log que imprimiam pedaco do JWT foram removidos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A documentacao descrevia um repositorio que nao existe mais: SDK do Mercado Pago dentro do Next, webhooks que foram removidos, rotas lendo array estatico, "no test runner configured yet". Quem chegasse pelo README implementaria contra a arquitetura errada. Conferido arquivo por arquivo contra o codigo. Quatro afirmacoes que os documentos faziam e o codigo desmente foram corrigidas: - "k6 scripts use __ENV.BFF_URL" -- BASE_URL e fixo em localhost:3000 - UniversityStorage "falls back to src/lib/dataUniversity" -- o arquivo nao existe; falhando, a lista fica vazia - docs/manual-frontend.md, "always consult this guide" -- nao existe - "There is no test runner configured yet" -- Vitest, 137 testes - README.md (raiz): mapa de qual documento responde o que, os dois caminhos de acesso ao BFF (Server Component direto vs Route Handler), secoes de Pagamento e Pendencias Conhecidas. - front/README.md: era o boilerplate do create-next-app. Agora e o guia operacional -- setup, scripts, estrutura, tres regras de arquitetura, tiers, testes e convencoes de nome. - front/src/app/api/README.md: reescrito com as 18 rotas reais, o endpoint do BFF de cada uma, mapa de consumo e checklist para rota nova. - front/src/test/K6/README.md: caminho dos scripts e separacao Vitest x k6. - CLAUDE.md: seis afirmacoes que viraram falsas, mais as regras novas de seguranca (cliente nao monta Authorization, nenhuma rota devolve o JWT no corpo, nunca logar token) e a secao de Payment Chain. - PAUTA.md -> docs/Pauta_para_reuniao.md, com o mapa de arquivos por rota de comunicacao e a tabela de desvios do fluxo de dez passos. - CHANGES.md: historico detalhado das tres entregas. Registradas como pendencia, nao corrigidas: POST /api/simulations/save-result e POST /api/generate-explanation nao existem, POST /api/games/flash-cards responde 405 (a rota exporta so GET), simulationStore.ts e LoadingContext.tsx sem importador, e npm run lint quebrado desde o Next 16. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Corte de escopo conforme "Fase 1 - Lançamento Core (JAN 2027)": remove o que restava de features das Fases 2 e 3, para que nada prometido chegue ao usuário antes da hora. Removido: - app/api/ai/ (VestIA — chat e histórico), Fase 3 - components/PremiumFeatureModal.tsx, modal de upgrade do VestIA (órfão) - components/mockups/CronogramaMockup.tsx, Planner de Estudos (órfão), Fase 2 - lib/data/playlists.ts, Playlist de Questões (órfão), Fases 2/5 - escopo calendar.events do provider Google em lib/core/auth.ts — existia só para o Planner; o login deixa de pedir acesso ao Google Calendar - wrappers DndProvider/HTML5Backend em app/page.tsx e profile/ProfileClient.tsx - 13 dependências sem consumidor: @google/genai, googleapis, date-holidays, react-dnd(+html5-backend), @fullcalendar/* (5), react-datetime-picker, docx, file-saver Mantido de propósito: Explicação de Gabarito por IA (rota, Questoes_Gemini, subjectKey, fiação no ProfileClient e @google/generative-ai), Discord, Stripe e a criação de prova personalizada. Copy da landing e do paidPlan reescrito: deixa de vender "agente de IA", "plano personalizado" e "Recomendação IA" — features que não vão ao ar na Fase 1. Documentação: - README.md da raiz reescrito de ponta a ponta contra o código atual. Corrige rotas e módulos inexistentes, o fluxo de sessão (JWT só no cookie HttpOnly), as variáveis de ambiente realmente lidas, e passa a registrar o estado atual e as dívidas conhecidas - src/app/api/README.md: corrige a nota sobre /api/generate-explanation (a rota existe; falta a UI) e o escopo OAuth - src/lib/MOCK_ARCHITECTURE.md: remove playlist, /api/ai e /api/planner front/CLAUDE.md também foi reescrito como briefing e mapa de documentação, mas não entra neste commit: o .gitignore ignora CLAUDE.md em qualquer nível. Este commit também carrega trabalho em andamento que já estava na árvore e não faz parte do corte: a reorganização de src/lib por domínio (core, data, games, store — os arquivos movidos aparecem como rename), o gate de IP em proxy.ts, a centralização de tests/ em front/ e os componentes novos de perfil e checkout. Pendência conhecida: npm run build falha por @/lib/badges/*, que vive em outra branch. Não foi introduzida aqui.
Duas entregas sobre o mesmo sintoma, nenhuma delas commitada até aqui:
401 permanente em GET /api/user/me e a aplicação inteira se comportando
como deslogada, sem nada na tela apontando a causa.
POST /api/sync-user — timeout explícito e diagnóstico
Timeout próprio de 8s (AbortSignal.timeout), abaixo dos 10s do undici,
para a falha ser nossa e nomeável em vez de um TypeError: fetch failed
genérico. O catch único virou classificação por causa, com as tags
[NET] 504, [BFF] relay, [SHAPE] 502 e [BUG] 500 no log do servidor —
na tela os quatro casos são idênticos. Fim do 200 silencioso: extrairJwt
aceita cinco grafias de resposta do Java, apara o prefixo Bearer, e o
cookie só é gravado se decodeJWT devolver payload válido. O corpo
devolvido ao navegador segue sanitizado nos quatro casos.
GET /api/user/me — fallback para a sessão do NextAuth
O cookie user_data só é gravado por /api/sync-user; quando essa rota
falha, o aluno tem sessão OAuth válida e mesmo assim aparece deslogado.
A rota passa a ler a sessão do NextAuth como segunda fonte.
Esse cookie chega fragmentado (next-auth.session-token.0/.1) porque o
callback jwt guarda o objeto account inteiro do Google e o valor passa
dos 4096 bytes que o navegador aceita. readNextAuthSessionToken() em
sessionToken.ts remonta os pedaços ordenando pelo sufixo numérico — não
alfabético, senão .10 cai depois de .1 — e reconhece também o nome
__Secure- usado em produção. O valor remontado é um JWE cifrado, não um
JWT: jwt-decode devolveria null, então quem o abre é o decode do
next-auth/jwt com a NEXTAUTH_SECRET.
No fallback o id é sempre null, nunca o sub do NextAuth: SyncUserEffect
só pula a sincronização quando claims.id é truthy, então preenchê-lo
faria o user_data nunca ser gravado. Novo header X-Claims-Source aponta
qual fonte respondeu.
Testes e documentação
21 specs em front/tests/sync-user.route.test.ts e 21 em
front/tests/user-me.route.test.ts, ambos passando. As specs de user/me
cifram de verdade com o encode do next-auth/jwt em vez de dublar o
decode. CHANGES.md, README.md da raiz e src/app/api/README.md
atualizados a partir do CHANGES.md.
Fica de fora, por decisão do usuário: a causa raiz do sync-user falhar.
Suspeitas em aberto — o endpoint chamado é ${BACKEND_API_URL}/auth/google
enquanto as rotas que funcionam usam ${BACKEND_API_URL}/api/...; o scope
do Google em lib/core/auth.ts não inclui openid; e refreshAccessToken
não renova o id_token guardado em googleAccount.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
`next lint` foi removido no Next 16 e o FlatCompat quebrava com "Converting circular structure to JSON". O script `lint` passa a ser `eslint .` com eslint-config-next (core-web-vitals + typescript) nativo. Com a config funcionando, 103 erros apareceram: - 63 no-explicit-any: tipos locais para as respostas do BFF (CursoBackend, ArtigoBackend, QuestaoBruta/ProvaBruta, CourseResult, ComplexEnunciado); casts desnecessários removidos dos gateways - 4 no-unescaped-entities, 1 ban-ts-comment - React Compiler: purity (ids determinísticos em NotaCorteResultados), immutability (Audio em useRef no NavigationSound), static-components (ArenaGameClient), preserve-manual-memoization (deps do summary), error-boundaries (JSX fora do try/catch nos loaders) - 29 set-state-in-effect rebaixados para warn: são efeitos de hidratação e refatorá-los pede teste visual (dívida no CHANGES.md) Resultado: 0 erros, 106 avisos; tsc --noEmit limpo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rate-token - generate-token: a rota lia o cookie com cookies() de next/headers, que lança fora do request scope e virava 500. Passa a usar readUserToken e decodeJWT (ponto único de decode). +2 specs: cookie user_data e JWT sem id/sub/email - badge-cohesion e user-profile: scripts assert convertidos para describe/it, mesmas asserções - materiaVisualIconBg: removido o bloco que lia Questoes_Gemini.tsx, apagado no commit 83575da npm test: 14 arquivos, 107 testes passando. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- .github/workflows/ci.yml: em PR contra main roda npm ci --legacy-peer-deps, npm run lint e npm run test com Node 20 - README.md: versão enxuta; a documentação técnica passa a morar em projeto-EdTech/docs (architecture/frontend.md e adr/). A linha do script lint diz `eslint .` - front/.env.example criado (o README manda copiá-lo) só com as chaves lidas pelo código; exceção no .gitignore - CHANGES.md: entrada [Chore/ci] Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
As duas branches divergiram desde ce69c32 (janeiro): 33 commits de um lado, 42 do outro, nenhum patch em comum. O merge mantém integralmente a árvore da Versão-1.0, que é a linha mais recente (ago/set) e a que documenta o corte para a Fase 1 em CHANGES.md e README.md. Ficam de fora os 68 arquivos que só existem na main — ranking, VestIA, planner, community, pricing e badges —, features de Fase 2 a 5 removidas de propósito no commit f73044e. Elas continuam recuperáveis pelo histórico da main quando a fase chegar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O LICENSE MIT entrou em 982f035, mas o repositório ainda se descrevia como fechado: - README: sai o aviso "Repositório PRIVADO — uso exclusivo da equipe"; entra o aviso de projeto open-source sob MIT, com badges de CI e licença. A seção Licença deixa de dizer "código proprietário, todos os direitos reservados" e passa a MIT, ressalvando que a licença cobre o código — marca, identidade visual e o banco de questões das bancas ficam de fora - README/Contribuindo: fluxo por fork para quem é de fora, e o workflow TDD descrito no próprio README, já que CLAUDE.md está no .gitignore e não chega a quem clona - Footer: "Todos os direitos reservados" → "Projeto open-source sob licença MIT", com link para o LICENSE - CHANGES.md: entrada [Chore/licença] Varredura no histórico completo por .env versionado e por chaves Stripe, Google, Mercado Pago, Slack e PEM: nenhum segredo real — só placeholders em documentação. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DiscordTokenModal.test.tsx fazia Object.assign(navigator, …) no
beforeEach. O global navigator só existe a partir do Node 21: passava
local (Node 24) e quebrava no runner do CI (Node 20) com
"ReferenceError: navigator is not defined".
Troca por vi.stubGlobal('navigator', …), com vi.unstubAllGlobals() no
afterEach. Verificado rodando a suíte com o global removido: 14
arquivos, 107 testes passando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grolla05
added a commit
that referenced
this pull request
Sep 17, 2026
Merge pull request #10 from projeto-EdTech/Versão-1.0
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resumo
Consolida a branch
Versão-1.0sobremain: a cadeia de pagamento passa a ser proxy fino para o BFF Java, o JWT do aluno sai dolocalStorage, o escopo do repositório é cortado para a Fase 1 e a documentação é reescrita contra o código atual.São 22 commits, 347 arquivos alterados (+28.809 / −21.315). Boa parte do volume é asset binário (stickers/mascotes gerados) e a reorganização de
src/libpor domínio, que aparece como rename.Entregas principais
1. Pagamento no BFF (
424857d)As cinco rotas de pagamento viraram proxy fino. Preço, status, titularidade e tier saem inteiramente do Java; os webhooks das gateways apontam direto para o BFF.
bffPayments.tsconcentra o encaminhamento (BACKEND_API_URL, Bearer do aluno, tratamento de erro). Nenhum erro do BFF chega ao navegador — acima de 2xx vira mensagem genérica do status.sanitizeCheckoutBodyremonta o corpo por allowlist: sóplanIdepayer. Valor, status e e-mail do pagador não saem do navegador.activate/route.tsgrava o JWT novo no cookieuser_dataHttpOnly e remove o campotokendo corpo.normalizeTiercom fail safe: desconhecido viraFREE.stripe,mercadopagoe@mercadopago/sdk-react. Ficam só@stripe/stripe-jse@stripe/react-stripe-js(Payment Element).2. JWT fora do
localStorage(f8a638e)O token era gravado no
localStoragealém do cookie — uma cópia legível por qualquer script na página (dependência npm comprometida, extensão, XSS).readUserToken(universities, flash-cards, user/stats, Nota-corte, subscribe). Osubscriberecebia o JWT no corpo da requisição.GET /api/user/medevolve só claims de exibição (id, nome, email, tier, newsletter) — nunca token,exp,iatou payload cru; um teste trava a lista exata de chaves.sync-userpara de devolver o JWT no corpo e de vazardetails/errordo backend.Authorization. Server Components que leem o cookie comcookies()ficaram intactos.useUserTierbusca/api/user/me; osetIntervalde 2s foi removido.localStorage.console.logque imprimiam pedaço do JWT removidos.3. Sessão não trava quando o BFF não responde (
bb83bc8)Sintoma: 401 permanente em
GET /api/user/mee a aplicação inteira se comportando como deslogada.POST /api/sync-user: timeout próprio de 8s (AbortSignal.timeout), abaixo dos 10s do undici. Classificação de falha por causa ([NET]504,[BFF]relay,[SHAPE]502,[BUG]500) no log do servidor — na tela os quatro casos são idênticos.extrairJwtaceita cinco grafias de resposta do Java e o cookie só é gravado sedecodeJWTdevolver payload válido.GET /api/user/me: fallback para a sessão do NextAuth como segunda fonte.readNextAuthSessionToken()remonta o cookie fragmentado (next-auth.session-token.0/.1) ordenando pelo sufixo numérico e reconhece o nome__Secure-. O valor remontado é um JWE, então quem abre é odecodedonext-auth/jwtcom aNEXTAUTH_SECRET. No fallback oidé semprenull, para não impedir a gravação futura douser_data. Novo headerX-Claims-Source.4. Corte de escopo para a Fase 1 (
f73044e)Removido:
app/api/ai/(VestIA, Fase 3),PremiumFeatureModal,CronogramaMockup(Planner, Fase 2),lib/data/playlists.ts, o escopocalendar.eventsdo provider Google, os wrappersDndProvidere 13 dependências sem consumidor (@google/genai,googleapis,date-holidays,react-dnd,@fullcalendar/*,docx,file-savere outras).Mantido de propósito: Explicação de Gabarito por IA, Discord, Stripe e a criação de prova personalizada. A copy da landing e do
paidPlanfoi reescrita para não vender features que não vão ao ar na Fase 1.5. Documentação e testes (
f00215e,10a70ee)README.mdda raiz,front/README.md,src/app/api/README.md,src/lib/MOCK_ARCHITECTURE.mdesrc/test/K6/README.mdreescritos contra o código atual — quatro afirmações que a documentação fazia e o código desmentia foram corrigidas.PAUTA.mdviroudocs/Pauta_para_reuniao.md.CHANGES.mdguarda o histórico detalhado.Vitest configurado (não havia test runner). Os scripts k6 foram separados dos unitários.
CI, README enxuto e lint/testes verdes (
66bf2b2,6ee5e21,b780b67).github/workflows/ci.yml): em todo PR contramainrodanpm ci --legacy-peer-deps→npm run lint→npm run testcom Node 20.next lint→eslint .com flat config nativo doeslint-config-next. Zerados 103 erros: 63anysubstituídos por tipos das respostas do BFF, entities, ts-comment e 7 regras do React Compiler (purity, immutability, static-components, preserve-manual-memoization, error-boundaries).react-hooks/set-state-in-effect(29, padrão de hidratação) fica como warn; está registrado como dívida.generate-tokenpassa a usarreadUserToken+decodeJWT(antes,cookies()fora do request scope virava 500); 2 scriptsassertconvertidos para Vitest; removido o bloco que liaQuestoes_Gemini.tsx, apagado em83575da.projeto-EdTech/docs;front/.env.examplecriado.Verificado localmente:
npm test14 arquivos / 107 testes ✅ ·npm run lint0 erros ✅ ·tsc --noEmit0 erros ✅ ·npm run build✅ ·npm ci --dry-run✅Open-source (MIT) e merge de
main(719a612,d3b8a62,3ee8161)LICENSEMIT já tinha entrado em982f035; saíram o aviso "Repositório PRIVADO — uso exclusivo da equipe" e a seção "código proprietário, todos os direitos reservados" do README, e o rodapé do site passa a dizer "Projeto open-source sob licença MIT". A ressalva: a licença cobre o código — marca, identidade visual e o banco de questões das bancas ficam de fora..envversionado e por chaves Stripe, Google, Mercado Pago, Slack e PEM — nenhum segredo real, só placeholders em documentação.main(-s ours): as branches divergiram emce69c32e seguiram paralelas, sem nenhum patch em comum — eram 87 arquivos em conflito, que travavam o CI. A árvore daVersão-1.0vale inteira, por ser a linha mais recente e a que registra o corte para a Fase 1. Ficam de fora os 68 arquivos que só existem namain(VestIA, planner, ranking, community, pricing, badges), removidos de propósito emf73044ee recuperáveis pelo histórico.DiscordTokenModal.test.tsxusava o globalnavigator, que só existe do Node 21 em diante — passava local (Node 24) e quebrava no runner (Node 20). Corrigido comvi.stubGlobal.Como testar
Pendências conhecidas (não introduzidas aqui)
POST /api/simulations/save-resultePOST /api/generate-explanationnão existem.POST /api/games/flash-cardsresponde 405 (a rota exporta só GET).simulationStore.tseLoadingContext.tsxsem importador.sync-userfalhar segue em aberto — suspeitas registradas embb83bc8: o endpoint chamado é${BACKEND_API_URL}/auth/googleenquanto as rotas que funcionam usam${BACKEND_API_URL}/api/...; o scope do Google não incluiopenid;refreshAccessTokennão renova oid_token.Base sugerida:
main. A branch está 42 commits atrás demain— vale conferir se o alvo certo não éDEV.🤖 Generated with Claude Code