from fastapi import APIRouter, HTTPException from pydantic import BaseModel from pathlib import Path import os from typing import Optional from modules.logger_tool import initialise_logger # Initialize logger logger = initialise_logger(__name__, os.getenv("LOG_LEVEL", "info"), os.getenv("LOG_PATH", "/logs")) # Create router router = APIRouter() # === CONFIG === SOLID_BASE_URL = os.getenv("SOLID_BASE_URL", "http://solid.classroomcopilot.test") SOLID_STORAGE_PATH = os.getenv("SOLID_STORAGE_PATH", "/data/users") # Path relative to Solid server data directory KEYCLOAK_ISSUER = os.getenv("KEYCLOAK_ISSUER", "http://keycloak.classroomcopilot.test/realms/ClassroomCopilot") # === DATA MODEL === class UserCreateRequest(BaseModel): username: str full_name: str email: Optional[str] = None # === UTILITIES === def create_profile_card(username: str, full_name: str, email: Optional[str] = None) -> str: webid = f"{SOLID_BASE_URL}/users/{username}/profile/card#me" profile_content = f"""@prefix foaf: . @prefix solid: . @prefix vcard: . <#me> a foaf:Person ; foaf:name "{full_name}" ; solid:oidcIssuer <{KEYCLOAK_ISSUER}> .""" if email: profile_content += f""" vcard:hasEmail .""" return profile_content def create_acl_file(username: str) -> str: return f"""@prefix acl: . @prefix foaf: . <#authorization> a acl:Authorization ; acl:agent <{SOLID_BASE_URL}/users/{username}/profile/card#me> ; acl:accessTo <./> ; acl:default <./> ; acl:mode acl:Read, acl:Write, acl:Control ; acl:agentClass foaf:Agent .""" def create_public_acl() -> str: return """@prefix acl: . @prefix foaf: . <#authorization> a acl:Authorization ; acl:agentClass foaf:Agent ; acl:accessTo <./> ; acl:default <./> ; acl:mode acl:Read .""" # === ENDPOINTS === @router.post("/provision") async def provision_user(data: UserCreateRequest): """ Create a new Solid pod for a user with their profile card and ACL files. """ try: # Create user directory structure user_base = Path(SOLID_STORAGE_PATH) / data.username profile_dir = user_base / "profile" public_dir = user_base / "public" private_dir = user_base / "private" # Create directories for dir_path in [profile_dir, public_dir, private_dir]: dir_path.mkdir(parents=True, exist_ok=True) # Create profile card profile_path = profile_dir / "card.ttl" profile_content = create_profile_card(data.username, data.full_name, data.email) profile_path.write_text(profile_content) # Create ACL files profile_acl_path = profile_dir / ".acl" profile_acl_content = create_acl_file(data.username) profile_acl_path.write_text(profile_acl_content) public_acl_path = public_dir / ".acl" public_acl_content = create_public_acl() public_acl_path.write_text(public_acl_content) private_acl_path = private_dir / ".acl" private_acl_content = create_acl_file(data.username) private_acl_path.write_text(private_acl_content) webid = f"{SOLID_BASE_URL}/users/{data.username}/profile/card#me" logger.info(f"Successfully provisioned Solid pod for user {data.username}") return { "message": "User pod created successfully", "webid": webid, "profile_path": str(profile_path), "pod_structure": { "profile": str(profile_dir), "public": str(public_dir), "private": str(private_dir) } } except Exception as e: logger.error(f"Error provisioning Solid pod for user {data.username}: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/status/{username}") async def check_pod_status(username: str): """ Check if a user's Solid pod exists and return its status. """ try: user_base = Path(SOLID_STORAGE_PATH) / username profile_path = user_base / "profile" / "card.ttl" if not profile_path.exists(): return { "exists": False, "message": "Pod not found" } return { "exists": True, "webid": f"{SOLID_BASE_URL}/users/{username}/profile/card#me", "profile_path": str(profile_path) } except Exception as e: logger.error(f"Error checking pod status for user {username}: {str(e)}") raise HTTPException(status_code=500, detail=str(e))