diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 00000000..6415af9b --- /dev/null +++ b/app/auth.py @@ -0,0 +1,56 @@ +# app/auth.py +import os +from authlib.integrations.starlette_client import OAuth +from starlette.config import Config +from starlette.middleware.sessions import SessionMiddleware +from fastapi import APIRouter, Request +from starlette.responses import RedirectResponse + +config = Config(".env") +oauth = OAuth(config) + +oauth.register( + name="authentik", + client_id=config("AUTHENTIK_CLIENT_ID"), + client_secret=config("AUTHENTIK_CLIENT_SECRET"), + server_metadata_url=config("AUTHENTIK_CONFIG_URL"), + client_kwargs={ + "scope": "openid profile email", + }, +) + +router = APIRouter() + +def get_current_user(request: Request): + return request.session.get("user") + +def require_login(func): + """Decorator to require login for particular endpoints.""" + async def wrapper(request: Request, *args, **kwargs): + if not request.session.get("user"): + return RedirectResponse(url="/login") + return await func(request, *args, **kwargs) + return wrapper + +@router.get("/login") +async def login(request: Request): + redirect_uri = request.url_for("auth") + return await oauth.authentik.authorize_redirect(request, redirect_uri) + +@router.get("/auth") +async def auth(request: Request): + token = await oauth.authentik.authorize_access_token(request) + userinfo = token.get("userinfo") + request.session["user"] = dict(userinfo) + return RedirectResponse(url="/ui") + +@router.get("/logout") +async def logout(request: Request): + request.session.pop("user", None) + return RedirectResponse(url="/") + +@router.get("/private") +@require_login +async def private_page(request: Request): + user = request.session.get("user") + return {"message": f"This is a protected page. Hello {user['email']}!"} diff --git a/app/main.py b/app/main.py index 9d03538f..432cb805 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import os from fastapi import FastAPI, HTTPException, UploadFile, File from app.config import settings @@ -9,9 +8,19 @@ from app.tasks.upload_to_paperless import upload_to_paperless from app.tasks.upload_to_nextcloud import upload_to_nextcloud from app.tasks.send_to_all import send_to_all_destinations from app.frontend import router as frontend_router +from app.auth import router as auth_router +from starlette.middleware.sessions import SessionMiddleware +from starlette.config import Config + +# Load configuration from .env for the session key +config = Config(".env") +SESSION_SECRET = config("SESSION_SECRET", default="YOUR_DEFAULT_SESSION_SECRET_MUST_BE_32_CHARS_OR_MORE") app = FastAPI(title="Document Processing API") +# Add session middleware (needed for storing user sessions) +app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET) + @app.get("/") def root(): return {"message": "Document Processing API"} @@ -22,8 +31,6 @@ def process(file_path: str): API Endpoint to start document processing. This enqueues the first task (upload_to_s3), which handles the full pipeline. """ - - # If file_path is not absolute, treat it as relative to settings.workdir. if not os.path.isabs(file_path): file_path = os.path.join(settings.workdir, file_path) @@ -60,27 +67,20 @@ def send_to_nextcloud(file_path: str): task = upload_to_nextcloud.delay(file_path) return {"task_id": task.id, "status": "queued"} - @app.post("/send_to_all_destinations/") def send_to_all_destinations_endpoint(file_path: str): """ Call the aggregator task that sends this file to dropbox, nextcloud, and paperless. """ if not os.path.isabs(file_path): - # If not absolute, assume it's in processed subdir file_path = os.path.join(settings.workdir, 'processed', file_path) if not os.path.exists(file_path): - raise HTTPException( - status_code=400, - detail=f"File {file_path} not found." - ) + raise HTTPException(status_code=400, detail=f"File {file_path} not found.") task = send_to_all_destinations.delay(file_path) return {"task_id": task.id, "status": "queued", "file_path": file_path} - - @app.post("/processall") def process_all_pdfs_in_workdir(): """ @@ -102,7 +102,6 @@ def process_all_pdfs_in_workdir(): task_ids = [] for pdf in pdf_files: file_path = os.path.join(target_dir, pdf) - # Enqueue upload_to_s3 from app.tasks.upload_to_s3 import upload_to_s3 task = upload_to_s3.delay(file_path) task_ids.append(task.id) @@ -113,14 +112,10 @@ def process_all_pdfs_in_workdir(): "task_ids": task_ids } -app.include_router(frontend_router) - @app.post("/ui-upload") async def ui_upload(file: UploadFile = File(...)): - # You can store this file in your 'workdir' (like how /process does) or a tmp dir workdir = "/workdir" target_path = os.path.join(workdir, file.filename) - try: with open(target_path, "wb") as f: content = await file.read() @@ -128,6 +123,9 @@ async def ui_upload(file: UploadFile = File(...)): except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to save file: {e}") - # Now you can call your existing Celery flow: task = upload_to_s3.delay(target_path) - return {"task_id": task.id, "status": "queued"} \ No newline at end of file + return {"task_id": task.id, "status": "queued"} + +# Include the frontend and auth routers +app.include_router(frontend_router) +app.include_router(auth_router)