from fastapi import FastAPI, Request, HTTPException from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from jinja2 import Environment, FileSystemLoader import httpx, os, json from k8s import create_service, delete_service, list_user_services from minio_client import ensure_bucket app = FastAPI() jinja_env = Environment(loader=FileSystemLoader("/app/code/templates")) KEYCLOAK_URL = "https://keycloak.lab.leadwire.dev/auth/realms/datalab" CLIENT_ID = "datalab-lite" CLIENT_SECRET= os.getenv("CLIENT_SECRET", "") REDIRECT_URI = "https://datalablite.lab.leadwire.dev/callback" SERVICES = { "jupyter": { "name": "JupyterLab", "image": "jupyter/scipy-notebook:latest", "port": 8888, "icon": "🔬", "env": {"JUPYTER_ENABLE_LAB": "yes", "JUPYTER_TOKEN": "{token}"} }, "rstudio": { "name": "RStudio", "image": "rocker/rstudio:latest", "port": 8787, "icon": "📊", "env": {"PASSWORD": "{token}", "USER": "rstudio"} }, "vscode": { "name": "VSCode Server", "image": "codercom/code-server:latest", "port": 8080, "icon": "💻", "env": {"PASSWORD": "{token}"} } } def render(template_name, **ctx): t = jinja_env.get_template(template_name) return HTMLResponse(t.render(**ctx)) @app.get("/", response_class=HTMLResponse) async def home(request: Request): username = request.cookies.get("username") if not username: return RedirectResponse("/login") services = list_user_services(username) return render("index.html", username=username, services=services, catalog=SERVICES) @app.get("/login") async def login(): url = ( f"{KEYCLOAK_URL}/protocol/openid-connect/auth" f"?client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URI}" f"&response_type=code" f"&scope=openid profile email" ) return RedirectResponse(url) @app.get("/callback") async def callback(code: str, request: Request): async with httpx.AsyncClient(verify=False) as client: resp = await client.post( f"{KEYCLOAK_URL}/protocol/openid-connect/token", data={ "grant_type": "authorization_code", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "code": code, "redirect_uri": REDIRECT_URI, } ) if resp.status_code != 200: raise HTTPException(status_code=401, detail="Auth failed") tokens = resp.json() access_token = tokens["access_token"] import base64 payload = access_token.split(".")[1] payload += "=" * (4 - len(payload) % 4) user_info = json.loads(base64.b64decode(payload)) username = user_info.get("preferred_username", "unknown") ensure_bucket(username) response = RedirectResponse("/", status_code=302) response.set_cookie("token", access_token, httponly=True) response.set_cookie("username", username, httponly=True) return response @app.get("/logout") async def logout(): response = RedirectResponse("/login") response.delete_cookie("token") response.delete_cookie("username") return response @app.post("/api/launch/{service_type}") async def launch(service_type: str, request: Request): username = request.cookies.get("username") if not username: raise HTTPException(status_code=401) if service_type not in SERVICES: raise HTTPException(status_code=400, detail="Service inconnu") svc = SERVICES[service_type] url = create_service(username, service_type, svc) return JSONResponse({"status": "ok", "url": url}) @app.delete("/api/delete/{service_type}") async def delete(service_type: str, request: Request): username = request.cookies.get("username") if not username: raise HTTPException(status_code=401) delete_service(username, service_type) return JSONResponse({"status": "deleted"}) @app.get("/api/services") async def services(request: Request): username = request.cookies.get("username") if not username: raise HTTPException(status_code=401) return JSONResponse(list_user_services(username)) @app.get("/api/minio-credentials") async def minio_credentials(request: Request): username = request.cookies.get("username") if not username: raise HTTPException(status_code=401) from minio_client import get_minio_credentials return JSONResponse(get_minio_credentials(username)) @app.get("/api/minio-credentials") async def minio_credentials(request: Request): username = request.cookies.get("username") if not username: raise HTTPException(status_code=401) from minio_client import get_minio_credentials return JSONResponse(get_minio_credentials(username)) @app.post("/api/chat") async def chat(request: Request): username = request.cookies.get("username") if not username: raise HTTPException(status_code=401) body = await request.json() messages = body.get("messages", []) async with httpx.AsyncClient(verify=False, timeout=60) as client: resp = await client.post( "http://10.233.9.253:4000/v1/chat/completions", headers={"Authorization": "Bearer sk-local123test", "Content-Type": "application/json"}, json={ "model": "llama3.2:3b", "messages": [ {"role": "system", "content": f"Tu es un assistant expert Data Science pour {username}. Tu aides avec Python, pandas, SQL, machine learning, et l'utilisation de MinIO S3. Réponds en français de manière concise."} ] + messages, "temperature": 0.7, "max_tokens": 512, "stream": False } ) if resp.status_code != 200: raise HTTPException(status_code=502, detail="LiteLLM error") result = resp.json() return JSONResponse({"response": result["choices"][0]["message"]["content"]})