diff --git a/.env.example b/.env.example index f665ee9..8dd8dbb 100644 --- a/.env.example +++ b/.env.example @@ -42,8 +42,8 @@ SOURCEHUB_MALWARE_SCAN=disabled # SOURCEHUB_CLAMD_PORT=3310 # ---------- Gitea ---------- -# SOURCEHUB_GITEA_BASE_URL=https://git.example.com -# SOURCEHUB_GITEA_TOKEN= +SOURCEHUB_GITEA_BASE_URL=https://git.nexpa-ai.com +SOURCEHUB_GITEA_TOKEN=CHANGE_ME # ---------- Deployment executor ---------- SOURCEHUB_DEPLOY_MODE=disabled diff --git a/.env.production.example b/.env.production.example index c4f15a1..12a55e0 100644 --- a/.env.production.example +++ b/.env.production.example @@ -38,7 +38,7 @@ SOURCEHUB_ALLOWED_HOSTS=sourcehub.nexpa-ai.com,localhost,127.0.0.1 SOURCEHUB_ENABLE_HSTS=true # Gitea (deploy/gitea — host 3200 / SSH 2222) -SOURCEHUB_GITEA_BASE_URL=http://127.0.0.1:3200 +SOURCEHUB_GITEA_BASE_URL=https://git.nexpa-ai.com SOURCEHUB_GITEA_TOKEN=CHANGE_ME # Deployment executor: keep disabled until CI/CD target is explicitly registered diff --git a/app.py b/app.py index 6ac9610..59ee275 100644 --- a/app.py +++ b/app.py @@ -497,6 +497,9 @@ def bind_git(project_code: str, body: RepoBindIn, request: Request, x_role: Opti conn.execute(update(repo_bindings).where(repo_bindings.c.project_code == project_code).values( owner=body.owner.strip(), repo=body.repo.strip(), branch=body.branch.strip(), updated_at=now_iso(), updated_by=a.role, )) + conn.execute(update(projects).where(projects.c.code == project_code).values( + branch=body.branch.strip(), + )) log(conn, a.role, "Git", "저장소 바인딩 변경", p["name"], {"owner": body.owner, "repo": body.repo, "branch": body.branch}) return git_status(project_code, request, x_role) diff --git a/deploy/apache-gitea.conf b/deploy/apache-gitea.conf new file mode 100644 index 0000000..116c7a2 --- /dev/null +++ b/deploy/apache-gitea.conf @@ -0,0 +1,22 @@ +# Apache Reverse Proxy Configuration for Gitea +# Copy to /etc/apache2/sites-available/gitea.conf +# Required modules: sudo a2enmod proxy proxy_http ssl headers rewrite + + + ServerName git.nexpa-ai.com + + ProxyPreserveHost On + ProxyRequests Off + + ProxyPass / http://127.0.0.1:3200/ + ProxyPassReverse / http://127.0.0.1:3200/ + + RequestHeader set X-Forwarded-Proto "http" + + # Allow large git push uploads (500MB) + LimitRequestBody 524288000 + + +# Note: Once SSL certificate is issued via Certbot: +# Run: sudo certbot --apache -d git.nexpa-ai.com +# Certbot will automatically add the SSL (*:443) VirtualHost and HTTPS redirect. diff --git a/deploy/apache-sourcehub.conf b/deploy/apache-sourcehub.conf new file mode 100644 index 0000000..f8214a5 --- /dev/null +++ b/deploy/apache-sourcehub.conf @@ -0,0 +1,21 @@ +# Apache Reverse Proxy Configuration for NEXPA SourceHub 3.8 +# Copy to /etc/apache2/sites-available/sourcehub.conf +# Required modules: sudo a2enmod proxy proxy_http ssl headers rewrite + + + ServerName sourcehub.nexpa-ai.com + + ProxyPreserveHost On + ProxyRequests Off + + ProxyPass / http://127.0.0.1:18000/ + ProxyPassReverse / http://127.0.0.1:18000/ + + RequestHeader set X-Forwarded-Proto "http" + + LimitRequestBody 524288000 + + +# Note: Once SSL certificate is issued via Certbot: +# Run: sudo certbot --apache -d sourcehub.nexpa-ai.com +# Certbot will automatically add the SSL (*:443) VirtualHost and HTTPS redirect. diff --git a/docker-compose.ops.yml b/docker-compose.ops.yml index 06a806f..a6cfa82 100644 --- a/docker-compose.ops.yml +++ b/docker-compose.ops.yml @@ -26,7 +26,7 @@ services: sourcehub: build: . ports: - - "8000:8000" + - "127.0.0.1:18000:8000" depends_on: - postgres - minio diff --git a/docs/NEXPA_SourceHub_기능개선_개발요청서.docx b/docs/NEXPA_SourceHub_기능개선_개발요청서.docx new file mode 100644 index 0000000..d77b2cc Binary files /dev/null and b/docs/NEXPA_SourceHub_기능개선_개발요청서.docx differ diff --git a/sourcehub/config.py b/sourcehub/config.py index bfb8e41..2b73b1a 100644 --- a/sourcehub/config.py +++ b/sourcehub/config.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import sys from dataclasses import dataclass from pathlib import Path @@ -55,6 +56,21 @@ class Settings: @classmethod def from_env(cls, base_dir: Path | None = None) -> "Settings": base = (base_dir or Path(__file__).resolve().parents[1]).resolve() + env_file = base / ".env" + if env_file.is_file() and "pytest" not in sys.modules: + try: + with open(env_file, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + k, v = k.strip(), v.strip().strip("'\"") + if k not in os.environ: + os.environ[k] = v + except Exception: + pass + data_dir = Path(os.getenv("SOURCEHUB_DATA_DIR", str(base / "data"))).expanduser() if not data_dir.is_absolute(): data_dir = (base / data_dir).resolve() diff --git a/static/index.html b/static/index.html index 6e4eb1c..ee695d8 100644 --- a/static/index.html +++ b/static/index.html @@ -75,7 +75,7 @@ function renderSystem(){const cards=[["DATABASE",system.database.kind,system.dat function populateProjects(){const opts=projects.map(p=>``).join("");$("#uploadProject").innerHTML=opts;$("#projectFilter").innerHTML=''+opts;$("#receiver").innerHTML=Object.entries(roles).map(([k,v])=>``).join("");renderProjects()} async function loadAll(){try{system=await api("/api/system/status");$("#userPill").textContent=roles[currentRole];await loadDashboard();await loadTransfers();await loadAudit()}catch(e){toast(parseErr(e))}} async function loadDashboard(){const [b,t]=await Promise.all([api("/api/baselines",{headers:{"X-Role":currentRole}}),api("/api/transfers",{headers:{"X-Role":currentRole}})]);$("#summary").innerHTML=`
기준 프로젝트${projects.length}현재 운영 기준
진행 전달${t.filter(x=>x.state!=="APPROVED").length}승인 흐름 포함
승인 완료${t.filter(x=>x.state==="APPROVED").length}immutable records
수정 요청${t.filter(x=>x.state==="CHANGES_REQUESTED").length}새 Revision 필요
`;$("#baselines").innerHTML=b.map(x=>`
${x.kind}
${x.project_name} ${x.version}${x.package_name}
${x.state}
`).join("")} -function renderProjects(){$("#projectGrid").innerHTML=projects.map(p=>{const b=p.repo_binding||{};return `

