From 184a48612161ad3fb21312ee936c358d1c2ab6a8 Mon Sep 17 00:00:00 2001 From: luna Date: Sat, 25 Jul 2026 14:02:59 +0000 Subject: [PATCH] fix(v2): authenticate Gitea clones without URL credentials --- backend/src/services/deploy.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/backend/src/services/deploy.ts b/backend/src/services/deploy.ts index dce8f566..d8c2d8e7 100644 --- a/backend/src/services/deploy.ts +++ b/backend/src/services/deploy.ts @@ -244,18 +244,23 @@ async function firstDeploy( const giteaPat = user.giteaPAT ? decrypt(user.giteaPAT) : ""; const parsedUrl = new URL(cloneUrl.startsWith("http") ? cloneUrl : `https://${cloneUrl}`); - parsedUrl.username = encodeURIComponent(user.giteaUsername || ""); - parsedUrl.password = encodeURIComponent(giteaPat); - const authCloneUrl = parsedUrl.toString(); + parsedUrl.username = ""; + parsedUrl.password = ""; + const cleanCloneUrl = parsedUrl.toString(); + const authHeader = Buffer.from(`${user.giteaUsername || ""}:${giteaPat}`).toString("base64"); - // Log a masked version so PAT is not exposed in preview logs - const maskedUrl = `${parsedUrl.protocol}//${parsedUrl.username}:****@${parsedUrl.hostname}${parsedUrl.port ? ":" + parsedUrl.port : ""}${parsedUrl.pathname}`; - await appendLog(previewId, `$ git clone '${maskedUrl}' /opt/app\n`); - const cloneResult = await sshSession.exec(`git clone '${authCloneUrl}' /opt/app`); + // Keep the PAT out of both preview logs and the URL. Gitea deployments can + // reject userinfo URLs, while Git's per-command HTTP header works for both + // private repositories and reverse proxies. + await appendLog(previewId, `$ git clone '${cleanCloneUrl}' /opt/app\n`); + const cloneCommand = giteaPat + ? `git -c http.extraHeader='Authorization: Basic ${authHeader}' clone '${cleanCloneUrl}' /opt/app` + : `git clone '${cleanCloneUrl}' /opt/app`; + const cloneResult = await sshSession.exec(cloneCommand); if (cloneResult.stdout) await appendLog(previewId, cloneResult.stdout); if (cloneResult.stderr) { // Mask PAT in stderr output too - const maskedStderr = cloneResult.stderr.replace(encodeURIComponent(giteaPat), "****").replace(giteaPat, "****"); + const maskedStderr = cloneResult.stderr.replace(giteaPat, "****").replace(authHeader, "****"); await appendLog(previewId, maskedStderr); } if (cloneResult.code !== 0) throw new Error(`git clone failed with exit code ${cloneResult.code}`);