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"); });