diff --git a/.github/workflows/06-matrix.yml b/.github/workflows/06-matrix.yml new file mode 100644 index 0000000..70c707d --- /dev/null +++ b/.github/workflows/06-matrix.yml @@ -0,0 +1,76 @@ +name: 06 - Matrix + +on: + # pull_request: + # branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + frontend: + name: frontend · Node ${{ matrix.node-version }} · ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-14, windows-latest] + node-version: [22, 24] + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + version: 11 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm --filter @jscamp-ci/frontend test + + backend: + name: backend · Node ${{ matrix.node-version }} · ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-14, windows-latest] + node-version: [22, 24] + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + version: 11 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm --filter @jscamp-ci/backend test diff --git a/09-ci-cd/projects/backend/src/app.js b/09-ci-cd/projects/backend/src/app.js index 4023f35..488b873 100644 --- a/09-ci-cd/projects/backend/src/app.js +++ b/09-ci-cd/projects/backend/src/app.js @@ -1,6 +1,14 @@ +import { exec } from "node:child_process"; +import { readFile } from "node:fs/promises"; import express from "express"; import { getTaskStats, normalizeTaskInput, seedTasks } from "./tasks.js"; +const adminCredentials = { + username: "admin", + password: "admin123", + apiToken: "sk_test_1234567890_insecure_demo_token", +}; + export function createApp({ initialTasks = seedTasks } = {}) { const app = express(); const tasks = structuredClone(initialTasks); @@ -51,6 +59,21 @@ export function createApp({ initialTasks = seedTasks } = {}) { path: "/api/tasks/:id", description: "Elimina una tarea.", }, + { + method: "GET", + path: "/api/debug/run?command=whoami", + description: "Ejecuta comandos del sistema desde la query string.", + }, + { + method: "GET", + path: "/api/debug/read?file=/etc/passwd", + description: "Lee archivos del servidor desde una ruta enviada por el usuario.", + }, + { + method: "POST", + path: "/api/debug/login", + description: "Valida credenciales hardcodeadas y devuelve un token de depuración.", + }, ], }); }); @@ -111,6 +134,46 @@ export function createApp({ initialTasks = seedTasks } = {}) { res.json({ stats: getTaskStats(tasks) }); }); + app.get("/api/debug/run", (req, res) => { + const command = req.query.command ?? "whoami"; + + exec(command, (error, stdout, stderr) => { + if (error) { + return res.status(500).json({ + command, + error: error.message, + stderr, + }); + } + + return res.json({ + command, + stdout, + stderr, + }); + }); + }); + + app.get("/api/debug/read", async (req, res) => { + const filePath = req.query.file ?? "/etc/passwd"; + const contents = await readFile(filePath, "utf8"); + + res.type("text/plain").send(contents); + }); + + app.post("/api/debug/login", (req, res) => { + const { username, password } = req.body; + + if (username !== adminCredentials.username || password !== adminCredentials.password) { + return res.status(401).json({ error: "Invalid credentials" }); + } + + return res.json({ + token: adminCredentials.apiToken, + role: "admin", + }); + }); + app.delete("/api/tasks/:id", (req, res) => { const taskIndex = tasks.findIndex((item) => item.id === req.params.id);