Files
hive/backend/app/api/project_setup.py
anthonyrawlins 268214d971 Major WHOOSH system refactoring and feature enhancements
- Migrated from HIVE branding to WHOOSH across all components
- Enhanced backend API with new services: AI models, BZZZ integration, templates, members
- Added comprehensive testing suite with security, performance, and integration tests
- Improved frontend with new components for project setup, AI models, and team management
- Updated MCP server implementation with WHOOSH-specific tools and resources
- Enhanced deployment configurations with production-ready Docker setups
- Added comprehensive documentation and setup guides
- Implemented age encryption service and UCXL integration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-27 08:34:48 +10:00

598 lines
22 KiB
Python

"""
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)}")