Skip to content

Commit eec0ca1

Browse files
authored
Refractor V2.3.0 (#19)
* Switch all endpoints to POST and use request body - Changed all API endpoint methods from GET to POST in configuration files and updated handler logic to accept parameters via a Pydantic request body model. * Add session-based authentication and secure file proxy - Introduced in-memory session management with secure cookie handling, added authentication dependencies, and implemented secure login/logout endpoints under /api/secure. - Added a /files proxy route for authenticated file access from Moodle, replacing insecure token-in-URL patterns. - Updated CORS handling for dynamic origin reflection - Refactored compose.yaml to use .env - Updated requirements for httpx[http2]. * Add async session cleanup task to FastAPI lifespan - Introduces an async lifespan context manager that periodically cleans up expired sessions using a background task. - The session cleanup runs every SESSION_MAX_AGE seconds and logs the number of sessions removed or any errors encountered. - The cleanup task is properly cancelled on application shutdown. * Migrate session storage to Redis and update config - Session management has been refactored to use Redis for storage instead of in-memory dictionaries. This includes async session functions, Redis initialization in app lifespan, and automatic session expiration via Redis. - The .env.example and compose.yaml files were updated to add Redis configuration, and requirements.txt now includes the redis package. - All session-related code and dependencies have been updated to support async Redis operations. * Refactor Dockerfile to use multi-stage build Switches to a multi-stage build using python:3.13-alpine for both build and runtime stages to reduce image size. - Installs build dependencies only in the builder stage and copies only the necessary Python packages and application code to the final image. * Update CORS handling and secure auth route prefix - Refactored CORS configuration to use allow_origin_regex for wildcard origins with credentials, exposing 'X-Request-Id' header and setting max_age. - Changed secure authentication route prefix from '/api/secure' to '/secure' for consistency. * test * test fix * Update environment and Docker Compose for Postgres support Added Postgres service to compose.yaml and updated environment variables in .env.example for database configuration. Improved organization and clarity of environment variables, and ensured all services use a shared network for better connectivity. * Update default MOODLE_URL in compose.yaml Changed the default MOODLE_URL environment variable
1 parent afd4622 commit eec0ca1

46 files changed

Lines changed: 1049 additions & 141 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Moodle Configuration
2+
MOODLE_URL=https://moodle.school.edu
3+
4+
# Port Configuration
5+
API_PORT=8000
6+
FRONTEND_PORT=80
7+
8+
# API URL for Frontend
9+
# This is the URL the frontend will use to make API requests
10+
# For local development: http://localhost:8000
11+
# For production: https://api.yourdomain.com
12+
API_BASE_URL=http://localhost:8000
13+
14+
# Security
15+
SECRET_KEY=CHANGE_ME_IN_PRODUCTION
16+
SESSION_MAX_AGE=14400
17+
18+
# CORS Configuration
19+
ALLOW_ORIGINS=*
20+
21+
# Environment
22+
ENVIRONMENT=development
23+
LOG_LEVEL=info
24+
25+
# Database
26+
POSTGRES_PORT=5432
27+
POSTGRES_DB=moodleng
28+
POSTGRES_USER=moodleng
29+
POSTGRES_PASSWORD=moodleng_dev_password
30+
31+
# Full database connection
32+
DATABASE_URL=postgresql://moodleng:moodleng_dev_password@postgres:5432/moodleng
33+
34+
REDIS_URL=redis://redis:6379/0

.env.template

Lines changed: 0 additions & 20 deletions
This file was deleted.

Dockerfile

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
1-
FROM python:3.13.5-slim
1+
# Stage 1: Builder - Install dependencies
2+
FROM python:3.13-alpine AS builder
23

3-
WORKDIR /app
4-
5-
ENV PYTHONUNBUFFERED=1
4+
# Install build dependencies needed to compile Python packages
5+
RUN apk add --no-cache gcc musl-dev
66

7+
# Copy requirements and install dependencies
78
COPY requirements.txt .
8-
RUN pip install --no-cache-dir -r requirements.txt
9+
RUN pip install --no-cache-dir --user -r requirements.txt
910

11+
# Stage 2: Runtime - Copy only what's needed
12+
FROM python:3.13-alpine
13+
14+
WORKDIR /app
15+
16+
# Copy Python packages from builder
17+
COPY --from=builder /root/.local /root/.local
18+
19+
# Copy application code
1020
COPY . .
1121

22+
# Make sure scripts in .local are usable
23+
ENV PATH=/root/.local/bin:$PATH
24+
ENV PYTHONUNBUFFERED=1
25+
1226
EXPOSE 8000
1327

1428
CMD ["python", "asgi.py"]

compose.yaml

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,74 @@
1-
# This compose file is meant for development purposes.
1+
name: MoodlewareAPI
22

33
services:
4-
moodle-api:
4+
redis:
5+
image: redis:alpine
6+
container_name: MoodleNG-Redis
7+
ports:
8+
- "6379:6379"
9+
volumes:
10+
- redis-data:/data
11+
command: redis-server --appendonly yes
12+
healthcheck:
13+
test: ["CMD", "redis-cli", "ping"]
14+
interval: 10s
15+
timeout: 3s
16+
retries: 3
17+
restart: unless-stopped
18+
networks:
19+
- moodleng-network
20+
21+
postgres:
22+
image: postgres:16-alpine
23+
container_name: MoodleNG-Postgres
24+
ports:
25+
- "${POSTGRES_PORT:-5432}:5432"
26+
environment:
27+
- POSTGRES_DB=${POSTGRES_DB:-moodleng}
28+
- POSTGRES_USER=${POSTGRES_USER:-moodleng}
29+
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-moodleng_dev_password}
30+
volumes:
31+
- postgres-data:/var/lib/postgresql/data
32+
healthcheck:
33+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-moodleng}"]
34+
interval: 10s
35+
timeout: 3s
36+
retries: 3
37+
restart: unless-stopped
38+
networks:
39+
- moodleng-network
40+
41+
moodleware-api:
542
container_name: MoodlewareAPI
6-
build: .
43+
build:
44+
context: .
45+
dockerfile: Dockerfile
746
ports:
8-
- ${PORT-8000}:8000
47+
- "${API_PORT:-8000}:8000"
948
environment:
10-
- MOODLE_URL=${MOODLE_URL}
11-
- ALLOW_ORIGINS=${ALLOW_ORIGINS}
12-
- LOG_LEVEL=${LOG_LEVEL}
49+
- MOODLE_URL=${MOODLE_URL:-https://moodle.school.edu}
50+
- ALLOW_ORIGINS=${ALLOW_ORIGINS:-*}
51+
- ENVIRONMENT=${ENVIRONMENT:-development}
52+
- LOG_LEVEL=${LOG_LEVEL:-info}
53+
- SECRET_KEY=${SECRET_KEY:-cXWIu5Yj5P4TpHNcqwwVrPDqBQTstQF0A3_C_vjM2LQ}
54+
- SESSION_MAX_AGE=${SESSION_MAX_AGE:-14400}
55+
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
56+
- DATABASE_URL=${DATABASE_URL:-postgresql://moodleng:moodleng_dev_password@postgres:5432/moodleng}
57+
depends_on:
58+
redis:
59+
condition: service_healthy
60+
postgres:
61+
condition: service_healthy
1362
restart: unless-stopped
63+
networks:
64+
- moodleng-network
65+
66+
networks:
67+
moodleng-network:
68+
driver: bridge
69+
70+
volumes:
71+
redis-data:
72+
driver: local
73+
postgres-data:
74+
driver: local

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
fastapi
2-
httpx
2+
httpx[http2]
33
pydantic
44
uvicorn
55
colorlog
66
python-dotenv
7-
itsdangerous
7+
itsdangerous
8+
redis>=5.0.0

src/app.py

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,98 @@
11
import os
22
import logging
33
import uuid
4+
import asyncio
45
from typing import Callable
6+
from contextlib import asynccontextmanager
57
from fastapi import FastAPI, Request, Security, Response
68
from dotenv import load_dotenv
79
from fastapi.middleware.cors import CORSMiddleware
810
from fastapi.security import HTTPBearer
11+
from redis.asyncio import Redis
912
from .mw_utils import get_env_variable, load_config, create_handler
13+
from .mw_utils.session import cleanup_expired_sessions, SESSION_MAX_AGE, init_redis, REDIS_URL
14+
from .routes.secure_auth import router as secure_auth_router
15+
from .routes.files import router as files_router
16+
from .routes.office_preview import router as office_preview_router
1017

1118
load_dotenv()
1219

13-
# Configure logging level (default to INFO)
1420
_log_level_name = (get_env_variable("LOG_LEVEL") or "info").upper()
1521
_log_level = getattr(logging, _log_level_name, logging.INFO)
1622
logging.basicConfig(level=_log_level)
1723
logger = logging.getLogger("moodleware")
1824

25+
@asynccontextmanager
26+
async def lifespan(app: FastAPI):
27+
# Initialize Redis connection
28+
redis_client = Redis.from_url(
29+
REDIS_URL,
30+
encoding="utf-8",
31+
decode_responses=True,
32+
socket_connect_timeout=5,
33+
socket_keepalive=True,
34+
)
35+
36+
try:
37+
# Test Redis connection
38+
await redis_client.ping()
39+
logger.info(f"Redis connected successfully: {REDIS_URL}")
40+
init_redis(redis_client)
41+
except Exception as e:
42+
logger.error(f"Failed to connect to Redis: {e}")
43+
await redis_client.aclose()
44+
raise
45+
46+
# Redis handles session expiration automatically via SETEX
47+
# No cleanup task needed anymore
48+
logger.info(f"Session storage initialized (Redis with automatic expiration)")
49+
50+
yield
51+
52+
# Close Redis connection
53+
await redis_client.aclose()
54+
logger.info("Redis connection closed")
55+
1956
app = FastAPI(
2057
title="MoodlewareAPI",
2158
description="A FastAPI application to wrap Moodle API functions into individual endpoints.",
2259
version="0.1.0",
2360
docs_url="/",
24-
redoc_url=None
61+
redoc_url=None,
62+
lifespan=lifespan
2563
)
2664

2765
# CORS configuration from env
2866
_allow_origins_env = (get_env_variable("ALLOW_ORIGINS") or "").strip()
67+
68+
# For wildcard CORS with credentials, we need to use regex to match all origins
2969
if _allow_origins_env == "" or _allow_origins_env == "*":
30-
_allow_origins = ["*"]
31-
_allow_credentials = False # '*' cannot be used with credentials per CORS spec
70+
# Use regex to allow any origin (required for credentials with wildcard)
71+
app.add_middleware(
72+
CORSMiddleware,
73+
allow_origin_regex=r".*", # Allow any origin
74+
allow_credentials=True,
75+
allow_methods=["*"],
76+
allow_headers=["*"],
77+
expose_headers=["X-Request-Id"],
78+
max_age=86400, # 24 hours
79+
)
80+
logger.info("CORS: Allowing all origins with credentials (regex pattern)")
3281
else:
82+
# Specific origins configured
3383
_allow_origins = [o.strip() for o in _allow_origins_env.split(",") if o.strip()]
34-
_allow_credentials = True
35-
36-
app.add_middleware(
37-
CORSMiddleware,
38-
allow_origins=_allow_origins,
39-
allow_credentials=_allow_credentials,
40-
allow_methods=["*"],
41-
allow_headers=["*"],
42-
)
84+
85+
app.add_middleware(
86+
CORSMiddleware,
87+
allow_origins=_allow_origins,
88+
allow_credentials=True,
89+
allow_methods=["*"],
90+
allow_headers=["*"],
91+
expose_headers=["X-Request-Id"],
92+
max_age=86400,
93+
)
94+
95+
logger.info(f"CORS: Allowing specific origins: {_allow_origins}")
4396

4497
# Request ID middleware
4598
@app.middleware("http")
@@ -76,7 +129,16 @@ async def add_request_id(request: Request, call_next: Callable):
76129
dependencies=deps,
77130
)
78131

132+
# Register secure authentication routes
133+
app.include_router(secure_auth_router)
134+
135+
# Register file proxy routes
136+
app.include_router(files_router)
137+
138+
# Register office preview one-time token routes
139+
app.include_router(office_preview_router)
140+
79141
# Health check
80-
@app.get("/healthz", tags=["meta"])
142+
@app.post("/healthz", tags=["meta"])
81143
async def healthz():
82144
return {"status": "ok"}

src/config/_login_token-php/Authentication/auth.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"method": "GET",
2+
"method": "POST",
33
"description": "Get Moodle token for API calls.",
44
"query_params": [
55
{

src/config/_webservice_rest_server-php/Assignments/mod_assign_get_assignments.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"method": "GET",
2+
"method": "POST",
33
"description": "Get assignments from specified courses",
44
"query_params": [
55
{

src/config/_webservice_rest_server-php/Assignments/mod_assign_get_submission_status.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"method": "GET",
2+
"method": "POST",
33
"description": "Get assignment submission status",
44
"query_params": [
55
{

src/config/_webservice_rest_server-php/Assignments/mod_assign_get_submissions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"method": "GET",
2+
"method": "POST",
33
"description": "Get assignment submissions",
44
"query_params": [
55
{

0 commit comments

Comments
 (0)