| import base64 |
| import hashlib |
| import re |
| from typing import List, Dict, Any |
| import httpx |
| from github import Github |
| from cryptography.fernet import Fernet |
| from app.config import settings |
| from loguru import logger |
|
|
| |
| def _get_fernet_cipher() -> Fernet: |
| |
| key_hash = hashlib.sha256(settings.SECRET_KEY.encode()).digest() |
| fernet_key = base64.urlsafe_b64encode(key_hash) |
| return Fernet(fernet_key) |
|
|
| def encrypt_token(token: str) -> str: |
| cipher = _get_fernet_cipher() |
| return cipher.encrypt(token.encode()).decode() |
|
|
| def decrypt_token(encrypted_token: str) -> str: |
| cipher = _get_fernet_cipher() |
| return cipher.decrypt(encrypted_token.encode()).decode() |
|
|
| def get_github_auth_url(state: str) -> str: |
| redirect_uri = f"{settings.FRONTEND_URL}/api/auth/callback" |
| return ( |
| f"https://github.com/login/oauth/authorize" |
| f"?client_id={settings.GITHUB_CLIENT_ID}" |
| f"&redirect_uri={redirect_uri}" |
| f"&state={state}" |
| f"&scope=repo,read:org" |
| ) |
|
|
| async def exchange_code_for_token(code: str) -> dict: |
| url = "https://github.com/login/oauth/access_token" |
| headers = {"Accept": "application/json"} |
| data = { |
| "client_id": settings.GITHUB_CLIENT_ID, |
| "client_secret": settings.GITHUB_CLIENT_SECRET, |
| "code": code |
| } |
| |
| async with httpx.AsyncClient() as client: |
| response = await client.post(url, headers=headers, data=data) |
| response.raise_for_status() |
| return response.json() |
|
|
| async def get_github_user_info(access_token: str) -> dict: |
| url = "https://api.github.com/user" |
| headers = { |
| "Authorization": f"token {access_token}", |
| "Accept": "application/vnd.github.v3+json" |
| } |
| async with httpx.AsyncClient() as client: |
| response = await client.get(url, headers=headers) |
| response.raise_for_status() |
| return response.json() |
|
|
| def get_user_repos(access_token: str) -> List[Dict[str, Any]]: |
| try: |
| g = Github(access_token) |
| repos = [] |
| |
| for repo in g.get_user().get_repos(sort="updated", direction="desc"): |
| repos.append({ |
| "id": repo.id, |
| "name": repo.name, |
| "full_name": repo.full_name, |
| "html_url": repo.html_url, |
| "description": repo.description, |
| "language": repo.language, |
| "updated_at": repo.updated_at.isoformat() if repo.updated_at else "", |
| "private": repo.private |
| }) |
| return repos |
| except Exception as e: |
| logger.error(f"Failed to fetch repos from GitHub: {e}") |
| return [] |
|
|
| def get_repo_branches(access_token: str, repo_full_name: str) -> List[str]: |
| try: |
| g = Github(access_token) |
| repo = g.get_repo(repo_full_name) |
| return [b.name for b in repo.get_branches()] |
| except Exception as e: |
| logger.error(f"Failed to fetch branches for {repo_full_name}: {e}") |
| return [] |
|
|
| |
| ALLOWED_EXTENSIONS = { |
| ".py", ".js", ".ts", ".jsx", ".tsx", ".json", ".yaml", ".yml", |
| ".toml", ".html", ".css", ".sql", ".go", ".java", ".rb" |
| } |
|
|
| def get_repo_files(access_token: str, repo_full_name: str, branch: str) -> List[Dict[str, str]]: |
| try: |
| g = Github(access_token) |
| repo = g.get_repo(repo_full_name) |
| |
| |
| files = [] |
| total_size = 0 |
| max_size = 50 * 1024 * 1024 |
| |
| |
| git_ref = repo.get_git_ref(f"heads/{branch}") |
| tree = repo.get_git_tree(git_ref.object.sha, recursive=True) |
| |
| for element in tree.tree: |
| if element.type == "blob": |
| |
| import os |
| _, ext = os.path.splitext(element.path) |
| if ext.lower() in ALLOWED_EXTENSIONS: |
| |
| if any(p in element.path.split("/") for p in ["node_modules", "vendor", "dist", "build", ".git", "venv", ".venv"]): |
| continue |
| |
| |
| blob = repo.get_git_blob(element.sha) |
| content = base64.b64decode(blob.content).decode("utf-8", errors="ignore") |
| |
| |
| content = redact_env_secrets(content, element.path) |
| |
| files.append({ |
| "filename": element.path, |
| "content": content |
| }) |
| |
| total_size += len(content) |
| if len(files) >= 20 or total_size >= max_size: |
| break |
| |
| return files |
| except Exception as e: |
| logger.error(f"Failed to fetch files from repo {repo_full_name}: {e}") |
| return [] |
|
|
| def redact_env_secrets(content: str, filename: str) -> str: |
| |
| lines = content.splitlines() |
| redacted_lines = [] |
| |
| |
| is_env = filename.endswith(".env") or filename.endswith(".env.production") or filename.endswith(".env.local") |
| |
| |
| secret_key_re = re.compile( |
| r"(password|secret|key|token|auth|pwd|credential|private_key|api_key|access_key)\s*[:=]\s*['\"]?([a-zA-Z0-9_\-\.\=\+]{8,})['\"]?", |
| re.IGNORECASE |
| ) |
| |
| for line in lines: |
| if is_env: |
| |
| if "=" in line and not line.strip().startswith("#"): |
| key, val = line.split("=", 1) |
| redacted_lines.append(f"{key}=[REDACTED]") |
| else: |
| redacted_lines.append(line) |
| else: |
| |
| match = secret_key_re.search(line) |
| if match: |
| |
| val = match.group(2) |
| redacted_lines.append(line.replace(val, "[REDACTED]")) |
| else: |
| redacted_lines.append(line) |
| |
| return "\n".join(redacted_lines) |
|
|