121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
"""GitHub OAuth + repository browsing (mirrors /api/gitea shape for shared UI)."""
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
router = APIRouter(tags=["github"], prefix="/api/github")
|
|
|
|
|
|
def _require_github(request: Request):
|
|
"""Return a per-user GitHubAdapter or raise 401 (same contract as Gitea)."""
|
|
from app.auth.session import SessionManager
|
|
from app.db import get_conn
|
|
from app.services.github_adapter import GitHubAdapter
|
|
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
with get_conn() as conn:
|
|
row = conn.execute(
|
|
"SELECT access_token FROM user_oauth_tokens WHERE user_id=? AND provider='github' "
|
|
"AND access_token IS NOT NULL AND access_token != '' ORDER BY updated_at DESC LIMIT 1",
|
|
(user["id"],),
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=401, detail="GitHub account not linked. Go to Settings → Integrations to connect.")
|
|
return GitHubAdapter(row["access_token"])
|
|
|
|
|
|
@router.get("/status")
|
|
def github_status(request: Request):
|
|
"""Check if the current user has GitHub linked."""
|
|
from app.auth.session import SessionManager
|
|
from app.db import get_conn
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
if not user:
|
|
return {"linked": False}
|
|
with get_conn() as conn:
|
|
row = conn.execute(
|
|
"SELECT access_token FROM user_oauth_tokens WHERE user_id=? AND provider='github' AND access_token IS NOT NULL AND access_token != ''",
|
|
(user["id"],)
|
|
).fetchone()
|
|
return {"linked": row is not None}
|
|
|
|
|
|
@router.delete("/disconnect")
|
|
def disconnect_github(request: Request):
|
|
"""Remove all GitHub OAuth tokens for the current user."""
|
|
from app.auth.session import SessionManager
|
|
from app.db import get_conn
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
if not user:
|
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
|
with get_conn() as conn:
|
|
conn.execute("DELETE FROM user_oauth_tokens WHERE user_id=? AND provider='github'", (user["id"],))
|
|
conn.commit()
|
|
return {"status": "ok"}
|
|
|
|
|
|
# ── Repo tree (same shape as /api/gitea/projects/{owner}/{repo}/tree) ──
|
|
@router.get("/projects/{owner}/{repo}/tree")
|
|
async def github_repo_tree(request: Request, owner: str, repo: str, path: str = ""):
|
|
"""List one level of a GitHub repo (folders first client-side)."""
|
|
adapter = _require_github(request)
|
|
try:
|
|
tree = await adapter.get_repo_tree(owner, repo)
|
|
prefix = path.strip("/").strip()
|
|
items = []
|
|
seen_dirs: set[str] = set()
|
|
for entry in tree:
|
|
p = (entry.get("path") or "").strip("/")
|
|
if not p:
|
|
continue
|
|
if prefix:
|
|
if p != prefix and not p.startswith(prefix + "/"):
|
|
continue
|
|
rel = p[len(prefix) + 1:] if p != prefix else ""
|
|
else:
|
|
rel = p
|
|
if not rel:
|
|
continue
|
|
if "/" in rel:
|
|
top = rel.split("/")[0]
|
|
dir_path = f"{prefix}/{top}".strip("/") if prefix else top
|
|
if dir_path not in seen_dirs:
|
|
seen_dirs.add(dir_path)
|
|
items.append({"name": top, "path": dir_path, "type": "folder", "size": 0, "sha": ""})
|
|
else:
|
|
is_dir = entry.get("type") == "tree"
|
|
items.append({
|
|
"name": rel,
|
|
"path": p,
|
|
"type": "folder" if is_dir else "file",
|
|
"size": entry.get("size", 0),
|
|
"sha": entry.get("sha", ""),
|
|
})
|
|
# De-duplicate (recursive tree can list both tree + blob entries)
|
|
dedup: dict[str, dict] = {}
|
|
for it in items:
|
|
key = it["path"]
|
|
if key not in dedup or (dedup[key]["type"] == "file" and it["type"] == "folder"):
|
|
dedup[key] = it
|
|
return {"tree": sorted(dedup.values(), key=lambda x: (x["type"] != "folder", x["name"].lower()))}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
return JSONResponse({"error": str(e)}, status_code=502)
|
|
|
|
|
|
@router.get("/projects/{owner}/{repo}/file")
|
|
async def github_get_file(request: Request, owner: str, repo: str, path: str = ""):
|
|
"""Get decoded file content (same shape as Gitea endpoint)."""
|
|
adapter = _require_github(request)
|
|
if not path:
|
|
return JSONResponse({"error": "Path required"}, status_code=400)
|
|
try:
|
|
content = await adapter.get_file_content(owner, repo, path)
|
|
return {"content": content, "path": path}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
return JSONResponse({"error": str(e)}, status_code=502)
|