api/routers/solid/pod_provisioner.py
2025-07-11 13:52:19 +00:00

143 lines
4.7 KiB
Python

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: <http://xmlns.com/foaf/0.1/> .
@prefix solid: <http://www.w3.org/ns/solid/terms#> .
@prefix vcard: <http://www.w3.org/2006/vcard/ns#> .
<#me> a foaf:Person ;
foaf:name "{full_name}" ;
solid:oidcIssuer <{KEYCLOAK_ISSUER}> ."""
if email:
profile_content += f"""
vcard:hasEmail <mailto:{email}> ."""
return profile_content
def create_acl_file(username: str) -> str:
return f"""@prefix acl: <http://www.w3.org/ns/acl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
<#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: <http://www.w3.org/ns/acl#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
<#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))