Skip to content
Open
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
8 changes: 6 additions & 2 deletions core_lib/session/user_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ def token_to_session_object(self, token):

def _secure_entry(self, request, policies):
cookies = {}
if WebHelpersUtils.get_server_type() == WebHelpersUtils.ServerType.DJANGO:
server_type = WebHelpersUtils.get_server_type()
if server_type == WebHelpersUtils.ServerType.DJANGO:
cookies = request.COOKIES
elif WebHelpersUtils.get_server_type() == WebHelpersUtils.ServerType.FLASK:
elif server_type == WebHelpersUtils.ServerType.FLASK:
cookies = request.cookies
elif server_type == WebHelpersUtils.ServerType.FASTAPI:
# Starlette's `Request.cookies` is a plain dict-like.
cookies = request.cookies
token = cookies.get(self.cookie_name)
session_obj = self.from_session_data(self.token_handler.decode(token)) if token else None
Expand Down
17 changes: 17 additions & 0 deletions core_lib/web_helpers/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ def _get_request():
return None
except Exception as e:
logger.debug(f"Unable to determine server type: {e}")
<<<<<<< Updated upstream
=======
return None

if server_type == WebHelpersUtils.ServerType.FLASK:
try:
from flask import request as flask_request
return flask_request
except Exception as e:
logger.debug(f"Failed to fetch Flask request: {e}")
return None
# DJANGO / FASTAPI / unknown: no thread-local request available — both
# bind the request per-task (per-view for Django, per-async-task for
# FastAPI / Starlette). Callers should attach the request to the error
# middleware context explicitly.
return None
>>>>>>> Stashed changes

def _execute_error_middlewares(exc, func):
request = _get_request()
Expand Down
Empty file.
30 changes: 30 additions & 0 deletions core_lib/web_helpers/fastapi/require_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""@RequireLogin decorator for FastAPI views.

Mirrors the Flask / Django variants. FastAPI views can be async or sync;
this decorator supports both. The wrapped view must accept the Starlette
``Request`` as its first parameter (the standard FastAPI convention via
``request: Request`` parameter binding).
"""
import logging
from functools import wraps

from core_lib.web_helpers.require_login_helper import require_login

logger = logging.getLogger(__name__)


class RequireLogin(object):
def __init__(self, policies=None):
if policies is None:
policies = []
self.policies = policies

def __call__(self, func, *args, **kwargs):
@wraps(func)
def __wrapper(request, *args, **kwargs):
# `require_login` itself re-passes `request` to `func` when the
# server type is FASTAPI (or DJANGO), so we strip it from
# *args here and only forward the rest.
return require_login(request, self.policies, func, *args, **kwargs)

return __wrapper
28 changes: 28 additions & 0 deletions core_lib/web_helpers/fastapi/user_auth_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""ASGI user-auth middleware for FastAPI / Starlette.

Mirrors the Flask `UserAuthMiddleware` (WSGI) and Django `UserAuthMiddleware`
patterns: read the configured cookie, decode it via the registered
SecurityHandler, and stash the resulting session object on the request
scope so views / dependencies can read it.
"""
from starlette.middleware.base import BaseHTTPMiddleware

from core_lib.session.security_handler import SecurityHandler


class UserAuthMiddleware(BaseHTTPMiddleware):
"""Attach the decoded session object to ``request.state.user`` when
the configured cookie is present. Use it like any Starlette middleware::

app.add_middleware(UserAuthMiddleware, cookie_name='my_cookie')
"""

def __init__(self, app, cookie_name: str):
super().__init__(app)
self.cookie_name = cookie_name

async def dispatch(self, request, call_next):
token = request.cookies.get(self.cookie_name)
if token:
request.state.user = SecurityHandler.get().token_to_session_object(token)
return await call_next(request)
29 changes: 29 additions & 0 deletions core_lib/web_helpers/request_response_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from django.http import HttpResponse
from flask import Flask
from starlette.responses import Response as StarletteResponse


def response_status(status: int = HTTPStatus.OK.value):
Expand Down Expand Up @@ -57,6 +58,8 @@ def generate_response(data, status, media_type: MediaType = MediaType.TEXT_HTML,
return generate_response_django(data, status, media_type, headers)
elif WebHelpersUtils.get_server_type() == WebHelpersUtils.ServerType.FLASK:
return generate_response_flask(data, status, media_type, headers)
elif WebHelpersUtils.get_server_type() == WebHelpersUtils.ServerType.FASTAPI:
return generate_response_fastapi(data, status, media_type, headers)


def generate_response_django(data, status, media_type: MediaType, headers: dict = {}):
Expand All @@ -73,6 +76,23 @@ def generate_response_flask(data, status, media_type: MediaType, headers: dict =
return response


def generate_response_fastapi(data, status, media_type: MediaType, headers: dict = None):
# Starlette Response — works for FastAPI views and middlewares.
# FastAPI accepts these directly as view return values.
if headers is None:
headers = {}
# IntEnum compatibility — Starlette accepts int but `status_code` is
# explicitly typed int and HTTPStatus members satisfy that.
response = StarletteResponse(
content=data,
status_code=int(status),
media_type=media_type.value,
)
for key, value in headers.items():
response.headers[key] = value
return response


#
# HELPERS
#
Expand All @@ -83,3 +103,12 @@ def request_body_dict(request):
return json.loads(request.body.decode('utf-8'))
elif WebHelpersUtils.get_server_type() == WebHelpersUtils.ServerType.FLASK:
return request.json
elif WebHelpersUtils.get_server_type() == WebHelpersUtils.ServerType.FASTAPI:
# FastAPI's normal pattern is `await request.json()` inside an async
# view. Inside this sync helper we expect callers to have pre-read
# the body (e.g. via a sync wrapper, or by passing `request.json()`
# already-resolved). Accept both shapes for ergonomics.
body = request.body if isinstance(request.body, (bytes, bytearray)) else None
if body is not None:
return json.loads(body.decode('utf-8'))
return None
17 changes: 17 additions & 0 deletions core_lib/web_helpers/require_login_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
def require_login(request, policies, func, *args, **kwargs):
response = handle_exception(SecurityHandler.get()._secure_entry, request, policies)
if not response:
<<<<<<< Updated upstream
try:
if WebHelpersUtils.get_server_type() == WebHelpersUtils.ServerType.DJANGO:
return func(request, *args, **kwargs)
Expand All @@ -18,4 +19,20 @@ def require_login(request, policies, func, *args, **kwargs):
logger.error(
f'error while loading target page for controller entry name `{func.__name__}`', exc_info=True
)
=======
# Route the view function through handle_exception so its exceptions
# become proper HTTP responses (rather than being silently swallowed
# and returning None — which previously rendered as a blank page).
# Django and FastAPI views receive the request as a positional arg
# (Django convention; FastAPI via this lib's RequireLogin wrapper).
# Flask views read the request from a thread-local proxy so it isn't
# passed positionally.
server_type = WebHelpersUtils.get_server_type()
if server_type in (
WebHelpersUtils.ServerType.DJANGO,
WebHelpersUtils.ServerType.FASTAPI,
):
return handle_exception(func, request, *args, **kwargs)
return handle_exception(func, *args, **kwargs)
>>>>>>> Stashed changes
return response
1 change: 1 addition & 0 deletions core_lib/web_helpers/web_helprs_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class WebHelpersUtils(object):
class ServerType(enum.Enum):
FLASK = 'flask'
DJANGO = 'django'
FASTAPI = 'fastapi'

@staticmethod
def init(server_type: ServerType):
Expand Down
Loading