Skip to content

Commit

Permalink
Merge pull request #99 from mitodl/jkachel/add-payments-api-and-cart-app
Browse files Browse the repository at this point in the history
Adds payment API and cart test mule app
  • Loading branch information
jkachel authored Sep 13, 2024
2 parents 0c5726f + 8d0aaf5 commit ee6dabe
Show file tree
Hide file tree
Showing 43 changed files with 4,545 additions and 343 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ KEYCLOAK_REALM_NAME=
POSTHOG_API_HOST=https://app.posthog.com
POSTHOG_API_TOKEN=

MITOL_UE_PAYMENT_INTERSTITIAL_DEBUG=False

MITOL_PAYMENT_GATEWAY_CYBERSOURCE_ACCESS_KEY=sample-setting
MITOL_PAYMENT_GATEWAY_CYBERSOURCE_PROFILE_ID=sample-setting
MITOL_PAYMENT_GATEWAY_CYBERSOURCE_SECURITY_KEY=sample-setting
Expand Down
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"filename": "docker-compose.yml",
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_verified": false,
"line_number": 7
"line_number": 5
}
],
"flow-typed/npm/jsdom_vx.x.x.js": [
Expand Down Expand Up @@ -163,5 +163,5 @@
}
]
},
"generated_at": "2024-06-20T14:52:01Z"
"generated_at": "2024-07-25T20:16:27Z"
}
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This application provides a central system to handle ecommerce activities across
- [Code Generation](#code-generation)
- [Committing \& Formatting](#committing--formatting)
- [Optional Setup](#optional-setup)
- [Interstitial Debug Mode](#interstitial-debug-mode)
- [Running the app in a notebook](#running-the-app-in-a-notebook)

## Initial Setup
Expand Down Expand Up @@ -160,16 +161,37 @@ DISCOVERY_URL=<OpenID Endpoint Configuration link>
curl "http://127.0.0.1:9180/apisix/admin/upstreams/2" \
-H "X-API-KEY: $API_KEY" -X PUT -d '
{
"type": "roundrobin",
"type": "chash",
"hash_on": "consumer",
"nodes": {
"nginx:8073": 1
}
}'

# Define the Universal Ecommerce unauthenticated route
# This is stuff that doesn't need a session - static resources, and the checkout result API

postbody=$(cat << ROUTE_END
{
"uris": [ "/checkout/result/", "/static" ],
"plugins": {},
"upstream_id": 2,
"priority": 0,
"desc": "Unauthenticated routes, including assets and the checkout callback API",
"name": "ue-unauth"
}
ROUTE_END
)

curl http://127.0.0.1:9180/apisix/admin/routes/ue-unauth -H "X-API-KEY: $API_KEY" -X PUT -d "$postbody"

# Define the Universal Ecommerce wildcard route

postbody=$(cat << ROUTE_END
{
"name": "ue-default",
"desc": "Wildcard route for the rest of the system - authentication required",
"priority": 1,
"uri": "/*",
"plugins":{
"openid-connect":{
Expand All @@ -187,7 +209,7 @@ postbody=$(cat << ROUTE_END
ROUTE_END
)

curl http://127.0.0.1:9180/apisix/admin/routes/ue -H "X-API-KEY: $API_KEY" -X PUT -d $postbody
curl http://127.0.0.1:9180/apisix/admin/routes/ue-default -H "X-API-KEY: $API_KEY" -X PUT -d "$postbody"
```

You should now be able to get to the app via APISIX. There is an internal API at `http://ue.odl.local:9080/_/v0/meta/apisix_test_request/` that you can hit to see if it worked. The wildcard route above will route all UE traffic (or, more correctly, all traffic going into APISIX) through Keycloak and then into UE, so you should also be able to access the Django Admin through it if you've set your Keycloak user to be an admin.
Expand Down Expand Up @@ -226,6 +248,10 @@ pre-commit init-templatedir ~/.git-template

Described below are some setup steps that are not strictly necessary for running Unified Ecommerce.

### Interstitial Debug Mode

You can set `MITOL_UE_PAYMENT_INTERSTITIAL_DEBUG` to control whether or not the checkout interstitial page displays additional data and waits to submit or not. By default, this tracks the `DEBUG` setting (so it should be off in production, and on in local testing).

### Running the app in a notebook

This repo includes a config for running a [Jupyter notebook](https://jupyter.org/) in a Docker container. This enables you to do in a Jupyter notebook anything you might otherwise do in a Django shell. To get started:
Expand Down
101 changes: 93 additions & 8 deletions authentication/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""API functions for authentication."""

import json
import logging

import requests
Expand All @@ -15,11 +16,87 @@
from authentication.models import KeycloakAdminToken
from unified_ecommerce.celery import app
from unified_ecommerce.exceptions import KeycloakAuthError
from unified_ecommerce.utils import decode_x_header

User = get_user_model()
log = logging.getLogger(__name__)


def decode_apisix_headers(request):
"""Decode the APISIX-specific headers."""

try:
apisix_result = decode_x_header(request, "HTTP_X_USERINFO")
if not apisix_result:
log.debug(
"decode_apisix_headers: No APISIX-specific header found",
)
return None
except json.JSONDecodeError:
log.debug(
"decode_apisix_headers: Got bad APISIX-specific header: %s",
request.META.get("HTTP_X_USERINFO", ""),
)

return None

log.debug("decode_apisix_headers: Got %s", apisix_result)

return {
"email": apisix_result["email"],
"preferred_username": apisix_result["sub"],
"given_name": apisix_result["given_name"],
"family_name": apisix_result["family_name"],
}


def get_user_from_apisix_headers(request):
"""Get a user based on the APISIX headers."""

decoded_headers = decode_apisix_headers(request)

if not decoded_headers:
return None

(
email,
preferred_username,
given_name,
family_name,
) = decoded_headers.values()

log.debug("get_user_from_apisix_headers: Authenticating %s", preferred_username)

user, created = User.objects.filter(username=preferred_username).get_or_create(
defaults={
"username": preferred_username,
"email": email,
"first_name": given_name,
"last_name": family_name,
}
)

if created:
log.debug(
"get_user_from_apisix_headers: User %s not found, created new",
preferred_username,
)
user.set_unusable_password()
user.save()
else:
log.debug(
"get_user_from_apisix_headers: Found existing user for %s: %s",
preferred_username,
user,
)

user.first_name = given_name
user.last_name = family_name
user.save()

return user


def keycloak_session_init(url, **kwargs): # noqa: C901
"""
Initialize a Keycloak session.
Expand All @@ -36,8 +113,7 @@ def keycloak_session_init(url, **kwargs): # noqa: C901
"""

token_url = (
f"{settings.KEYCLOAK_ADMIN_URL}/auth/realms/master/"
"protocol/openid-connect/token"
f"{settings.KEYCLOAK_ADMIN_URL}/realms/master/protocol/openid-connect/token"
)
client = BackendApplicationClient(client_id=settings.KEYCLOAK_ADMIN_CLIENT_ID)

Expand All @@ -46,6 +122,8 @@ def keycloak_session_init(url, **kwargs): # noqa: C901
"client_secret": settings.KEYCLOAK_ADMIN_CLIENT_SECRET,
}

log.debug("Token URL is %s", token_url)

def update_token(token):
log.debug("Refreshing Keycloak token %s", token)
KeycloakAdminToken.objects.all().delete()
Expand Down Expand Up @@ -83,28 +161,25 @@ def regenerate_token(client):
except InvalidGrantError:
log.exception(
(
"keycloak_session_init couldn't refresh token %s because of an"
"keycloak_session_init couldn't refresh token because of an"
" invalid grant error"
),
token,
)
return None
except TokenExpiredError:
log.exception(
(
"keycloak_session_init couldn't refresh token %s because of an"
"keycloak_session_init couldn't refresh token because of an"
" expired token error"
),
token,
)
return None
except requests.exceptions.RequestException:
log.exception(
(
"keycloak_session_init couldn't refresh token %s because of an"
"keycloak_session_init couldn't refresh token because of an"
" HTTP error"
),
token,
)
return None

Expand All @@ -129,6 +204,13 @@ def regenerate_token(client):

session = regenerate_token(client)

if not session:
log.exception(
"keycloak_session_init: attempted to regenerate the token, but"
" failed to"
)
return None

keycloak_response = session.get(url, **kwargs).json()
except requests.exceptions.RequestException:
log.exception(
Expand All @@ -139,6 +221,9 @@ def regenerate_token(client):
token,
)
return None
except AttributeError:
log.exception("keycloak_session_init failed")
return None

log.debug("Keycloak response: %s", keycloak_response)

Expand Down
4 changes: 2 additions & 2 deletions authentication/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ def authenticate(self, request, remote_user):
log.debug("KeycloakRemoteUserBackend is running for %s", remote_user)

userinfo_url = (
f"{settings.KEYCLOAK_ADMIN_URL}/auth/admin/"
f"realms/{settings.KEYCLOAK_REALM}/users/"
f"{settings.KEYCLOAK_ADMIN_URL}"
f"/admin/realms/{settings.KEYCLOAK_REALM}/users/"
)

if not remote_user:
Expand Down
Empty file added cart/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions cart/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""App initialization for cart"""

from django.apps import AppConfig


class CartConfig(AppConfig):
"""Config for the cart app"""

default_auto_field = "django.db.models.BigAutoField"
name = "cart"
Empty file added cart/migrations/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions cart/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<head>
<title>MIT ODL Ecommerce - {% block title %}{% endblock title %}</title>

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous"><!-- pragma: allowlist secret -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script><!-- pragma: allowlist secret -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body class="bg-body-secondary">
<div id="body-container" class="w-75 mx-auto flex container mt-5 bg-light p-4">
<div class="row mb-3">
<div class="col-12">
<h1 class="border-bottom border-5 border-black">MIT ODL Ecommerce - {% block innertitle %}{% endblock innertitle %}</h1>
</div>
</div>
{% block body %}{% endblock body %}
</div>
</body>
</html>
Loading

0 comments on commit ee6dabe

Please sign in to comment.