diff --git a/README.md b/README.md index 713f90ef..41dc8125 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,11 @@ Same variables as compose; use `-e` instead of `.env` when running `docker run` Content-Type: application/json

+

+Authentication (HTTP Basic):
+All /api/v1/ routes except /api/v1/auth/* require authentication. Send your Presenton admin username and password (same as the web UI, or AUTH_USERNAME / AUTH_PASSWORD when preseeding Docker). With curl, put them right after -u as -u USERNAME:PASSWORD — that is HTTP Basic auth and sets Authorization: Basic … for you. Replace the sample sudipnext:sudipnext below with your real credentials. +

+ **Request Body** @@ -475,18 +480,20 @@ Options: pptx, pdf "edit_path": "string" } -**Example Request** +**Example (curl + HTTP Basic auth with -u)** -
curl -X POST http://localhost:5000/api/v1/ppt/presentation/generate \
+
curl -u username:password \
+  -X POST http://localhost:5000/api/v1/ppt/presentation/generate \
   -H "Content-Type: application/json" \
   -d '{
-    "content": "Introduction to Machine Learning",
+   "content": "Introduction to Machine Learning",
     "n_slides": 5,
     "language": "English",
     "template": "general",
     "export_as": "pptx"
   }'
+ **Example Response**
{
diff --git a/servers/fastapi/api/middlewares.py b/servers/fastapi/api/middlewares.py
index fb45e7f1..b8bcd11c 100644
--- a/servers/fastapi/api/middlewares.py
+++ b/servers/fastapi/api/middlewares.py
@@ -3,7 +3,12 @@ from starlette.responses import JSONResponse
 from starlette.middleware.base import BaseHTTPMiddleware
 
 from utils.get_env import get_can_change_keys_env
-from utils.simple_auth import get_auth_status, get_session_token_from_request
+from utils.simple_auth import (
+    get_auth_status,
+    get_basic_auth_credentials_from_request,
+    get_session_token_from_request,
+    verify_credentials,
+)
 from utils.user_config import update_env_with_user_config
 
 
@@ -55,6 +60,13 @@ class SessionAuthMiddleware(BaseHTTPMiddleware):
             )
 
         if not auth_status["authenticated"]:
+            basic_credentials = get_basic_auth_credentials_from_request(request)
+            if basic_credentials and verify_credentials(
+                basic_credentials[0], basic_credentials[1]
+            ):
+                request.state.auth_username = basic_credentials[0].strip()
+                return await call_next(request)
+
             return JSONResponse(
                 status_code=401,
                 content={"detail": "Unauthorized"},
diff --git a/servers/fastapi/templates/get_layout_by_name.py b/servers/fastapi/templates/get_layout_by_name.py
index f69251ff..d6baca16 100644
--- a/servers/fastapi/templates/get_layout_by_name.py
+++ b/servers/fastapi/templates/get_layout_by_name.py
@@ -2,12 +2,23 @@ import aiohttp
 from fastapi import HTTPException
 
 from templates.presentation_layout import PresentationLayoutModel
+from utils.simple_auth import (
+    SESSION_COOKIE_NAME,
+    create_session_token,
+    get_configured_auth_username,
+)
 
 
 async def get_layout_by_name(layout_name: str) -> PresentationLayoutModel:
     url = f"http://localhost/api/template?group={layout_name}"
+    headers = {}
+    auth_username = get_configured_auth_username()
+    if auth_username:
+        internal_token = create_session_token(auth_username)
+        headers["Cookie"] = f"{SESSION_COOKIE_NAME}={internal_token}"
+
     async with aiohttp.ClientSession() as session:
-        async with session.get(url) as response:
+        async with session.get(url, headers=headers) as response:
             if response.status != 200:
                 error_text = await response.text()
                 raise HTTPException(
diff --git a/servers/fastapi/utils/simple_auth.py b/servers/fastapi/utils/simple_auth.py
index 5eb23f50..0acae5bc 100644
--- a/servers/fastapi/utils/simple_auth.py
+++ b/servers/fastapi/utils/simple_auth.py
@@ -98,6 +98,14 @@ def is_auth_configured() -> bool:
     return bool(config.get("AUTH_USERNAME") and config.get("AUTH_PASSWORD_HASH"))
 
 
+def get_configured_auth_username() -> Optional[str]:
+    config = _load_user_config()
+    username = config.get("AUTH_USERNAME")
+    if isinstance(username, str) and username.strip():
+        return username.strip()
+    return None
+
+
 def setup_initial_credentials(username: str, password: str) -> None:
     cleaned_username = (username or "").strip()
     if len(cleaned_username) < 3:
@@ -244,6 +252,29 @@ def get_session_token_from_request(request: Request) -> Optional[str]:
     return None
 
 
+def get_basic_auth_credentials_from_request(
+    request: Request,
+) -> Optional[tuple[str, str]]:
+    auth_header = request.headers.get("Authorization", "")
+    if not auth_header.lower().startswith("basic "):
+        return None
+
+    encoded_value = auth_header[6:].strip()
+    if not encoded_value:
+        return None
+
+    try:
+        decoded_value = base64.b64decode(encoded_value).decode("utf-8")
+    except Exception:
+        return None
+
+    if ":" not in decoded_value:
+        return None
+
+    username, password = decoded_value.split(":", 1)
+    return username, password
+
+
 def get_auth_status(session_token: Optional[str] = None) -> dict:
     config = _load_user_config()
     configured = bool(config.get("AUTH_USERNAME") and config.get("AUTH_PASSWORD_HASH"))