"""Organizations API endpoints""" from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from typing import List import secrets from app.database import get_db from app.models import Organization, Repository, PullRequest, ReviewTask from app.schemas.organization import ( OrganizationCreate, OrganizationUpdate, OrganizationResponse, OrganizationList, OrganizationScanResult ) from app.utils import encrypt_token, decrypt_token from app.config import settings from app.services.gitea import GiteaService from app.services.github import GitHubService from app.services.bitbucket import BitbucketService router = APIRouter() @router.get("", response_model=OrganizationList) async def get_organizations( skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db) ): """Get all organizations""" # Count total count_query = select(func.count(Organization.id)) count_result = await db.execute(count_query) total = count_result.scalar() # Get organizations query = select(Organization).order_by(Organization.created_at.desc()).offset(skip).limit(limit) result = await db.execute(query) organizations = result.scalars().all() # Add webhook URLs items = [] for org in organizations: webhook_url = f"http://{settings.host}:{settings.port}/api/webhooks/{org.platform.value}/org/{org.id}" items.append(OrganizationResponse( **org.__dict__, webhook_url=webhook_url )) return OrganizationList(items=items, total=total) @router.post("", response_model=OrganizationResponse) async def create_organization( organization: OrganizationCreate, db: AsyncSession = Depends(get_db) ): """Create a new organization""" # Generate webhook secret if not provided webhook_secret = organization.webhook_secret or secrets.token_urlsafe(32) # Encrypt API token (если указан) encrypted_token = encrypt_token(organization.api_token) if organization.api_token else None # Create organization db_organization = Organization( name=organization.name, platform=organization.platform, base_url=organization.base_url.rstrip('/'), api_token=encrypted_token, webhook_secret=webhook_secret, config=organization.config or {} ) db.add(db_organization) await db.commit() await db.refresh(db_organization) # Prepare response webhook_url = f"http://{settings.host}:{settings.port}/api/webhooks/{db_organization.platform.value}/org/{db_organization.id}" return OrganizationResponse( **db_organization.__dict__, webhook_url=webhook_url ) @router.get("/{organization_id}", response_model=OrganizationResponse) async def get_organization( organization_id: int, db: AsyncSession = Depends(get_db) ): """Get organization by ID""" result = await db.execute( select(Organization).where(Organization.id == organization_id) ) organization = result.scalar_one_or_none() if not organization: raise HTTPException(status_code=404, detail="Organization not found") webhook_url = f"http://{settings.host}:{settings.port}/api/webhooks/{organization.platform.value}/org/{organization.id}" return OrganizationResponse( **organization.__dict__, webhook_url=webhook_url ) @router.put("/{organization_id}", response_model=OrganizationResponse) async def update_organization( organization_id: int, organization_update: OrganizationUpdate, db: AsyncSession = Depends(get_db) ): """Update organization""" result = await db.execute( select(Organization).where(Organization.id == organization_id) ) organization = result.scalar_one_or_none() if not organization: raise HTTPException(status_code=404, detail="Organization not found") # Update fields update_data = organization_update.model_dump(exclude_unset=True) # Encrypt API token if provided and not empty if "api_token" in update_data and update_data["api_token"]: update_data["api_token"] = encrypt_token(update_data["api_token"]) elif "api_token" in update_data and not update_data["api_token"]: # If empty string provided, don't update token del update_data["api_token"] for field, value in update_data.items(): setattr(organization, field, value) await db.commit() await db.refresh(organization) webhook_url = f"http://{settings.host}:{settings.port}/api/webhooks/{organization.platform.value}/org/{organization.id}" return OrganizationResponse( **organization.__dict__, webhook_url=webhook_url ) @router.delete("/{organization_id}") async def delete_organization( organization_id: int, db: AsyncSession = Depends(get_db) ): """Delete organization""" result = await db.execute( select(Organization).where(Organization.id == organization_id) ) organization = result.scalar_one_or_none() if not organization: raise HTTPException(status_code=404, detail="Organization not found") await db.delete(organization) await db.commit() return {"message": "Organization deleted successfully"} @router.post("/{organization_id}/scan", response_model=OrganizationScanResult) async def scan_organization( organization_id: int, background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db) ): """Scan organization for repositories and PRs""" # Get organization result = await db.execute( select(Organization).where(Organization.id == organization_id) ) organization = result.scalar_one_or_none() if not organization: raise HTTPException(status_code=404, detail="Organization not found") # Get API token if organization.api_token: try: api_token = decrypt_token(organization.api_token) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) else: # Use master token platform = organization.platform.value.lower() if platform == "gitea": api_token = settings.master_gitea_token elif platform == "github": api_token = settings.master_github_token elif platform == "bitbucket": api_token = settings.master_bitbucket_token else: raise HTTPException(status_code=400, detail=f"Unsupported platform: {organization.platform}") if not api_token: raise HTTPException( status_code=400, detail=f"API token not set and master token for {platform} not configured" ) # Start scan scan_result = OrganizationScanResult( organization_id=organization_id, repositories_found=0, repositories_added=0, pull_requests_found=0, tasks_created=0, errors=[] ) try: if organization.platform.value == "gitea": await _scan_gitea_organization(organization, api_token, scan_result, db) elif organization.platform.value == "github": await _scan_github_organization(organization, api_token, scan_result, db) elif organization.platform.value == "bitbucket": await _scan_bitbucket_organization(organization, api_token, scan_result, db) else: raise HTTPException(status_code=400, detail=f"Unsupported platform: {organization.platform}") # Update last scan time from datetime import datetime organization.last_scan_at = datetime.utcnow() await db.commit() except Exception as e: scan_result.errors.append(str(e)) raise HTTPException(status_code=500, detail=f"Scan failed: {str(e)}") return scan_result async def _scan_gitea_organization( organization: Organization, api_token: str, scan_result: OrganizationScanResult, db: AsyncSession ): """Scan Gitea organization for repositories and PRs""" import httpx headers = { "Authorization": f"token {api_token}", "Content-Type": "application/json" } # Get all repositories in organization url = f"{organization.base_url}/api/v1/orgs/{organization.name}/repos" print(f"\n🔍 Сканирование организации {organization.name} на {organization.base_url}") async with httpx.AsyncClient() as client: response = await client.get(url, headers=headers) response.raise_for_status() repos = response.json() scan_result.repositories_found = len(repos) print(f" Найдено репозиториев: {len(repos)}") for repo_data in repos: repo_name = repo_data["name"] repo_owner = repo_data["owner"]["login"] repo_url = repo_data["html_url"] # Check if repository already exists existing_repo = await db.execute( select(Repository).where(Repository.url == repo_url) ) repository = existing_repo.scalar_one_or_none() if not repository: # Create new repository repository = Repository( name=f"{repo_owner}/{repo_name}", platform=organization.platform, url=repo_url, api_token=organization.api_token, # Use same token as org webhook_secret=organization.webhook_secret, config=organization.config ) db.add(repository) await db.flush() scan_result.repositories_added += 1 print(f" ✅ Добавлен репозиторий: {repo_owner}/{repo_name}") # Scan PRs in this repository await _scan_repository_prs( repository, organization.base_url, repo_owner, repo_name, api_token, scan_result, db ) await db.commit() async def _scan_github_organization( organization: Organization, api_token: str, scan_result: OrganizationScanResult, db: AsyncSession ): """Scan GitHub organization""" # TODO: Implement GitHub org scanning scan_result.errors.append("GitHub organization scanning not yet implemented") async def _scan_bitbucket_organization( organization: Organization, api_token: str, scan_result: OrganizationScanResult, db: AsyncSession ): """Scan Bitbucket organization""" # TODO: Implement Bitbucket org scanning scan_result.errors.append("Bitbucket organization scanning not yet implemented") async def _scan_repository_prs( repository: Repository, base_url: str, owner: str, repo: str, api_token: str, scan_result: OrganizationScanResult, db: AsyncSession ): """Scan repository for open PRs and create tasks""" import httpx headers = { "Authorization": f"token {api_token}", "Content-Type": "application/json" } # Get open PRs url = f"{base_url}/api/v1/repos/{owner}/{repo}/pulls?state=open" async with httpx.AsyncClient() as client: response = await client.get(url, headers=headers) response.raise_for_status() prs = response.json() for pr_data in prs: pr_number = pr_data["number"] scan_result.pull_requests_found += 1 # Check if PR already exists existing_pr = await db.execute( select(PullRequest).where( PullRequest.repository_id == repository.id, PullRequest.pr_number == pr_number ) ) pull_request = existing_pr.scalar_one_or_none() if not pull_request: # Create new PR pull_request = PullRequest( repository_id=repository.id, pr_number=pr_number, title=pr_data["title"], author=pr_data["user"]["login"], source_branch=pr_data["head"]["ref"], target_branch=pr_data["base"]["ref"], url=pr_data["html_url"], status="OPEN" ) db.add(pull_request) await db.flush() # Check if task already exists for this PR existing_task = await db.execute( select(ReviewTask).where( ReviewTask.pull_request_id == pull_request.id, ReviewTask.status.in_(["pending", "in_progress"]) ) ) task = existing_task.scalar_one_or_none() if not task: # Create review task task = ReviewTask( pull_request_id=pull_request.id, priority="normal" ) db.add(task) scan_result.tasks_created += 1 print(f" 📝 Создана задача для PR #{pr_number}: {pr_data['title']}")