""" Project Setup API for WHOOSH - Comprehensive project creation with GITEA integration. """ from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks from pydantic import BaseModel, Field from typing import List, Dict, Optional, Any from datetime import datetime import asyncio from app.services.gitea_service import GiteaService from app.services.project_service import ProjectService from app.services.age_service import AgeService from app.services.member_service import MemberService from app.models.project import Project router = APIRouter(prefix="/api/project-setup", tags=["project-setup"]) # Pydantic models for request/response validation class ProjectTemplateConfig(BaseModel): template_id: str name: str description: str icon: str features: List[str] starter_files: Dict[str, Any] = {} class AgeKeyConfig(BaseModel): generate_new_key: bool = True master_key_passphrase: Optional[str] = None key_backup_location: Optional[str] = None key_recovery_questions: Optional[List[Dict[str, str]]] = None class GitConfig(BaseModel): repo_type: str = Field(..., pattern="^(new|existing|import)$") repo_name: Optional[str] = None git_url: Optional[str] = None git_owner: Optional[str] = None git_branch: str = "main" auto_initialize: bool = True add_gitignore: bool = True add_readme: bool = True license_type: Optional[str] = "MIT" private: bool = False class ProjectMember(BaseModel): email: str role: str = Field(..., pattern="^(owner|maintainer|developer|viewer)$") age_public_key: Optional[str] = None invite_message: Optional[str] = None class MemberConfig(BaseModel): initial_members: List[ProjectMember] = [] role_permissions: Dict[str, List[str]] = { "owner": ["all"], "maintainer": ["read", "write", "deploy"], "developer": ["read", "write"], "viewer": ["read"] } class BzzzSyncPreferences(BaseModel): real_time: bool = True conflict_resolution: str = Field("manual", pattern="^(manual|automatic|priority)$") backup_frequency: str = Field("hourly", pattern="^(real-time|hourly|daily)$") class BzzzConfig(BaseModel): enable_bzzz: bool = False network_peers: Optional[List[str]] = None auto_discovery: bool = True task_coordination: bool = True ai_agent_access: bool = False sync_preferences: BzzzSyncPreferences = BzzzSyncPreferences() class AdvancedConfig(BaseModel): project_visibility: str = Field("private", pattern="^(private|internal|public)$") security_level: str = Field("standard", pattern="^(standard|high|maximum)$") backup_enabled: bool = True monitoring_enabled: bool = True ci_cd_enabled: bool = False custom_workflows: Optional[List[str]] = None class ProjectSetupRequest(BaseModel): # Basic Information name: str = Field(..., min_length=1, max_length=100) description: Optional[str] = Field(None, max_length=500) tags: Optional[List[str]] = None template_id: Optional[str] = None # Configuration sections age_config: AgeKeyConfig = AgeKeyConfig() git_config: GitConfig member_config: MemberConfig = MemberConfig() bzzz_config: BzzzConfig = BzzzConfig() advanced_config: AdvancedConfig = AdvancedConfig() class ProjectSetupStatus(BaseModel): step: str status: str = Field(..., pattern="^(pending|in_progress|completed|failed)$") message: str details: Optional[Dict[str, Any]] = None class ProjectSetupResponse(BaseModel): project_id: str status: str progress: List[ProjectSetupStatus] repository: Optional[Dict[str, Any]] = None age_keys: Optional[Dict[str, str]] = None member_invitations: Optional[List[Dict[str, str]]] = None next_steps: List[str] # Project templates configuration PROJECT_TEMPLATES = { "full-stack": ProjectTemplateConfig( template_id="full-stack", name="Full-Stack Application", description="Complete web application with frontend, backend, and database", icon="🌐", features=["React/Vue", "Node.js/Python", "Database", "CI/CD"], starter_files={ "frontend": {"package.json": {}, "src/index.js": ""}, "backend": {"requirements.txt": "", "app.py": ""}, "docker-compose.yml": {}, ".github/workflows/ci.yml": {} } ), "ai-research": ProjectTemplateConfig( template_id="ai-research", name="AI Research Project", description="Machine learning and AI development workspace", icon="🤖", features=["Jupyter", "Python", "GPU Support", "Data Pipeline"], starter_files={ "notebooks": {}, "src": {}, "data": {}, "models": {}, "requirements.txt": "", "environment.yml": {} } ), "documentation": ProjectTemplateConfig( template_id="documentation", name="Documentation Site", description="Technical documentation and knowledge base", icon="📚", features=["Markdown", "Static Site", "Search", "Multi-language"], starter_files={ "docs": {}, "mkdocs.yml": {}, ".readthedocs.yml": {} } ), "mobile-app": ProjectTemplateConfig( template_id="mobile-app", name="Mobile Application", description="Cross-platform mobile app development", icon="📱", features=["React Native", "Flutter", "Push Notifications", "App Store"], starter_files={ "src": {}, "assets": {}, "package.json": {}, "app.json": {} } ), "data-science": ProjectTemplateConfig( template_id="data-science", name="Data Science", description="Data analysis and visualization project", icon="📊", features=["Python", "R", "Visualization", "Reports"], starter_files={ "data": {}, "notebooks": {}, "src": {}, "reports": {}, "requirements.txt": {} } ), "empty": ProjectTemplateConfig( template_id="empty", name="Empty Project", description="Start from scratch with minimal setup", icon="📁", features=["Git", "Basic Structure", "README"], starter_files={ "README.md": "", ".gitignore": "" } ) } def get_gitea_service(): """Dependency injection for GITEA service.""" return GiteaService() def get_project_service(): """Dependency injection for project service.""" return ProjectService() def get_age_service(): """Dependency injection for Age service.""" return AgeService() def get_member_service(): """Dependency injection for Member service.""" return MemberService() @router.get("/templates") async def get_project_templates() -> Dict[str, Any]: """Get available project templates.""" return { "templates": list(PROJECT_TEMPLATES.values()), "count": len(PROJECT_TEMPLATES) } @router.get("/templates/{template_id}") async def get_project_template(template_id: str) -> ProjectTemplateConfig: """Get specific project template details.""" if template_id not in PROJECT_TEMPLATES: raise HTTPException(status_code=404, detail="Template not found") return PROJECT_TEMPLATES[template_id] @router.post("/validate-repository") async def validate_repository( owner: str, repo_name: str, gitea_service: GiteaService = Depends(get_gitea_service) ) -> Dict[str, Any]: """Validate repository access and BZZZ readiness.""" return gitea_service.validate_repository_access(owner, repo_name) @router.post("/create") async def create_project( request: ProjectSetupRequest, background_tasks: BackgroundTasks, gitea_service: GiteaService = Depends(get_gitea_service), project_service: ProjectService = Depends(get_project_service), age_service: AgeService = Depends(get_age_service), member_service: MemberService = Depends(get_member_service) ) -> ProjectSetupResponse: """Create a new project with comprehensive setup.""" project_id = request.name.lower().replace(" ", "-").replace("_", "-") # Initialize setup progress tracking progress = [ ProjectSetupStatus(step="validation", status="pending", message="Validating project configuration"), ProjectSetupStatus(step="age_keys", status="pending", message="Setting up Age master keys"), ProjectSetupStatus(step="git_repository", status="pending", message="Creating Git repository"), ProjectSetupStatus(step="bzzz_setup", status="pending", message="Configuring BZZZ integration"), ProjectSetupStatus(step="member_invites", status="pending", message="Sending member invitations"), ProjectSetupStatus(step="finalization", status="pending", message="Finalizing project setup") ] try: # Step 1: Validation progress[0].status = "in_progress" progress[0].message = "Validating project name and configuration" # Check if project name is available existing_project = project_service.get_project_by_id(project_id) if existing_project: progress[0].status = "failed" progress[0].message = f"Project '{project_id}' already exists" raise HTTPException(status_code=409, detail="Project name already exists") progress[0].status = "completed" progress[0].message = "Validation completed" # Step 2: Age Keys Setup progress[1].status = "in_progress" age_keys = None if request.age_config.generate_new_key: progress[1].message = "Generating Age master key pair" age_keys = await generate_age_keys(project_id, request.age_config, age_service) if age_keys: progress[1].status = "completed" progress[1].message = f"Age master keys generated (Key ID: {age_keys['key_id']})" progress[1].details = { "key_id": age_keys["key_id"], "public_key": age_keys["public_key"], "encrypted": age_keys["encrypted"], "backup_created": age_keys.get("backup_created", False) } else: progress[1].status = "failed" progress[1].message = "Age key generation failed" raise HTTPException(status_code=500, detail="Age key generation failed") else: progress[1].status = "completed" progress[1].message = "Skipped Age key generation" # Step 3: Git Repository Setup progress[2].status = "in_progress" repository_info = None if request.git_config.repo_type == "new": progress[2].message = "Creating new Git repository" # Prepare repository data repo_data = { "name": request.git_config.repo_name or project_id, "description": request.description or f"WHOOSH project: {request.name}", "owner": request.git_config.git_owner or "whoosh", "private": request.git_config.private } repository_info = gitea_service.setup_project_repository(repo_data) if repository_info: progress[2].status = "completed" progress[2].message = f"Repository created: {repository_info['gitea_url']}" progress[2].details = repository_info else: progress[2].status = "failed" progress[2].message = "Failed to create Git repository" raise HTTPException(status_code=500, detail="Repository creation failed") elif request.git_config.repo_type == "existing": progress[2].message = "Validating existing repository" validation = gitea_service.validate_repository_access( request.git_config.git_owner, request.git_config.repo_name ) if validation["accessible"]: repository_info = { "repository": validation["repository"], "gitea_url": f"{gitea_service.gitea_base_url}/{request.git_config.git_owner}/{request.git_config.repo_name}", "bzzz_enabled": validation["bzzz_ready"] } progress[2].status = "completed" progress[2].message = "Existing repository validated" else: progress[2].status = "failed" progress[2].message = f"Repository validation failed: {validation.get('error', 'Unknown error')}" raise HTTPException(status_code=400, detail="Repository validation failed") # Step 4: BZZZ Setup progress[3].status = "in_progress" if request.bzzz_config.enable_bzzz: progress[3].message = "Configuring BZZZ task coordination" # Ensure BZZZ labels are set up if repository_info and request.git_config.repo_type == "new": # Labels already set up during repository creation pass elif repository_info: # Set up labels for existing repository gitea_service._setup_bzzz_labels( request.git_config.git_owner, request.git_config.repo_name ) progress[3].status = "completed" progress[3].message = "BZZZ integration configured" else: progress[3].status = "completed" progress[3].message = "BZZZ integration disabled" # Step 5: Member Invitations progress[4].status = "in_progress" member_invitations = [] if request.member_config.initial_members: progress[4].message = f"Sending invitations to {len(request.member_config.initial_members)} members" # Get Age public key for invitations age_public_key = None if age_keys: age_public_key = age_keys.get("public_key") for member in request.member_config.initial_members: invitation = await send_member_invitation( project_id, member, repository_info, member_service, request.name, age_public_key ) member_invitations.append(invitation) progress[4].status = "completed" progress[4].message = f"Sent {len(member_invitations)} member invitations" else: progress[4].status = "completed" progress[4].message = "No member invitations to send" # Step 6: Finalization progress[5].status = "in_progress" progress[5].message = "Creating project record" # Create project in database project_data = { "name": request.name, "description": request.description, "tags": request.tags, "git_url": repository_info.get("gitea_url") if repository_info else None, "git_owner": request.git_config.git_owner, "git_repository": request.git_config.repo_name or project_id, "git_branch": request.git_config.git_branch, "bzzz_enabled": request.bzzz_config.enable_bzzz, "private_repo": request.git_config.private, "metadata": { "template_id": request.template_id, "security_level": request.advanced_config.security_level, "created_via": "whoosh_setup_wizard", "age_keys_enabled": request.age_config.generate_new_key, "member_count": len(request.member_config.initial_members) } } created_project = project_service.create_project(project_data) progress[5].status = "completed" progress[5].message = "Project setup completed successfully" # Generate next steps next_steps = [] if repository_info: next_steps.append(f"Clone repository: git clone {repository_info['repository']['clone_url']}") if request.bzzz_config.enable_bzzz: next_steps.append("Create BZZZ tasks by adding issues with 'bzzz-task' label") if member_invitations: next_steps.append("Follow up on member invitation responses") next_steps.append("Configure project settings and workflows") return ProjectSetupResponse( project_id=project_id, status="completed", progress=progress, repository=repository_info, age_keys=age_keys, member_invitations=member_invitations, next_steps=next_steps ) except HTTPException: raise except Exception as e: # Update progress with error for step in progress: if step.status == "in_progress": step.status = "failed" step.message = f"Error: {str(e)}" break raise HTTPException(status_code=500, detail=f"Project setup failed: {str(e)}") async def generate_age_keys(project_id: str, age_config: AgeKeyConfig, age_service: AgeService) -> Optional[Dict[str, str]]: """Generate Age master key pair using the Age service.""" try: result = age_service.generate_master_key_pair( project_id=project_id, passphrase=age_config.master_key_passphrase ) # Create backup if location specified if age_config.key_backup_location: backup_success = age_service.backup_key( project_id=project_id, key_id=result["key_id"], backup_location=age_config.key_backup_location ) result["backup_created"] = backup_success # Generate recovery phrase recovery_phrase = age_service.generate_recovery_phrase( project_id=project_id, key_id=result["key_id"] ) result["recovery_phrase"] = recovery_phrase return { "key_id": result["key_id"], "public_key": result["public_key"], "private_key_stored": result["private_key_stored"], "backup_location": result["backup_location"], "recovery_phrase": recovery_phrase, "encrypted": result["encrypted"] } except Exception as e: print(f"Age key generation failed: {e}") return None async def send_member_invitation(project_id: str, member: ProjectMember, repository_info: Optional[Dict], member_service: MemberService, project_name: str, age_public_key: Optional[str] = None) -> Dict[str, str]: """Send invitation to project member using the member service.""" try: # Generate invitation invitation_result = member_service.generate_member_invitation( project_id=project_id, member_email=member.email, role=member.role, inviter_name="WHOOSH Project Setup", project_name=project_name, custom_message=member.invite_message ) if not invitation_result.get("created"): return { "email": member.email, "role": member.role, "invitation_sent": False, "error": invitation_result.get("error", "Failed to create invitation") } # Send email invitation email_sent = member_service.send_email_invitation(invitation_result, age_public_key) return { "email": member.email, "role": member.role, "invitation_sent": email_sent, "invitation_id": invitation_result["invitation_id"], "invitation_url": invitation_result["invitation_url"], "expires_at": invitation_result["expires_at"] } except Exception as e: return { "email": member.email, "role": member.role, "invitation_sent": False, "error": str(e) } # === Age Key Management Endpoints === @router.get("/age-keys/{project_id}") async def get_project_age_keys( project_id: str, age_service: AgeService = Depends(get_age_service) ) -> Dict[str, Any]: """Get Age keys for a project.""" try: keys = age_service.list_project_keys(project_id) return { "project_id": project_id, "keys": keys, "count": len(keys) } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to retrieve Age keys: {str(e)}") @router.post("/age-keys/{project_id}/validate") async def validate_age_key_access( project_id: str, key_id: str, age_service: AgeService = Depends(get_age_service) ) -> Dict[str, Any]: """Validate access to an Age key.""" try: validation = age_service.validate_key_access(project_id, key_id) return validation except Exception as e: raise HTTPException(status_code=500, detail=f"Key validation failed: {str(e)}") @router.post("/age-keys/{project_id}/backup") async def backup_age_key( project_id: str, key_id: str, backup_location: str, age_service: AgeService = Depends(get_age_service) ) -> Dict[str, Any]: """Create a backup of an Age key.""" try: success = age_service.backup_key(project_id, key_id, backup_location) return { "project_id": project_id, "key_id": key_id, "backup_location": backup_location, "success": success } except Exception as e: raise HTTPException(status_code=500, detail=f"Key backup failed: {str(e)}") @router.post("/age-keys/{project_id}/encrypt") async def encrypt_data_with_age( project_id: str, data: str, recipients: List[str], age_service: AgeService = Depends(get_age_service) ) -> Dict[str, Any]: """Encrypt data using Age with specified recipients.""" try: encrypted_data = age_service.encrypt_data(data, recipients) return { "project_id": project_id, "encrypted_data": encrypted_data, "recipients": recipients } except Exception as e: raise HTTPException(status_code=500, detail=f"Data encryption failed: {str(e)}")