${p.name}

현재 기준 ${p.current_version}

v${p.current_version}
BRANCH${p.branch||"-"}
OWNER${p.owner}
GIT${b.repo?b.owner+"/"+b.repo:"미연결"}
`}).join("")} +function renderProjects(){$("#projectGrid").innerHTML=projects.map(p=>{const b=p.repo_binding||{};return `

${p.name}

현재 기준 ${p.current_version}

v${p.current_version}
BRANCH${b.branch||p.branch||"-"}
담당${p.owner}
GIT${b.repo?b.owner+"/"+b.repo:"미연결"}
`}).join("")} async function checkGit(code){try{const r=await api(`/api/projects/${code}/git/status`,{headers:{"X-Role":currentRole}});toast(r.status.message+(r.status.commit?` · ${r.status.commit.slice(0,8)}`:""))}catch(e){toast(parseErr(e))}} function openGit(code){const p=projects.find(x=>x.code===code),b=p.repo_binding||{};const f=$("#gitForm");f.project_code.value=code;f.owner.value=b.owner||"";f.repo.value=b.repo||"";f.branch.value=b.branch||p.branch||"";openModal("gitModal")} $("#gitForm").onsubmit=async e=>{e.preventDefault();const fd=new FormData(e.target),code=fd.get("project_code");try{await api(`/api/projects/${code}/git/bind`,{method:"POST",headers:headers(),body:JSON.stringify({owner:fd.get("owner"),repo:fd.get("repo"),branch:fd.get("branch")})});closeModal("gitModal");projects=await api("/api/projects",{headers:{"X-Role":currentRole}});renderProjects();toast("Git 바인딩이 저장되었습니다.")}catch(err){toast(parseErr(err))}};