Skip to content

Commit

Permalink
Updates to auth and basket APIs for frontend code (#171)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkachel authored Nov 21, 2024
1 parent 094d3d5 commit f529e25
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ POSTHOG_API_HOST=https://app.posthog.com
POSTHOG_API_TOKEN=

MITOL_UE_PAYMENT_INTERSTITIAL_DEBUG=False
MITOL_UE_PAYMENT_BASKET_ROOT=http://learn.odl.local:8073/cart/
MITOL_UE_PAYMENT_BASKET_CHOOSER=http://learn.odl.local:8073/cart/

MITOL_PAYMENT_GATEWAY_CYBERSOURCE_ACCESS_KEY=sample-setting
MITOL_PAYMENT_GATEWAY_CYBERSOURCE_PROFILE_ID=sample-setting
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ The following settings must be configured before running the app:

The client secret for the OIDC client. No default - you will need to get this from the Keycloak admin, even if you're using the pack-in Keycloak instance.

- `MITOL_UE_PAYMENT_BASKET_ROOT`

The root URL for the basket page. This defaults to `/cart/` (which is the cart test mule app), but if you're testing the actual frontend, this needs to be set to go there (i.e. `http://learn.odl.local:8062/cart/`). Make sure this has a `/` appended since it is used to _generate_ a URL.

- `MITOL_UE_PAYMENT_BASKET_CHOOSER`

The URL for an optional "chooser" page. If the `establish_session` call happens without a valid system slug, the user gets sent here so they can choose which cart they want to see.


### Loading and Accessing Data

You'll need an integrated system and product for that system to be able to do much of anything. A management command exists to create the test data: `generate_test_data`. This will create a system and add some products with random (but usable) prices in it.
Expand Down
14 changes: 14 additions & 0 deletions config/apisix/apisix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ routes:
- "/api/v0/payments/checkout/result/*"
- "/static/*"
- "/api/v0/schema/*"
- "/auth/*"
- "/_/v0/meta/apisix_test_request/"
- "/logged_out/"
- id: 2
name: "ue-default"
desc: "Wildcard route for the rest of the system - authentication required"
Expand All @@ -30,6 +33,17 @@ routes:
bearer_only: false
introspection_endpoint_auth_method: "client_secret_post"
ssl_verify: false
logout_path: "/logout/"
post_logout_redirect_uri: ${{UE_LOGOUT_URL}}
cors:
allow_origins: "**"
allow_methods: "**"
allow_headers: "**"
allow_credential: true
response-rewrite:
headers:
set:
Referrer-Policy: "origin"
uris:
- "/*"

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ services:
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET}
- KEYCLOAK_DISCOVERY_URL=${KEYCLOAK_DISCOVERY_URL:-https://kc.odl.local:7443/realms/ol-local/.well-known/openid-configuration}
- APISIX_PORT=${APISIX_PORT:-9080}
- UE_LOGOUT_URL=${UE_LOGOUT_URL:-http://ue.odl.local:9080/auth/logout/}
ports:
- 9080:9080
- 9180:9180
Expand Down
23 changes: 18 additions & 5 deletions frontends/api/src/generated/v0/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1925,12 +1925,13 @@ export const PaymentsApiAxiosParamCreator = function (configuration?: Configurat
},
/**
* Retrives the current user\'s baskets, one per system.
* @param {number} [integrated_system]
* @param {number} [limit] Number of results to return per page.
* @param {number} [offset] The initial index from which to return the results.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
paymentsBasketsList: async (limit?: number, offset?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
paymentsBasketsList: async (integrated_system?: number, limit?: number, offset?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/v0/payments/baskets/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
Expand All @@ -1943,6 +1944,10 @@ export const PaymentsApiAxiosParamCreator = function (configuration?: Configurat
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;

if (integrated_system !== undefined) {
localVarQueryParameter['integrated_system'] = integrated_system;
}

if (limit !== undefined) {
localVarQueryParameter['limit'] = limit;
}
Expand Down Expand Up @@ -2116,13 +2121,14 @@ export const PaymentsApiFp = function(configuration?: Configuration) {
},
/**
* Retrives the current user\'s baskets, one per system.
* @param {number} [integrated_system]
* @param {number} [limit] Number of results to return per page.
* @param {number} [offset] The initial index from which to return the results.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async paymentsBasketsList(limit?: number, offset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedBasketWithProductList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.paymentsBasketsList(limit, offset, options);
async paymentsBasketsList(integrated_system?: number, limit?: number, offset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedBasketWithProductList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.paymentsBasketsList(integrated_system, limit, offset, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PaymentsApi.paymentsBasketsList']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
Expand Down Expand Up @@ -2208,7 +2214,7 @@ export const PaymentsApiFactory = function (configuration?: Configuration, baseP
* @throws {RequiredError}
*/
paymentsBasketsList(requestParameters: PaymentsApiPaymentsBasketsListRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedBasketWithProductList> {
return localVarFp.paymentsBasketsList(requestParameters.limit, requestParameters.offset, options).then((request) => request(axios, basePath));
return localVarFp.paymentsBasketsList(requestParameters.integrated_system, requestParameters.limit, requestParameters.offset, options).then((request) => request(axios, basePath));
},
/**
* Retrieve a basket for the current user.
Expand Down Expand Up @@ -2295,6 +2301,13 @@ export interface PaymentsApiPaymentsBasketsCreateFromProductCreateRequest {
* @interface PaymentsApiPaymentsBasketsListRequest
*/
export interface PaymentsApiPaymentsBasketsListRequest {
/**
*
* @type {number}
* @memberof PaymentsApiPaymentsBasketsList
*/
readonly integrated_system?: number

/**
* Number of results to return per page.
* @type {number}
Expand Down Expand Up @@ -2407,7 +2420,7 @@ export class PaymentsApi extends BaseAPI {
* @memberof PaymentsApi
*/
public paymentsBasketsList(requestParameters: PaymentsApiPaymentsBasketsListRequest = {}, options?: RawAxiosRequestConfig) {
return PaymentsApiFp(this.configuration).paymentsBasketsList(requestParameters.limit, requestParameters.offset, options).then((request) => request(this.axios, this.basePath));
return PaymentsApiFp(this.configuration).paymentsBasketsList(requestParameters.integrated_system, requestParameters.limit, requestParameters.offset, options).then((request) => request(this.axios, this.basePath));
}

/**
Expand Down
4 changes: 4 additions & 0 deletions openapi/specs/v0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ paths:
operationId: payments_baskets_list
description: Retrives the current user's baskets, one per system.
parameters:
- in: query
name: integrated_system
schema:
type: integer
- name: limit
required: false
in: query
Expand Down
25 changes: 23 additions & 2 deletions payments/views/v0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from django_filters import rest_framework as filters
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
Expand Down Expand Up @@ -54,9 +55,24 @@
# Baskets


class BasketFilter(filters.FilterSet):
"""Filter class for Basket, just so we can filter by integrated system."""

class Meta:
"""Meta class for BasketFilter"""

model = Basket
fields = ["integrated_system"]


@extend_schema_view(
list=extend_schema(
description=("Retrives the current user's baskets, one per system.")
description=("Retrives the current user's baskets, one per system."),
parameters=[
OpenApiParameter(
"integrated_system", OpenApiTypes.INT, OpenApiParameter.QUERY
),
],
),
retrieve=extend_schema(
description="Retrieve a basket for the current user.",
Expand All @@ -69,11 +85,16 @@ class BasketViewSet(ReadOnlyModelViewSet):
"""API view set for Basket"""

serializer_class = BasketWithProductSerializer
permission_classes = [IsAuthenticated]
permission_classes = (IsAuthenticated,)
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = BasketFilter

def get_queryset(self):
"""Return only baskets owned by this user."""

if getattr(self, "swagger_fake_view", False):
return Basket.objects.none()

return Basket.objects.filter(user=self.request.user).all()


Expand Down
16 changes: 15 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ djangorestframework-dataclasses = "^1.3.1"
django-countries = "^7.6.1"
mitol-django-geoip = ">=2024.11.05"
py-moneyed = "^3.0"
django-extensions = "^3.2.3"

[tool.poetry.group.dev.dependencies]
bpython = "^0.24"
Expand Down
8 changes: 6 additions & 2 deletions unified_ecommerce/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import logging

from django.contrib.auth import login, logout
from django.contrib.auth.middleware import RemoteUserMiddleware
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
from django.core.exceptions import ImproperlyConfigured

from unified_ecommerce.utils import decode_apisix_headers, get_user_from_apisix_headers

log = logging.getLogger(__name__)


class ApisixUserMiddleware(RemoteUserMiddleware):
class ApisixUserMiddleware(PersistentRemoteUserMiddleware):
"""Checks for and processes APISIX-specific headers."""

def process_request(self, request):
Expand All @@ -28,6 +28,7 @@ def process_request(self, request):
apisix_user = get_user_from_apisix_headers(request)
except KeyError:
if self.force_logout_if_no_header and request.user.is_authenticated:
log.debug("Forcing user logout due to missing APISIX headers.")
logout(request)
return None

Expand All @@ -36,6 +37,9 @@ def process_request(self, request):
# The user is authenticated, but doesn't match the user we got
# from APISIX. So, log them out so the APISIX user takes
# precedence.
log.debug(
"Forcing user logout because request user doesn't match APISIX user"
)

logout(request)

Expand Down
10 changes: 9 additions & 1 deletion unified_ecommerce/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"mitol.mail.apps.MailApp",
"django_countries",
"mitol.geoip",
"django_extensions",
# Application modules
"unified_ecommerce",
"users",
Expand Down Expand Up @@ -133,7 +134,7 @@
LOGIN_URL = "/login"
LOGIN_ERROR_URL = "/login"
LOGOUT_URL = "/logout"
LOGOUT_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/logged_out"

ROOT_URLCONF = "unified_ecommerce.urls"

Expand Down Expand Up @@ -497,6 +498,13 @@
name="MITOL_UE_FORCE_PROFILE_COUNTRY", default=False
)

MITOL_UE_PAYMENT_BASKET_ROOT = get_string(
name="MITOL_UE_PAYMENT_BASKET_ROOT", default="/cart/"
)
MITOL_UE_PAYMENT_BASKET_CHOOSER = get_string(
name="MITOL_UE_PAYMENT_BASKET_CHOOSER", default="/cart/"
)

import_settings_modules("mitol.payment_gateway.settings.cybersource")

# Keycloak API settings
Expand Down
3 changes: 2 additions & 1 deletion unified_ecommerce/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

urlpatterns = [
path("", include("cart.urls")),
path("", include("django.contrib.auth.urls")),
path("auth/", include("django.contrib.auth.urls")),
path("admin/", admin.site.urls),
path("hijack/", include("hijack.urls")),
# OAuth2 Paths
Expand All @@ -35,6 +35,7 @@
# API Paths
re_path(r"", include("payments.urls")),
re_path(r"", include("system_meta.urls")),
re_path(r"", include("users.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

if settings.DEBUG:
Expand Down
10 changes: 10 additions & 0 deletions users/templates/logged_out.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>Logged out</title>
</head>

<body>
<h1>You're logged out.</h1>
</body>
</html>
31 changes: 31 additions & 0 deletions users/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Routes for the users app."""

from django.urls import include, re_path

from users.views import (
CurrentUserRetrieveViewSet,
LoggedOutView,
establish_session,
)

v0_urls = [
re_path(
r"^me/$",
CurrentUserRetrieveViewSet.as_view({"get": "retrieve"}),
name="current_user",
),
]

urlpatterns = [
re_path(
r"^logged_out/$",
LoggedOutView.as_view(),
name="logged_out_page",
),
re_path(
r"^establish_session/$",
establish_session,
name="users-establish_session",
),
re_path(r"^api/v0/users/", include(v0_urls)),
]
Loading

0 comments on commit f529e25

Please sign in to comment.