From 39506c7fb0e180850cf0de09b8f1ce6a3a1d8463 Mon Sep 17 00:00:00 2001 From: devguimaraes Date: Wed, 22 Apr 2026 20:38:09 -0300 Subject: [PATCH 1/8] ci(actions): add manual workflow trigger --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65ad928..8fa8a80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: CI on: + workflow_dispatch: pull_request: push: branches: From afb42e478c5679dc1badb030608988d8e979f700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Guimar=C3=A3es?= Date: Wed, 22 Apr 2026 20:51:53 -0300 Subject: [PATCH 2/8] docs(ci-checklist): adicionar checklist operacional para CI local --- docs/ci-checklist.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/ci-checklist.md diff --git a/docs/ci-checklist.md b/docs/ci-checklist.md new file mode 100644 index 0000000..bc157f6 --- /dev/null +++ b/docs/ci-checklist.md @@ -0,0 +1,31 @@ +# CI Checklist + +Checklist operacional para manter a qualidade local enquanto o GitHub Actions estiver bloqueado por billing. + +## Rotina local (antes de commit/PR) + +- [ ] Rodar `bun run lint` +- [ ] Rodar `bun run test:unit` +- [ ] Rodar `bun run build` +- [ ] Rodar `bun run test:e2e -- --project=chromium` +- [ ] Verificar `git status` limpo antes do commit +- [ ] Usar Conventional Commits nas mensagens +- [ ] Publicar trabalho na branch `develop` + +## Retomada quando billing liberar + +- [ ] Abrir **Actions > CI** no GitHub +- [ ] Disparar manualmente via **Run workflow** (branch `develop`) +- [ ] Confirmar execução dos jobs `lint`, `unit`, `build` e `e2e` +- [ ] Se falhar, salvar o link do run para análise +- [ ] Com CI verde em `develop`, seguir com PR de promoção para `main` + +## Comandos úteis + +```bash +bun run lint +bun run test:unit +bun run build +bun run test:e2e -- --project=chromium +``` + From 2e56eb1f9f7122133b3fdb3aa8f673455ad14a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Guimar=C3=A3es?= Date: Wed, 29 Apr 2026 00:16:25 -0300 Subject: [PATCH 3/8] fix: migrate cms auth flow to GitHub App --- .env.example | 5 +++++ api/auth.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++- api/auth.ts | 30 ++++++++++++++++++-------- 3 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d3b5741 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +GITHUB_CLIENT_ID=your_github_app_client_id +GITHUB_CLIENT_SECRET=your_github_app_client_secret +GITHUB_REPOSITORY_ID=your_repository_id +ALLOWED_GITHUB_USER=devguimaraes +OAUTH_REDIRECT_URI=https://www.devguimaraes.com.br/api/auth/callback diff --git a/api/auth.test.ts b/api/auth.test.ts index 9072e5a..98c4231 100644 --- a/api/auth.test.ts +++ b/api/auth.test.ts @@ -54,6 +54,7 @@ async function importHandler() { vi.stubEnv("GITHUB_CLIENT_ID", "test-client-id"); vi.stubEnv("GITHUB_CLIENT_SECRET", "test-client-secret"); vi.stubEnv("ALLOWED_GITHUB_USER", "devguimaraes"); + vi.stubEnv("GITHUB_REPOSITORY_ID", "123456789"); const mod = await import("./auth"); return mod.default; } @@ -78,7 +79,7 @@ describe("api/auth handler", () => { expect(redirectUrl).toContain("https://github.com/login/oauth/authorize"); expect(redirectUrl).toContain("client_id=test-client-id"); expect(redirectUrl).toContain("state=test-state-uuid"); - expect(redirectUrl).toContain("scope=repo%2Cuser%3Aemail"); + expect(redirectUrl).not.toContain("scope="); expect(res.setHeader).toHaveBeenCalledWith( "Set-Cookie", expect.stringContaining("oauth_state:test-state-uuid="), @@ -136,6 +137,19 @@ describe("api/auth handler", () => { await handler(req, res); + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + "https://github.com/login/oauth/access_token", + expect.objectContaining({ + body: JSON.stringify({ + client_id: "test-client-id", + client_secret: "test-client-secret", + code: "abc123", + redirect_uri: "https://www.devguimaraes.com.br/api/auth/callback", + repository_id: "123456789", + }), + }), + ); expect(res.status).toHaveBeenCalledWith(403); expect(res.send).toHaveBeenCalledWith("Access denied"); }); @@ -176,6 +190,45 @@ describe("api/auth handler", () => { expect(html).toContain("window.close"); }); + it("escapes callback auth data before embedding it in script HTML", async () => { + mockFetch + .mockResolvedValueOnce({ + json: () => + Promise.resolve({ + access_token: "tok", + token_type: "bearer", + scope: "", + }), + }) + .mockResolvedValueOnce({ + json: () => + Promise.resolve({ + login: "devguimaraes", + name: "Bruno ", + avatar_url: "https://example.com/avatar.png?a=&b=>", + bio: "bio & test", + }), + }); + + const req = mockReq({ + query: { code: "valid-code", state: "test-state-uuid" }, + headers: { + host: "devguimaraes.com.br", + origin: "https://devguimaraes.com.br", + cookie: "oauth_state:test-state-uuid=1", + }, + }); + const res = mockRes(); + + await handler(req, res); + + const html = (res.send as ReturnType).mock.calls[0][0] as string; + const scriptBody = html.match(/"); + expect(scriptBody).toContain("\\u003c/script\\u003e"); + expect(scriptBody).toContain("\\u0026"); + }); + it("returns 404 for unknown routes", async () => { const req = mockReq({ query: {} }); const res = mockRes(); diff --git a/api/auth.ts b/api/auth.ts index e5f7987..7f29ab1 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -2,10 +2,18 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; const CLIENT_ID = process.env.GITHUB_CLIENT_ID ?? ""; const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET ?? ""; +const REPOSITORY_ID = process.env.GITHUB_REPOSITORY_ID ?? ""; const ALLOWED_USER = process.env.ALLOWED_GITHUB_USER ?? ""; const CSRF_STATE_PREFIX = "oauth_state:"; const REDIRECT_URI = process.env.OAUTH_REDIRECT_URI ?? "https://www.devguimaraes.com.br/api/auth/callback"; +function escapeForScript(value: string): string { + return value + .replace(//g, "\\u003e") + .replace(/&/g, "\\u0026"); +} + export default async function handler( req: VercelRequest, res: VercelResponse, @@ -26,7 +34,6 @@ export default async function handler( const params = new URLSearchParams({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, - scope: "repo,user:email", state: oauthState, }); res.redirect(`https://github.com/login/oauth/authorize?${params}`); @@ -43,18 +50,23 @@ export default async function handler( } // Exchange code for access token + const tokenRequestBody: Record = { + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + code, + redirect_uri: REDIRECT_URI, + }; + if (REPOSITORY_ID) { + tokenRequestBody.repository_id = REPOSITORY_ID; + } + const tokenRes = await fetch("https://github.com/login/oauth/access_token", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, - body: JSON.stringify({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - code, - redirect_uri: REDIRECT_URI, - }), + body: JSON.stringify(tokenRequestBody), }); const tokenData = (await tokenRes.json()) as { @@ -87,7 +99,7 @@ export default async function handler( // Return HTML that uses the NetlifyAuthenticator protocol: // 1. Listen for "authorizing:github" from parent, respond to handshake // 2. Then send "authorization:github:success:{JSON}" with token data - const authData = JSON.stringify({ + const authData = escapeForScript(JSON.stringify({ token: tokenData.access_token, provider: "github", scope: tokenData.scope, @@ -95,7 +107,7 @@ export default async function handler( name: userData.name || userData.login, avatar_url: userData.avatar_url || "", bio: userData.bio || "", - }); + })); res.status(200).setHeader("Content-Type", "text/html; charset=utf-8").send(` + From eba6030ebaf140339c7043e1ebce9b5118eaf1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Guimar=C3=A3es?= Date: Wed, 29 Apr 2026 01:01:08 -0300 Subject: [PATCH 5/8] fix: prevent duplicate cms config load --- public/admin/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/admin/index.html b/public/admin/index.html index 2af18f2..b75da87 100644 --- a/public/admin/index.html +++ b/public/admin/index.html @@ -11,7 +11,7 @@ - +