diff --git a/bridge/Dockerfile b/bridge/Dockerfile new file mode 100644 index 0000000..1bc306b --- /dev/null +++ b/bridge/Dockerfile @@ -0,0 +1,7 @@ +FROM node:22-bookworm-slim +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install --omit=dev +COPY server.mjs ./ +EXPOSE 8787 +CMD ["node","server.mjs"] diff --git a/bridge/docker-compose-bridge.yml b/bridge/docker-compose-bridge.yml new file mode 100644 index 0000000..edee86e --- /dev/null +++ b/bridge/docker-compose-bridge.yml @@ -0,0 +1,15 @@ +services: + issue_bridge: + build: ./bridge + environment: + GITEA_API_BASE: "http://gitea:3000" + GITEA_TOKEN: "${GITEA_TOKEN}" + GITEA_OWNER: "Archicratia" + GITEA_REPO: "archicratie-edition" + restart: unless-stopped + networks: + - internal + +networks: + internal: + external: true diff --git a/bridge/package.json b/bridge/package.json new file mode 100644 index 0000000..8bed1bd --- /dev/null +++ b/bridge/package.json @@ -0,0 +1,10 @@ +{ + "name": "issue-bridge", + "private": true, + "type": "module", + "dependencies": { + "express": "^4.19.2", + "multer": "^1.4.5-lts.1" + } + } + \ No newline at end of file diff --git a/bridge/server.mjs b/bridge/server.mjs new file mode 100644 index 0000000..60be636 --- /dev/null +++ b/bridge/server.mjs @@ -0,0 +1,89 @@ +import express from "express"; +import multer from "multer"; + +const app = express(); +const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } }); // 25 MB + +const { + GITEA_API_BASE, // ex: http://gitea:3000 (ou https://forge.tld) + GITEA_TOKEN, // PAT du bot + GITEA_OWNER, // owner/org + GITEA_REPO // repo +} = process.env; + +function mustEnv(name) { + if (!process.env[name]) throw new Error(`Missing env ${name}`); +} +["GITEA_API_BASE","GITEA_TOKEN","GITEA_OWNER","GITEA_REPO"].forEach(mustEnv); + +function isEditor(req) { + // Adapte selon tes headers Authelia. Souvent Remote-Groups / Remote-User. + const groups = String(req.header("Remote-Groups") || req.header("X-Remote-Groups") || ""); + return groups.split(/[,\s]+/).includes("editors"); +} + +async function giteaFetch(path, init = {}) { + const url = String(GITEA_API_BASE).replace(/\/+$/, "") + path; + const headers = new Headers(init.headers || {}); + headers.set("Authorization", `token ${GITEA_TOKEN}`); + return fetch(url, { ...init, headers }); +} + +app.get("/health", (_req, res) => res.json({ ok: true })); + +app.post("/media", upload.single("file"), async (req, res) => { + try { + if (!isEditor(req)) return res.status(403).json({ ok: false, error: "forbidden" }); + + const file = req.file; + const title = String(req.body.title || "").trim(); + const body = String(req.body.body || "").trim(); + const suggestedName = String(req.body.suggestedName || "").trim(); + + if (!file) return res.status(400).json({ ok: false, error: "missing_file" }); + if (!title) return res.status(400).json({ ok: false, error: "missing_title" }); + if (!body) return res.status(400).json({ ok: false, error: "missing_body" }); + + // 1) Create issue + const r1 = await giteaFetch(`/api/v1/repos/${encodeURIComponent(GITEA_OWNER)}/${encodeURIComponent(GITEA_REPO)}/issues`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title, body }) + }); + + if (!r1.ok) { + const t = await r1.text().catch(() => ""); + return res.status(502).json({ ok: false, step: "create_issue", status: r1.status, detail: t.slice(0, 2000) }); + } + + const issue = await r1.json(); + const index = issue?.number ?? issue?.index; + const issueUrl = issue?.html_url; + + if (!index) return res.status(502).json({ ok: false, step: "create_issue", error: "missing_issue_index" }); + + // 2) Upload attachment (multipart field name = "attachment") :contentReference[oaicite:1]{index=1} + const fd = new FormData(); + fd.append("attachment", new Blob([file.buffer], { type: file.mimetype || "application/octet-stream" }), file.originalname); + + const q = suggestedName ? `?name=${encodeURIComponent(suggestedName)}` : ""; + const r2 = await giteaFetch(`/api/v1/repos/${encodeURIComponent(GITEA_OWNER)}/${encodeURIComponent(GITEA_REPO)}/issues/${encodeURIComponent(String(index))}/assets${q}`, { + method: "POST", + body: fd + }); + + if (!r2.ok) { + const t = await r2.text().catch(() => ""); + return res.status(502).json({ ok: false, step: "upload_asset", status: r2.status, detail: t.slice(0, 2000), issueUrl }); + } + + const asset = await r2.json().catch(() => ({})); + return res.json({ ok: true, issueUrl, issueIndex: index, asset }); + } catch (e) { + return res.status(500).json({ ok: false, error: String(e?.message || e) }); + } +}); + +app.listen(8787, "0.0.0.0", () => { + console.log("issue-bridge listening on :8787"); +});