Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Example environment variables for MoodlewareAPI
# Copy this file to ".env" and adjust values.

# Port the app listens on (used by compose)
PORT=8000

# Base URL of your Moodle instance (required)
MOODLE_URL=https://moodle.school.edu

3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ httpx
pydantic
uvicorn
colorlog
python-dotenv
python-dotenv
itsdangerous
11 changes: 9 additions & 2 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import os
import json
from pathlib import Path
from fastapi import FastAPI
from fastapi import FastAPI, Request, HTTPException, Body, Depends, Security
from dotenv import load_dotenv
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from .utils import get_env_variable, load_config, create_handler

load_dotenv()
Expand All @@ -24,17 +25,23 @@
allow_headers=["*"],
)

# Optional HTTP Bearer security for Swagger Authorize
http_bearer = HTTPBearer(auto_error=False)

config = load_config("config.json")

for endpoint_path, functions in config.items():
print(f"Processing endpoint: {endpoint_path}")
for function in functions:
print(f"Processing function: {function['function']} at path {function['path']}")
# Attach bearer scheme to all but the open /auth endpoint so Swagger propagates the token
deps = [Security(http_bearer)] if function["path"] != "/auth" else None
app.add_api_route(
path=function["path"],
endpoint=create_handler(function, endpoint_path),
methods=[function["method"].upper()],
tags=function["tags"],
summary=function["description"],
responses=function.get("responses")
responses=function.get("responses"),
dependencies=deps,
)
42 changes: 26 additions & 16 deletions src/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import json
from fastapi import Query, HTTPException, Response
from fastapi import Query, HTTPException, Response, Request
from typing import Optional
import httpx
from urllib.parse import urlencode
Expand Down Expand Up @@ -119,11 +119,24 @@ def _encode_param(params: dict, name: str, value, declared_type: str):
params[name] = value


# Extract token from Authorization header or query (no sessions)
async def _resolve_token_from_request(request: Request) -> str:
# Prefer Authorization header if present
auth = request.headers.get("Authorization")
if auth and auth.lower().startswith("bearer "):
return auth.split(" ", 1)[1].strip()
# Fallback to query string (?wstoken=)
qtok = request.query_params.get("wstoken")
if qtok:
return qtok
return ""


def create_handler(function_config, endpoint_path: str):
query_params = function_config.get("query_params", [])
method = function_config.get("method", "GET").upper()

async def handler(response: Response, **kwargs):
async def handler(request: Request, response: Response, **kwargs):
base_url = get_env_variable("MOODLE_URL") or kwargs.get("moodle_url")
if not base_url:
raise HTTPException(status_code=400, detail="Moodle URL not provided. Set MOODLE_URL env var or pass moodle_url as query param.")
Expand All @@ -142,9 +155,12 @@ async def handler(response: Response, **kwargs):
if pname in kwargs and kwargs[pname] is not None:
_encode_param(params, pname, kwargs[pname], ptype)

# Token handling
if "wstoken" in kwargs and kwargs["wstoken"] is not None:
params["wstoken"] = kwargs["wstoken"]
# Token handling: for non-auth routes, resolve from header or query
is_auth_endpoint = ep_path.endswith("/login/token.php")
if not is_auth_endpoint:
token = await _resolve_token_from_request(request)
if token:
params["wstoken"] = token

# For core REST endpoint include wsfunction & format
if ep_path.endswith("/webservice/rest/server.php"):
Expand Down Expand Up @@ -185,6 +201,11 @@ async def handler(response: Response, **kwargs):
# Build dynamic signature for OpenAPI/Docs
import inspect
sig_params = [
inspect.Parameter(
"request",
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=Request
),
inspect.Parameter(
"response",
inspect.Parameter.POSITIONAL_OR_KEYWORD,
Expand All @@ -203,17 +224,6 @@ async def handler(response: Response, **kwargs):
)
)

param_names = {p["name"] if isinstance(p, dict) else p for p in query_params}
if not {"username", "password"}.issubset(param_names):
sig_params.append(
inspect.Parameter(
"wstoken",
inspect.Parameter.KEYWORD_ONLY,
annotation=str,
default=Query(..., description="Your Moodle Token, obtained from /auth")
)
)

# Map config 'type' to Python types for docs only
def _py_type(tname: str):
t = (tname or "str").lower()
Expand Down
Loading