-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce Gateway Aggregation / Backend-for-Frontend abstractio…
…n via LearnerPortalBFFAPIView
- Loading branch information
1 parent
813442a
commit 52aa2d9
Showing
11 changed files
with
886 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,3 +17,4 @@ | |
SubsidyAccessPolicyRedeemViewset, | ||
SubsidyAccessPolicyViewSet | ||
) | ||
from .bffs import LearnerPortalBFFAPIView |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
""" | ||
Enterprise BFFs for MFEs. | ||
""" | ||
|
||
from rest_framework.response import Response | ||
from rest_framework.views import APIView | ||
from rest_framework.permissions import IsAuthenticated | ||
from rest_framework.authentication import get_authorization_header, SessionAuthentication | ||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication | ||
from edx_rest_framework_extensions.auth.jwt.cookies import jwt_cookie_name | ||
|
||
from enterprise_access.apps.bffs.context import HandlerContext | ||
from enterprise_access.apps.bffs.handlers import LearnerPortalHandlerFactory | ||
from enterprise_access.apps.bffs.response_builder import LearnerPortalResponseBuilderFactory | ||
|
||
|
||
class LearnerPortalBFFAPIView(APIView): | ||
""" | ||
API view for learner portal BFF routes. | ||
""" | ||
|
||
authentication_classes = [JwtAuthentication] | ||
permission_classes = [IsAuthenticated] | ||
|
||
def post(self, request, page_route, *args, **kwargs): | ||
""" | ||
Handles GET requests for learner-specific routes. | ||
Args: | ||
request (Request): The request object. | ||
route (str): The specific learner portal route (e.g., 'dashboard'). | ||
Returns: | ||
Response: The response data formatted by the response builder. | ||
""" | ||
|
||
# Create the context based on the request | ||
context = HandlerContext(page_route=page_route, request=request) | ||
|
||
# Use the LearnerPortalResponseBuilderFactory to get the appropriate response builder | ||
response_builder = LearnerPortalResponseBuilderFactory.get_response_builder(context) | ||
|
||
try: | ||
# Use the LearnerHandlerFactory to get the appropriate handler | ||
handler = LearnerPortalHandlerFactory.get_handler(context) | ||
|
||
# Load and process data using the handler | ||
handler.load_and_process() | ||
except Exception as exc: | ||
context.add_error( | ||
user_message="An error occurred while processing the request.", | ||
developer_message=f"Error: {exc}", | ||
severity="error", | ||
) | ||
|
||
# Build the response data and status code | ||
response_data, status_code = response_builder.build() | ||
|
||
return Response(response_data, status=status_code) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import crum | ||
import requests | ||
|
||
from edx_django_utils.monitoring import set_custom_attribute | ||
from edx_rest_framework_extensions.auth.jwt.cookies import jwt_cookie_name | ||
|
||
|
||
def get_request_id(): | ||
""" | ||
Helper to get the request id - usually set via an X-Request-ID header | ||
""" | ||
request = crum.get_current_request() | ||
if request is not None and request.headers is not None: | ||
return request.headers.get('X-Request-ID') | ||
else: | ||
return None | ||
|
||
|
||
class BaseUserApiClient(requests.Session): | ||
""" | ||
A requests Session that includes the Authorization and User-Agent headers from the original request. | ||
""" | ||
def __init__(self, original_request, **kwargs): | ||
super().__init__(**kwargs) | ||
self.original_request = original_request | ||
|
||
self.headers = {} | ||
|
||
if self.original_request: | ||
# If Authorization header is present in the original request, pass through to subsequent request headers | ||
if 'Authorization' in self.original_request.headers: | ||
self.headers['Authorization'] = self.original_request.headers['Authorization'] | ||
|
||
# If no Authorization header, check for JWT in cookies | ||
jwt_token = self.original_request.COOKIES.get(jwt_cookie_name()) | ||
if 'Authorization' not in self.headers and jwt_token is not None: | ||
self.headers['Authorization'] = f'JWT {jwt_token}' | ||
|
||
# Add X-Request-ID header if applicable | ||
request_id = get_request_id() | ||
if self.headers.get('X-Request-ID') is None and request_id is not None: | ||
self.headers['X-Request-ID'] = request_id | ||
|
||
def request(self, method, url, headers=None, **kwargs): # pylint: disable=arguments-differ | ||
if headers: | ||
headers.update(self.headers) | ||
else: | ||
headers = self.headers | ||
|
||
# Set `api_client` as a custom attribute for monitoring, reflecting the API client's module path | ||
set_custom_attribute('api_client', 'enterprise_access.apps.api_client.base_user.BaseUserApiClient') | ||
|
||
return super().request(method, url, headers=headers, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class BffsConfig(AppConfig): | ||
default_auto_field = 'django.db.models.BigAutoField' | ||
name = 'enterprise_access.apps.bffs' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
""" | ||
HandlerContext for bffs app. | ||
""" | ||
|
||
class HandlerContext: | ||
""" | ||
A context object for managing the state throughout the lifecycle of a Backend-for-Frontend (BFF) request. | ||
The `HandlerContext` class stores request information, the current route, loaded data, and any errors | ||
that may occur during the request. | ||
Attributes: | ||
request: The original request object containing information about the incoming HTTP request. | ||
route: The route for which the response is being generated. | ||
data: A dictionary to store data loaded and processed by the handlers. | ||
errors: A list to store errors that occur during request processing. | ||
""" | ||
|
||
def __init__(self, request, page_route): | ||
""" | ||
Initializes the HandlerContext with request information, route, and optional initial data. | ||
Args: | ||
request: The incoming HTTP request. | ||
page_route: The route identifier for the request. | ||
""" | ||
self.page_route = page_route | ||
self.request = request | ||
self.user = request.user | ||
self.data = {} # Stores processed data for the response | ||
self.errors = [] # Stores any errors that occur during processing | ||
self.enterprise_customer_uuid = None | ||
self.lms_user_id = None | ||
|
||
def add_error(self, user_message, developer_message, severity='error'): | ||
""" | ||
Adds an error to the context. | ||
Args: | ||
user_message (str): A user-friendly error message. | ||
developer_message (str): A more detailed error message for debugging purposes. | ||
severity (str): The severity level of the error ('error' or 'warning'). Defaults to 'error'. | ||
""" | ||
if not (user_message and developer_message): | ||
raise ValueError("User message and developer message are required for errors.") | ||
|
||
self.errors.append({ | ||
"user_message": user_message, | ||
"developer_message": developer_message, | ||
"severity": severity, | ||
}) |
Oops, something went wrong.