diff --git a/apps/home/templates/authenticated-home.html b/apps/home/templates/authenticated-home.html index 1572d0296..7326a5425 100644 --- a/apps/home/templates/authenticated-home.html +++ b/apps/home/templates/authenticated-home.html @@ -81,6 +81,13 @@

My Sandbox Apps

View/Edit App
+
{% csrf_token %} + + +
+
+
{% endfor %} {% if not applications %} diff --git a/apps/testclient/templates/3rd_party_sample_app.html b/apps/testclient/templates/3rd_party_sample_app.html new file mode 100644 index 000000000..55342429b --- /dev/null +++ b/apps/testclient/templates/3rd_party_sample_app.html @@ -0,0 +1,7732 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load waffle_tags %} + + +{% block banner %}{% endblock %} + +{% block Content %} + + +
+
+
+
+

Blue Button 2.0 Sample App +

+
+ Medicare Prescription Drug Claims Data +
+
+ {% if user.is_authenticated %} +
+
+

Log out to continue

+ + Log Out to Continue +
+
+ {% endif %} +
+ +
+

Patient information

+
+
+
+

Clinic records

+
+ Profile avatar +
    +
  • John Doe
  • +
  • Springfield General Hospital
  • +
  • Dr. Hibbert
  • +
+
+
+
+
+
+

Medicare Prescription Drug Records

+
+ Chart icon +

+ John, you can now allow Springfield General Hospital + access to your Medicare prescription drug records! +

+
+
+
+

Add your Medicare Prescription Drug data

+
+
+
+
+
+
{% csrf_token %} +
+
+
+ + +
+
+
+
+
+
+
+ + + + + + + + + + + {% for claim in claims %} + + + + + + {% endfor %} + +
+ Medicare Prescription Drug Claims Data +
+ NDC Code + + Prescription Drug Name + + Cost +
{{ claim.code }}{{ claim.name }}{{ claim.cost }}
+
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/apps/testclient/templates/authorize2.html b/apps/testclient/templates/authorize2.html new file mode 100644 index 000000000..c13f9f8fe --- /dev/null +++ b/apps/testclient/templates/authorize2.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block banner %}{% endblock %} + +{% block bannerDescription %} +{% endblock %} + +{% block Content %} +
+
+
+ {% if user.is_authenticated %} +
+
+

Log out to continue

+ + Log Out to Continue +
+
+ {% endif %} +
+
+
+
+
{% csrf_token %} +
+
+
+ +
+ +
+
+
+
+
+
+
+
+

MyApp Web Client

+

The MyApp Client allows you to see what it is like for a Medicare beneficiary to grant access to your app. It allows you to see sample data returned from our various API endpoints.

+

You need to sign up and register the app on Blue Button 2.0 API Sandbox, and add 'https://sandbox.bluebutton.cms.gov/myapp/callback' to the 'redirect_urls' of the app

+

To start authorization, provide your app's credential on the left and click "Authorize as a Beneficiary"

+
+
+
+
+
+
+{% endblock %} diff --git a/apps/testclient/templates/home.html b/apps/testclient/templates/home.html index d204a257f..bc2293266 100644 --- a/apps/testclient/templates/home.html +++ b/apps/testclient/templates/home.html @@ -44,72 +44,45 @@

Step 1: Sample Authorization


- Get a Sample Authorization Token + Get a Sample Authorization Token {% switch "testclient_v2" %} - Get a Sample Authorization Token for v2 + Get a Sample Authorization Token for v2 {% endswitch %}
- Get a Sample Authorization Token (PKCE Enabled) + Get a Sample Authorization Token (PKCE Enabled) {% switch "testclient_v2" %} - Get a Sample Authorization Token for v2 (PKCE Enabled) + Get a Sample Authorization Token for v2 (PKCE Enabled) {% endswitch %} {% endif %} {% if session_token is not None %} - {% url 'testclient-restart' as restart_url %} - {% if api_ver == 'v2' %} - {% url 'authorize_link_v2' as auth_url %} - {% url 'test_metadata_v2' as meta_url %} - {% url 'test_openid_config_v2' as openid_cfg_url %} - {% url 'test_userinfo_v2' as test_userinfo_url %} - {% url 'test_eob_v2' as test_eob_url %} - {% url 'test_patient_v2' as test_patient_url %} - {% url 'test_coverage_v2' as test_coverage_url %} - {% else %} - {% url 'authorize_link' as auth_url %} - {% url 'test_metadata' as meta_url %} - {% url 'test_openid_config' as openid_cfg_url %} - {% url 'test_userinfo' as test_userinfo_url %} - {% url 'test_eob' as test_eob_url %} - {% url 'test_patient' as test_patient_url %} - {% url 'test_coverage' as test_coverage_url %} - {% endif %} - -

Success! You have a token. Now you can make API calls below. Or you can repeat this step if you need a new token. or restart testclient

- -
+

Success! You have a token. Now you can make API calls below. Or you can repeat this step if you need a new token. or restart testclient

+
+
{{ session_token }}
+
-
{{ session_token }}
- -
- -

Step 2: API Calls

+

Step 2: API Calls

-

Once you've completed step one and have an authorization token, you can click on any of the links below to simulate calls to different endpoints and see the sample data that is delivered in the response.

- - - -

Additional Resources

- -

If you need more information about the API or the sample data, feel free to read our developer documentation.

- -

Testing your own application:

- -

If you want to test the Blue Button 2.0 API with your own application, create a Sandbox account and register your application. Once registered, you can use your test credentials to re-create this experience and include synthetic Medicare data in your own application.

- - Create a Sandbox Account - +

Once you've completed step one and have an authorization token, you can click on any of the links below to simulate calls to different endpoints and see the sample data that is delivered in the response.

+ + + +

Additional Resources

+

If you need more information about the API or the sample data, feel free to read our developer documentation.

+

Testing your own application:

+

If you want to test the Blue Button 2.0 API with your own application, create a Sandbox account and register your application. Once registered, you can use your test credentials to re-create this experience and include synthetic Medicare data in your own application.

+ Create a Sandbox Account {% endif %}
diff --git a/apps/testclient/templates/home2.html b/apps/testclient/templates/home2.html new file mode 100644 index 000000000..280cd08f6 --- /dev/null +++ b/apps/testclient/templates/home2.html @@ -0,0 +1,90 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load waffle_tags %} + +{% block bannerBackButton %}{% endblock %} + +{% block bannerTitle %} +MyApp Web Client for Blue Button 2.0 API +{% endblock %} + +{% block bannerCallToActionButtons %} +{% endblock %} + +{% block Content %} +
+
+
+ + {% if user.is_authenticated %} +
+
+

Log out to continue

+ + Log Out to Continue +
+
+ {% endif %} + + {% if app_name is not None %} +

App Name: {{ app_name }}

+ {% endif %} + + {% if app_dag_type is not None %} +

Data Access Grant Type: {{ app_dag_type }}

+ {% endif %} + + {% if app_req_demo is not None %} +

App Require Demographic Data: {{ app_req_demo }}

+ {% endif %} + + {% if app_pkce_method is not None %} +

PKCE method: {{ app_pkce_method }}

+ {% endif %} + + + {% if session_token is None %} +

Authorized: No

+ {% else %} +

Authorized: Yes

+ {% endif %} + + + {% if session_token is not None %} +

Success! You have a token. Now you can make API calls below. Or you can or restart my app authorization.

+
+
{{ session_token }}
+
+

You can call API endpoints below:

+

Once you are authorized by the beneficiary (obtained an data access grant), you can click on any of the links below to simulate calls to different endpoints and see the sample data that is delivered in the response.

+ + + +

Additional Resources

+ +

If you need more information about the API or the sample data, feel free to read our developer documentation.

+ +

Testing your own application:

+ +

If you want to test the Blue Button 2.0 API with your own application, create a Sandbox account and register your application. Once registered, you can use your test credentials to re-create this experience and include synthetic Medicare data in your own application.

+ + Create a Sandbox Account + + {% endif %} + +
+
+
+
+ +{% endblock %} diff --git a/apps/testclient/urls4myapp.py b/apps/testclient/urls4myapp.py new file mode 100644 index 000000000..acabd4d8d --- /dev/null +++ b/apps/testclient/urls4myapp.py @@ -0,0 +1,43 @@ +from django.urls import path +from .views import ( + authorize_link, + authorize_link_v2, + restart, + callback, + test_eob, + test_eob_v2, + test_userinfo, + test_userinfo_v2, + test_metadata, + test_metadata_v2, + test_openid_config, + test_openid_config_v2, + test_coverage, + test_coverage_v2, + test_patient, + test_patient_v2, + test_links, + authorize, +) + +# not using url reverse in POC but provide it here anyways - passing url reverse tests +urlpatterns = [ + path("authorize", authorize, name="testclient-authorize-4-myapp"), + path("restart", restart, name="testclient-restart-4-myapp"), + path("callback", callback, name="testclient-callback-4-myapp"), + path("authorize-link", authorize_link, name="authorize_link-4-myapp"), + path("authorize-link-v2", authorize_link_v2, name="authorize_link_v2-4-myapp"), + path("", test_links, name="test_links-4-myapp"), + path("ExplanationOfBenefit", test_eob, name="test_eob-4-myapp"), + path("Patient", test_patient, name="test_patient-4-myapp"), + path("Coverage", test_coverage, name="test_coverage-4-myapp"), + path("userinfo", test_userinfo, name="test_userinfo-4-myapp"), + path("metadata", test_metadata, name="test_metadata-4-myapp"), + path("openidConfig", test_openid_config, name="test_openid_config-4-myapp"), + path("ExplanationOfBenefitV2", test_eob_v2, name="test_eob_v2-4-myapp"), + path("PatientV2", test_patient_v2, name="test_patient_v2-4-myapp"), + path("CoverageV2", test_coverage_v2, name="test_coverage_v2-4-myapp"), + path("userinfoV2", test_userinfo_v2, name="test_userinfo_v2-4-myapp"), + path("metadataV2", test_metadata_v2, name="test_metadata_v2-4-myapp"), + path("openidConfigV2", test_openid_config_v2, name="test_openid_config_v2-4-myapp"), +] diff --git a/apps/testclient/urlsfor3rdapp.py b/apps/testclient/urlsfor3rdapp.py new file mode 100644 index 000000000..de232c4a1 --- /dev/null +++ b/apps/testclient/urlsfor3rdapp.py @@ -0,0 +1,44 @@ +from django.urls import path +from .views import ( + authorize_link, + authorize_link_v2, + restart, + callback, + test_eob, + test_eob_v2, + test_userinfo, + test_userinfo_v2, + test_metadata, + test_metadata_v2, + test_openid_config, + test_openid_config_v2, + test_coverage, + test_coverage_v2, + test_patient, + test_patient_v2, + test_links, + launch3rdapp, + authorize, +) + +urlpatterns = [ + path("authorize", authorize, name="testclient-authorize-4-3rdapp"), + path("launch", launch3rdapp, name="testclient-launch-for-3rdapp"), + path("restart", restart, name="testclient-restart-for-3rdapp"), + path("callback", callback, name="testclient-callback-for-3rdapp"), + path("authorize-link", authorize_link, name="authorize_link-for-3rdapp"), + path("authorize-link-v2", authorize_link_v2, name="authorize_link_v2-for-3rdapp"), + path("", test_links, name="test_links-for-3rdapp"), + path("ExplanationOfBenefit", test_eob, name="test_eob-for-3rdapp"), + path("Patient", test_patient, name="test_patient-for-3rdapp"), + path("Coverage", test_coverage, name="test_coverage-for-3rdapp"), + path("userinfo", test_userinfo, name="test_userinfo-for-3rdapp"), + path("metadata", test_metadata, name="test_metadata-for-3rdapp"), + path("openidConfig", test_openid_config, name="test_openid_config-for-3rdapp"), + path("ExplanationOfBenefitV2", test_eob_v2, name="test_eob_v2-for-3rdapp"), + path("PatientV2", test_patient_v2, name="test_patient_v2-for-3rdapp"), + path("CoverageV2", test_coverage_v2, name="test_coverage_v2-for-3rdapp"), + path("userinfoV2", test_userinfo_v2, name="test_userinfo_v2-for-3rdapp"), + path("metadataV2", test_metadata_v2, name="test_metadata_v2-for-3rdapp"), + path("openidConfigV2", test_openid_config_v2, name="test_openid_config_v2-for-3rdapp"), +] diff --git a/apps/testclient/utils.py b/apps/testclient/utils.py index 89104e227..93e0c1838 100644 --- a/apps/testclient/utils.py +++ b/apps/testclient/utils.py @@ -9,23 +9,32 @@ from ..dot_ext.models import Application -def test_setup(include_client_secret=True, v2=False, pkce=False): +def test_setup(include_client_secret=True, v2=False, pkce=False, client_id=None, path=None): response = OrderedDict() ver = 'v2' if v2 else 'v1' response['api_ver'] = ver - oa2client = Application.objects.get(name="TestApp") - response['client_id'] = oa2client.client_id - - if include_client_secret: - response['client_secret'] = oa2client.client_secret host = getattr(settings, 'HOSTNAME_URL', 'http://localhost:8000') if not (host.startswith("http://") or host.startswith("https://")): host = "https://" + host + if client_id and path.startswith('/myapp/'): + oa2client = Application.objects.get(client_id=client_id) + response['redirect_uri'] = '{}{}'.format(host, settings.MYAPP_REDIRECT_URI) + elif client_id and path.startswith('/3rdapp/'): + oa2client = Application.objects.get(client_id=client_id) + response['redirect_uri'] = '{}{}'.format(host, settings.APP3RD_REDIRECT_URI) + else: + oa2client = Application.objects.get(name="TestApp") + response['redirect_uri'] = '{}{}'.format(host, settings.TESTCLIENT_REDIRECT_URI) + + response['client_id'] = oa2client.client_id + + if include_client_secret: + response['client_secret'] = oa2client.client_secret + response['resource_uri'] = host - response['redirect_uri'] = '{}{}'.format(host, settings.TESTCLIENT_REDIRECT_URI) response['coverage_uri'] = '{}/{}/fhir/Coverage/'.format(host, ver) if pkce: @@ -44,8 +53,13 @@ def test_setup(include_client_secret=True, v2=False, pkce=False): return (response) -def get_client_secret(): - oa2client = Application.objects.get(name="TestApp") +def get_app_info_by_id(client_id): + oa2client = Application.objects.get(client_id=client_id) + return oa2client.name, oa2client.data_access_type, oa2client.require_demographic_scopes, oa2client.client_secret_plain + + +def get_client_secret(app_name): + oa2client = Application.objects.get(name=app_name) return oa2client.client_secret_plain diff --git a/apps/testclient/views.py b/apps/testclient/views.py index e6cf45cb0..a05e13712 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -7,22 +7,41 @@ from django.shortcuts import render, redirect from django.urls import reverse from django.views.decorators.cache import never_cache +from ipware import get_client_ip from oauthlib.oauth2.rfc6749.errors import MissingTokenError +from oauth2_provider.models import get_application_model from requests_oauthlib import OAuth2Session from rest_framework import status from waffle.decorators import waffle_switch -from .utils import test_setup, get_client_secret +from .utils import test_setup, get_client_secret, get_app_info_by_id from apps.dot_ext.loggers import cleanup_session_auth_flow_trace from apps.fhir.bluebutton.views.home import fhir_conformance, fhir_conformance_v2 from apps.wellknown.views.openid import openid_configuration import apps.logging.request_logger as bb2logging + +Application = get_application_model() + logger = logging.getLogger(bb2logging.HHS_SERVER_LOGNAME_FMT.format(__name__)) HOME_PAGE = "home.html" +HOME2_PAGE = "home2.html" +CLINIC_PAGE = "3rd_party_sample_app.html" +AUTH_PAGE = "authorize2.html" RESULTS_PAGE = "results.html" +MYAPP_ROOT_PATH = "/myapp/" +APP3RD_ROOT_PATH = "/3rdapp/" +TESTCLIENT_ROOT_PATH = "/testclient/" + +# hacky app token registry +# path -> ip -> app : token +CLIENT2TOKEN_MAP = { + TESTCLIENT_ROOT_PATH: {}, + MYAPP_ROOT_PATH: {}, + APP3RD_ROOT_PATH: {} +} ENDPOINT_URL_FMT = { "userinfo": "{}/{}/connect/userinfo", @@ -96,8 +115,29 @@ def _get_oauth2_session_with_token(request): def _get_oauth2_session_with_redirect(request): - return OAuth2Session( - request.session['client_id'], redirect_uri=request.session['redirect_uri']) + return OAuth2Session(request.session['client_id'], redirect_uri=request.session['redirect_uri']) + + +@waffle_switch('enable_testclient') +def launch3rdapp(request): + # 3rd party app launched from SBX account, assume every launch is a fresh session + if 'token' in request.session: + del request.session['token'] + + client_id = None + client_secret = None + + for key in request.POST: + if key.startswith('id-3rdapp-'): + client_id = request.POST[key] + elif key.startswith('secret-3rdapp-'): + client_secret = request.POST[key] + + if client_secret and client_id: + return render(request, CLINIC_PAGE, context={"client_id": client_id, "client_secret": client_secret}) + else: + return JsonResponse({"error": "Invalid app launch: missing app credential"}, + status=status.HTTP_400_BAD_REQUEST) @waffle_switch('enable_testclient') @@ -106,17 +146,25 @@ def restart(request): if 'token' in request.session: del request.session['token'] - return render(request, HOME_PAGE, context={"session_token": None}) + return render(request, AUTH_PAGE if request.path.startswith(MYAPP_ROOT_PATH) else HOME_PAGE, context={"session_token": None}) @waffle_switch('enable_testclient') def callback(request): # Authorization has been denied or another error has occured, remove token if existing # and redirect to home page view to force re-authorization + app_name = request.session.get('auth_app_name') + app_dag_type = request.session.get('auth_app_data_access_type') + app_pkce_method = request.session.get('auth_pkce_method') + app_req_demo = request.session.get('auth_require_demographic_scopes') + if 'error' in request.GET: if 'token' in request.session: del request.session['token'] - return redirect('test_links', permanent=True) + if request.path == MYAPP_ROOT_PATH: + return redirect(MYAPP_ROOT_PATH, permanent=True) + else: + return redirect(TESTCLIENT_ROOT_PATH, permanent=True) oas = _get_oauth2_session_with_redirect(request) @@ -130,10 +178,26 @@ def callback(request): token_uri += reverse('oauth2_provider_v2:token-v2') \ if request.session.get('api_ver', 'v1') == 'v2' else reverse('oauth2_provider:token') + secret = None + # need client secret here + if app_name: + secret = get_client_secret(app_name) + + if secret is None: + cid = request.session.get('client_id') + if cid: + app_name, app_dag_type, app_req_demo, secret = get_app_info_by_id(cid) + request.session['auth_app_name'] = app_name + request.session['auth_app_data_access_type'] = app_dag_type + request.session['auth_require_demographic_scopes'] = app_req_demo + + if secret is None: + raise Exception("Invalid state: missing info to get access token...") + try: cv = request.session.get('code_verifier') token = oas.fetch_token(token_uri, - client_secret=get_client_secret(), + client_secret=secret, authorization_response=auth_uri, code_verifier=cv if cv else '') except MissingTokenError: @@ -163,24 +227,206 @@ def callback(request): userinfo = {'patient': token.get('patient', None)} request.session['patient'] = userinfo.get('patient', token.get('patient', None)) - # We are done using auth flow trace, clear from the session. cleanup_session_auth_flow_trace(request) # Successful token response, redirect to home page view - return redirect('test_links', permanent=True) + request.session['auth_app_name'] = app_name + tk = request.session.get('token') + if request.path.startswith('/myapp/callback'): + # authorized myapp, redirect to page where app info and FHIR links are ready + token = _update_client2token_map(request, MYAPP_ROOT_PATH, app_name, new_token=tk) + return render(request, HOME2_PAGE, + context={ + "session_token": tk, + "api_ver": 'v2', + "app_name": app_name, + "app_dag_type": app_dag_type, + "app_req_demo": app_req_demo, + "app_pkce_method": app_pkce_method, + "api_ver_ending": 'V2', + "api_ver_suffix": '-v2' + }) + elif request.path.startswith('/3rdapp/callback'): + # authorized 3rdapp, redirect to page where clinician see claims e.g. + token = _update_client2token_map(request, APP3RD_ROOT_PATH, app_name, new_token=tk) + return redirect(APP3RD_ROOT_PATH, permanent=True) + elif request.path.startswith('/testclient/callback'): + # authorized testclient, redirect to page where FHIR links are ready + token = _update_client2token_map(request, TESTCLIENT_ROOT_PATH, app_name, new_token=tk) + return redirect(TESTCLIENT_ROOT_PATH, permanent=True) + else: + return JsonResponse({"error": "Invalid callback path: {}".format(request.path)}, + status=status.HTTP_400_BAD_REQUEST) + + +@waffle_switch('enable_testclient') +def authorize(request, **kwargs): + if request.method == 'POST': + if request.path.startswith(MYAPP_ROOT_PATH): + # POST /myapp (default to en locale): parse form for client creds, and + # generate authorization URL and redirect to medicare login + client_id = request.POST.get('client_id') + client_secret = request.POST.get('client_secret') + if (client_id is not None and client_id.strip() != "" + and client_secret is not None and client_secret.strip() != ""): + app = None + try: + app = Application.objects.get(client_id=client_id) + except Application.DoesNotExist: + pass + if app: + # validate + if app.name == 'TestApp': + # Also consider exclude other internal apps: e.g. newrelic + return JsonResponse({"error": "Not Authorized."}, + status=status.HTTP_401_UNAUTHORIZED) + if (client_secret == app.client_secret_plain): + # generate auth url and redirect + request.session.update(test_setup(v2=True, pkce='true', client_id=client_id, path=request.path)) + request.session['auth_app_name'] = app.name + request.session['auth_app_data_access_type'] = app.data_access_type + request.session['auth_require_demographic_scopes'] = app.require_demographic_scopes + auth_url = _generate_auth_url(request, True) + return redirect(auth_url) + else: + return JsonResponse({"error": "No app found matching the info provided."}, + status=status.HTTP_404_NOT_FOUND) + else: + return JsonResponse({"error": "No app found matching the info provided."}, + status=status.HTTP_404_NOT_FOUND) + else: + return JsonResponse({"error": "Both client_id, and client_secret required."}, + status=status.HTTP_400_BAD_REQUEST) + elif request.path.startswith(APP3RD_ROOT_PATH): + # POST /3rdapp/ (default to en locale): parse form for client creds, and + # generate authorization URL and redirect to medicare login + client_id = request.POST.get('client_id') + client_secret = request.POST.get('client_secret') + if (client_id is not None and client_id.strip() != "" + and client_secret is not None and client_secret.strip() != ""): + app = None + try: + app = Application.objects.get(client_id=client_id) + except Application.DoesNotExist: + pass + if app: + # validate + if app.name == 'TestApp': + # Also consider exclude other internal apps: e.g. newrelic + return JsonResponse({"error": "Not Authorized."}, + status=status.HTTP_401_UNAUTHORIZED) + if (client_secret == app.client_secret_plain): + # generate auth url and redirect + request.session.update(test_setup(v2=True, pkce='true', client_id=client_id, path=request.path)) + auth_url = _generate_auth_url(request, True) + return redirect(auth_url) + else: + return JsonResponse({"error": "No app found matching the info provided."}, + status=status.HTTP_404_NOT_FOUND) + else: + return JsonResponse({"error": "No app found matching the info provided."}, + status=status.HTTP_404_NOT_FOUND) + else: + return JsonResponse({"error": "Both client_id, and client_secret required."}, + status=status.HTTP_400_BAD_REQUEST) + else: + # a bad request, just return json for POC + # testclient path does not accept POST + return JsonResponse({"error": "Unexpected request method: {}, path: {}".format(request.method, request.path)}, + status=status.HTTP_400_BAD_REQUEST) # New home page view consolidates separate success, error, and access denied views @waffle_switch('enable_testclient') def test_links(request, **kwargs): - # If authorization was successful, pass token to template - if 'token' in request.session: - return render(request, HOME_PAGE, - context={"session_token": request.session['token'], - "api_ver": request.session.get('api_ver', 'v1')}) + if request.method == 'GET': + # If authorization was successful, pass token to template + tk = request.session.get('token', None) + if tk: + # check path: + # GET /myapp: load the MyApp authorization page + # otherwise, load the testclient landing page (the page with 4 authorize buttons) + # need to check the app name associated with the token + app_name = request.session.get('auth_app_name') + if request.path == MYAPP_ROOT_PATH: + token = _update_client2token_map(request, MYAPP_ROOT_PATH, app_name) + if token and token['access_token'] == tk['access_token']: + # token found, render data + app_name = request.session.get('auth_app_name') + app_dag_type = request.session.get('auth_app_data_access_type') + app_pkce_method = request.session.get('auth_pkce_method') + app_req_demo = request.session.get('auth_require_demographic_scopes') + return render(request, HOME2_PAGE, + context={"session_token": tk, + "api_ver": 'v2', + "app_name": app_name, + "app_dag_type": app_dag_type, + "app_req_demo": app_req_demo, + "app_pkce_method": app_pkce_method}) + else: + # token is for other apps, show AUTH_PAGE + return render(request, AUTH_PAGE, context={}) + elif request.path == APP3RD_ROOT_PATH: + token = _update_client2token_map(request, APP3RD_ROOT_PATH, app_name) + if token and token['access_token'] == tk['access_token']: + # token found fetch claims + eob = _get_data_json(request, 'eob', [request.session['resource_uri'], 'v2']) + # extract claims from eob (bundle of eob resources) + claims = _extract_claims(eob) + return render(request, CLINIC_PAGE, + context={'claims': claims}) + else: + return render(request, CLINIC_PAGE, + context={}) + else: + token = _update_client2token_map(request, TESTCLIENT_ROOT_PATH, 'TestApp') + if token and token['access_token'] == tk['access_token']: + # show data end points page + ver = request.session.get('api_ver', 'v1') + return render(request, HOME_PAGE, + context={"session_token": tk, + "api_ver": ver, + "api_ver_ending": "V2" if ver == 'v2' else "", + "api_ver_suffix": "-v2" if ver == 'v2' else ""} + ) + else: + # show the 4 buttons page + return render(request, HOME_PAGE, context={"session_token": None}) + else: + # fresh home or auth page, there is no token + if request.path == MYAPP_ROOT_PATH: + return render(request, AUTH_PAGE, context={}) + elif request.path == APP3RD_ROOT_PATH: + return render(request, CLINIC_PAGE, context={}) + elif request.path == TESTCLIENT_ROOT_PATH: + return render(request, HOME_PAGE, context={"session_token": None}) + else: + return JsonResponse({"error": "Invalid path: {}".format(request.path)}, + status=status.HTTP_400_BAD_REQUEST) else: - return render(request, HOME_PAGE, context={"session_token": None}) + # a bad request, only GET accepted, just return json for POC + return JsonResponse({"error": "Unexpected method: {}".format(request.method)}, + status=status.HTTP_400_BAD_REQUEST) + + +# helper: register the app : token pair under path -> ip context +def _update_client2token_map(request, root, app_name, new_token=None): + if app_name is None: + raise Exception("App name required.") + tk = None + ip, _ = get_client_ip(request) + + if CLIENT2TOKEN_MAP.get(root).get(ip) is None: + CLIENT2TOKEN_MAP.get(root)[ip] = {} + + app2token = CLIENT2TOKEN_MAP.get(root)[ip] + + tk = app2token.get(app_name) + + if new_token: + app2token[app_name] = new_token + return tk @never_cache @@ -194,7 +440,10 @@ def test_userinfo_v2(request): def test_userinfo(request, version=1): if 'token' not in request.session: - return redirect('test_links', permanent=True) + if request.path == MYAPP_ROOT_PATH: + return redirect(MYAPP_ROOT_PATH, permanent=True) + else: + return redirect(TESTCLIENT_ROOT_PATH, permanent=True) params = [request.session['resource_uri'], 'v1' if version == 1 else 'v2'] @@ -219,7 +468,9 @@ def test_metadata(request, v2=False): return render(request, RESULTS_PAGE, {"fhir_json_pretty": json.dumps(json_response, indent=3), "response_type": "FHIR Metadata", - "api_ver": "v2" if v2 else "v1"}) + "api_ver": "v2" if v2 else "v1", + "api_ver_ending": "V2" if v2 else "", + "api_ver_suffix": "-v2" if v2 else ""}) @never_cache @@ -236,7 +487,9 @@ def test_openid_config(request, v2=False): return render(request, RESULTS_PAGE, {"fhir_json_pretty": json.dumps(json_response, indent=3), "response_type": "OIDC Discovery", - "api_ver": "v2" if v2 else "v1"}) + "api_ver": "v2" if v2 else "v1", + "api_ver_ending": "V2" if v2 else "", + "api_ver_suffix": "-v2" if v2 else ""}) @never_cache @@ -250,7 +503,10 @@ def test_coverage_v2(request): def test_coverage(request, version=1): if 'token' not in request.session: - return redirect('test_links', permanent=True) + if request.path == MYAPP_ROOT_PATH: + return redirect(MYAPP_ROOT_PATH, permanent=True) + else: + return redirect(TESTCLIENT_ROOT_PATH, permanent=True) coverage = _get_data_json(request, 'coverage', [request.session['resource_uri'], 'v1' if version == 1 else 'v2']) @@ -266,7 +522,9 @@ def test_coverage(request, version=1): "nav_list": nav_info, "page_loc": _get_page_loc(request, coverage), "response_type": "Bundle of Coverage", "total_resource": coverage.get('total', 0), - "api_ver": "v2" if version == 2 else "v1"}) + "api_ver": "v2" if version == 2 else "v1", + "api_ver_ending": "V2" if version == 2 else "", + "api_ver_suffix": "-v2" if version == 2 else ""}) @never_cache @@ -280,16 +538,21 @@ def test_patient_v2(request): def test_patient(request, version=1): if 'token' not in request.session: - return redirect('test_links', permanent=True) + if request.path == MYAPP_ROOT_PATH: + return redirect(MYAPP_ROOT_PATH, permanent=True) + else: + return redirect(TESTCLIENT_ROOT_PATH, permanent=True) params = [request.session['resource_uri'], 'v1' if version == 1 else 'v2', request.session['patient']] patient = _get_data_json(request, 'patient', params) - + # result page only use api_ver, other context provided anyways return render(request, RESULTS_PAGE, {"fhir_json_pretty": json.dumps(patient, indent=3), "response_type": "Patient", - "api_ver": "v2" if version == 2 else "v1"}) + "api_ver": "v2" if version == 2 else "v1", + "api_ver_ending": "V2" if version == 2 else "", + "api_ver_suffix": "-v2" if version == 2 else ""}) @never_cache @@ -302,7 +565,10 @@ def test_eob_v2(request): @waffle_switch('enable_testclient') def test_eob(request, version=1): if 'token' not in request.session: - return redirect('test_links', permanent=True) + if request.path == MYAPP_ROOT_PATH: + return redirect(MYAPP_ROOT_PATH, permanent=True) + else: + return redirect(TESTCLIENT_ROOT_PATH, permanent=True) params = [request.session['resource_uri'], 'v1' if version == 1 else 'v2'] @@ -320,7 +586,9 @@ def test_eob(request, version=1): "nav_list": nav_info, "page_loc": _get_page_loc(request, eob), "response_type": "Bundle of ExplanationOfBenefit", "total_resource": eob.get('total', 0), - "api_ver": "v2" if version == 2 else "v1"}) + "api_ver": "v2" if version == 2 else "v1", + "api_ver_ending": "V2" if version == 2 else "", + "api_ver_suffix": "-v2" if version == 2 else ""}) @never_cache @@ -333,7 +601,16 @@ def authorize_link_v2(request): @waffle_switch('enable_testclient') def authorize_link(request, v2=False): pkce_enabled = request.GET.get('pkce') - request.session.update(test_setup(v2=v2, pkce=pkce_enabled)) + request.session.update(test_setup(v2=v2, pkce=pkce_enabled, path=request.path)) + authorization_url = _generate_auth_url(request, pkce_enabled) + if request.path.startswith('/myapp/authorize-link-v2'): + return render(request, 'authorize2.html', {}) + else: + return render(request, 'authorize.html', + {"authorization_url": authorization_url, "api_ver": "v2" if v2 else "v1"}) + + +def _generate_auth_url(request, pkce_enabled): oas = _get_oauth2_session_with_redirect(request) authorization_url = None if pkce_enabled: @@ -345,6 +622,36 @@ def authorize_link(request, v2=False): else: authorization_url = oas.authorization_url( request.session['authorization_uri'])[0] - - return render(request, 'authorize.html', - {"authorization_url": authorization_url, "api_ver": "v2" if v2 else "v1"}) + return authorization_url + + +# POC: extract claims as list of tuples (code, drug name, cost) +# refer to sample app ts logic for extracting claims: +# code: resource.item[0]?.productOrService?.coding[0]?.code || 'Unknown', +# display: resource.item[0]?.productOrService?.coding[0]?.display || 'Unknown Prescription Drug', +# amount: resource.item[0]?.adjudication[7]?.amount?.value || '0' +def _extract_claims(eob): + claims = [] + if eob and eob.get('entry'): + for r in eob['entry']: + items = r['resource']['item'] + # only sampling element 0 for demo purpose + if items and items[0]: + prod_and_service = items[0]['productOrService'] + adjudications = items[0]['adjudication'] + if prod_and_service and adjudications: + claim = {'code': 'Unknown', 'name': 'Unknown Prescription Drug', 'cost': '0'} + coding = prod_and_service['coding'] + if coding and coding[0]: + claim['code'] = coding[0].get('code', 'Unknown') + claim['name'] = coding[0].get('display', 'Unknown Prescription Drug') + if adjudications and len(adjudications) >= 7: + if adjudications[7].get('amount', None): + amt = adjudications[7].get('amount') + if amt: + claim['cost'] = "{} {}".format(amt.get('value', '0'), amt.get('currency', '')) + + claims.append(claim) + else: + raise Exception("Bad claims data..., expecting EOBs but got: {}".format(eob)) + return claims diff --git a/hhs_oauth_server/settings/base.py b/hhs_oauth_server/settings/base.py index 9c17aee88..fa1de7910 100644 --- a/hhs_oauth_server/settings/base.py +++ b/hhs_oauth_server/settings/base.py @@ -707,6 +707,8 @@ USER_ID_TYPE_DEFAULT = "H" DEFAULT_SAMPLE_FHIR_ID = env("DJANGO_DEFAULT_SAMPLE_FHIR_ID", "-20140000008325") TESTCLIENT_REDIRECT_URI = "/testclient/callback" +MYAPP_REDIRECT_URI = "/myapp/callback" +APP3RD_REDIRECT_URI = "/3rdapp/callback" OFFLINE = False EXTERNAL_LOGIN_TEMPLATE_NAME = "/v1/accounts/upstream-login" diff --git a/hhs_oauth_server/urls.py b/hhs_oauth_server/urls.py index 5c05de7ab..0ba32a793 100644 --- a/hhs_oauth_server/urls.py +++ b/hhs_oauth_server/urls.py @@ -53,6 +53,8 @@ if IsAppInstalled("apps.testclient"): urlpatterns += [ path("testclient/", include("apps.testclient.urls")), + path("myapp/", include("apps.testclient.urls4myapp")), + path("3rdapp/", include("apps.testclient.urlsfor3rdapp")), ] if IsAppInstalled("apps.mymedicare_cb"): diff --git a/manage.py b/manage.py index f5cd0a355..c0bfb255d 100755 --- a/manage.py +++ b/manage.py @@ -15,5 +15,4 @@ "hhs_oauth_server.settings.base") from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) diff --git a/requirements/requirements.dev.txt b/requirements/requirements.dev.txt index a4ab3e329..dab6745a3 100644 --- a/requirements/requirements.dev.txt +++ b/requirements/requirements.dev.txt @@ -12,7 +12,7 @@ attrs==23.1.0 \ --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # jsonschema # outcome # trio @@ -20,7 +20,7 @@ beautifulsoup4==4.12.2 \ --hash=sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da \ --hash=sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # django-bootstrap-v5 blinker==1.8.2 \ --hash=sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01 \ @@ -30,7 +30,7 @@ boto3==1.26.133 \ --hash=sha256:62285ecee7629a4388d55ae369536f759622d68d5b9a0ced7c58a0c1a409c0f7 \ --hash=sha256:8ff0af0b25266a01616396abc19eb34dc3d44bd867fa4158985924128b9034fb # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # django-ses botocore==1.29.165 \ --hash=sha256:6f35d59e230095aed7cd747604fe248fa384bebb7d09549077892f936a8ca3df \ @@ -42,7 +42,7 @@ certifi==2024.7.4 \ --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # requests # selenium cffi==1.15.1 \ @@ -195,7 +195,7 @@ click==8.1.7 \ configparser==5.3.0 \ --hash=sha256:8be267824b541c09b08db124917f48ab525a6c3e837011f3130781a224c57090 \ --hash=sha256:b065779fd93c6bf4cee42202fa4351b4bb842e96a3fb469440e484517a49b9fa - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in cryptography==44.0.0 \ --hash=sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7 \ --hash=sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731 \ @@ -227,7 +227,7 @@ cryptography==44.0.0 \ --hash=sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756 \ --hash=sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4 # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # jwcrypto debugpy==1.6.7 \ --hash=sha256:0679b7e1e3523bd7d7869447ec67b59728675aadfc038550a63a362b63029d2c \ @@ -252,12 +252,12 @@ debugpy==1.6.7 \ dj-database-url==2.0.0 \ --hash=sha256:9c9e5f7224f62635a787e9cc3c6762c9be2b19541a21e3c08fa573bd01609b4b \ --hash=sha256:a35a9f0f43775ca6f90d819dc456233ef7bcc76b47377d5d908b75c7eb320624 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django==4.2.17 \ --hash=sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0 \ --hash=sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # dj-database-url # django-axes # django-bootstrap-v5 @@ -273,15 +273,15 @@ django==4.2.17 \ django-axes==5.41.1 \ --hash=sha256:38b89dc71104ace0498d08d7100a1af67688d151206e3c17d98f75bff1351c38 \ --hash=sha256:aa43fe3a5763bea28d320b1d98e481c12e2dbc12f4dd851aaf7308e80629df9f - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-bootstrap-v5==1.0.11 \ --hash=sha256:2d431308859ce3cab7729bb09c76039059cd5fbdd34484da82c4c7f8d49da3a2 \ --hash=sha256:a207aa804938164c8450bbbef4faaba6d2093b3236000557a50f3bd44b53d268 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-cors-headers==4.0.0 \ --hash=sha256:a971cd4c75b29974068cc36b5c595698822f1e0edd5f1b32ea42ea37326ad4aa \ --hash=sha256:e3cbd247a1a835da4cf71a70d4214378813ea7e08337778b82cb2c1bc19d28d6 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-debug-toolbar==4.0.0 \ --hash=sha256:89619f6e0ea1057dca47bfc429ed99b237ef70074dabc065a7faa5f00e1459cf \ --hash=sha256:bad339d68520652ddc1580c76f136fcbc3e020fd5ed96510a89a02ec81bb3fb1 @@ -289,47 +289,49 @@ django-debug-toolbar==4.0.0 \ django-filter==23.2 \ --hash=sha256:2fe15f78108475eda525692813205fa6f9e8c1caf1ae65daa5862d403c6dbf00 \ --hash=sha256:d12d8e0fc6d3eb26641e553e5d53b191eb8cec611427d4bdce0becb1f7c172b5 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-getenv==1.3.2 \ --hash=sha256:cede44ed68570aefe91a32925b28f1d111c93023bb387c5adb36bbd42c0e5739 \ --hash=sha256:d292571cefb84904a25d163689fc8f451c3cd090be72c6b3dc60a9c44730baee - # via -r requirements/requirements.in -django-ipware==5.0.0 \ - --hash=sha256:4fa5607ee85e12ee5e158bc7569ff1e134fb1579681aa1ff3f0ed04be21be153 \ - --hash=sha256:80b52a3f571a371519cc552798f1015b934dd5dd7738bfad87e101e861bd21b8 - # via django-axes + # via -r /code/requirements/requirements.in +django-ipware==7.0.1 \ + --hash=sha256:d9ec43d2bf7cdf216fed8d494a084deb5761a54860a53b2e74346a4f384cff47 \ + --hash=sha256:db16bbee920f661ae7f678e4270460c85850f03c6761a4eaeb489bdc91f64709 + # via + # -r /code/requirements/requirements.in + # django-axes django-localflavor==4.0 \ --hash=sha256:11859e522dba74aa6dde5a659242b1fbc5efb4dea08e9b77315402bdeca5194e \ --hash=sha256:7a5b1df03ca8e10df9d1b3c2e4314e43383067868183cdf41ab4e7a973694a8b - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-oauth-toolkit==2.2.0 \ --hash=sha256:46890decb24a34e2a5382debeaf7752e50d90b7a11716cf2a9fd067097ec0963 \ --hash=sha256:abd85c74af525a62365ec2049113e73a2ff8b46ef906e7104a7ba968ef02a11d - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-ses==3.5.0 \ --hash=sha256:3522fe531155eb06bb015b3b36324c059194450633b33f9bd5bc9d1328822fe2 \ --hash=sha256:dc1644f50608fbf3a64f085a371c61d56d68eba3c5efa69651f13dc3ba05049d - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-settings-export==1.2.1 \ --hash=sha256:fceeae49fc597f654c1217415d8e049fc81c930b7154f5d8f28c432db738ff79 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-storages==1.13.2 \ --hash=sha256:31dc5a992520be571908c4c40d55d292660ece3a55b8141462b4e719aa38eab3 \ --hash=sha256:cbadd15c909ceb7247d4ffc503f12a9bec36999df8d0bef7c31e57177d512688 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in django-waffle==3.0.0 \ --hash=sha256:3acef6692cb745ed8109f0a3076c5a27da38a716deb70d64c5fb404d65ccd910 \ --hash=sha256:dd8bcc62269b35000a05a7d87e8a000136b6b1568952e2e707ef450717b1cd9f - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in djangorestframework==3.15.2 \ --hash=sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20 \ --hash=sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # djangorestframework-csv djangorestframework-csv==2.1.1 \ --hash=sha256:aa0ee4c894fe319c68e042b05c61dace43a9fb6e6872e1abe1724ca7ea4d15f7 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in flake8==6.0.0 \ --hash=sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7 \ --hash=sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181 @@ -361,11 +363,11 @@ idna==3.7 \ importlib-metadata==6.6.0 \ --hash=sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed \ --hash=sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in importlib-resources==5.12.0 \ --hash=sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6 \ --hash=sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 @@ -389,12 +391,12 @@ jmespath==1.0.1 \ jsonschema==4.17.3 \ --hash=sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d \ --hash=sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in jwcrypto==1.5.6 \ --hash=sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789 \ --hash=sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039 # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # django-oauth-toolkit markupsafe==2.1.5 \ --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ @@ -481,12 +483,12 @@ newrelic==8.8.0 \ --hash=sha256:d21af16cee1e0caf4c73c4c1b2d7ba9f33fe6a870d93135dc8b23ac592f49b38 \ --hash=sha256:da8f2dc31e182768fe314d8ceb6f42acd09956708846f8ae71f07f044a3aa05e \ --hash=sha256:ef9c178329f8c04f0574908c1f04ff1f18b9eba55b869744583fee3eac48e571 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in oauthlib==3.2.2 \ --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \ --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918 # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # django-oauth-toolkit # requests-oauthlib outcome==1.3.0.post0 \ @@ -569,11 +571,11 @@ pillow==10.3.0 \ --hash=sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f \ --hash=sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27 \ --hash=sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in pkgutil-resolve-name==1.3.10 \ --hash=sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174 \ --hash=sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in pluggy==1.5.0 \ --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 @@ -641,11 +643,11 @@ psycopg2-binary==2.9.6 \ --hash=sha256:f6a88f384335bb27812293fdb11ac6aee2ca3f51d3c7820fe03de0a304ab6249 \ --hash=sha256:f81e65376e52f03422e1fb475c9514185669943798ed019ac50410fb4c4df232 \ --hash=sha256:ffe9dc0a884a8848075e576c1de0290d85a533a9f6e9c4e564f19adf8f6e54a7 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in py==1.11.0 \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in pycodestyle==2.10.0 \ --hash=sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053 \ --hash=sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610 @@ -661,7 +663,7 @@ pyflakes==3.0.1 \ pyjwt==2.7.0 \ --hash=sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1 \ --hash=sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in pyrsistent==0.19.3 \ --hash=sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8 \ --hash=sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440 \ @@ -691,7 +693,7 @@ pyrsistent==0.19.3 \ --hash=sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9 \ --hash=sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # jsonschema pysocks==1.7.1 \ --hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \ @@ -709,7 +711,11 @@ python-dateutil==2.8.2 \ python-dotenv==1.0.0 \ --hash=sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba \ --hash=sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in +python-ipware==3.0.0 \ + --hash=sha256:9117b1c4dddcb5d5ca49e6a9617de2fc66aec2ef35394563ac4eecabdf58c062 \ + --hash=sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60 + # via django-ipware python-stdnum==1.18 \ --hash=sha256:bcc763d9c49ae23da5d2b7a686d5fd1deec9d9051341160a10d1ac723a26bec0 \ --hash=sha256:d7f2a3c7ef4635c957b9cbdd9b1993d1f6ee3a2959f03e172c45440d99f296eb @@ -718,7 +724,7 @@ pytz==2023.3 \ --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \ --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # django-ses pyyaml==6.0.1 \ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ @@ -772,19 +778,19 @@ pyyaml==6.0.1 \ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in requests==2.32.2 \ --hash=sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289 \ --hash=sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # django-oauth-toolkit # httmock # requests-oauthlib requests-oauthlib==1.3.1 \ --hash=sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5 \ --hash=sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in s3transfer==0.6.1 \ --hash=sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346 \ --hash=sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9 @@ -815,13 +821,13 @@ soupsieve==2.4.1 \ --hash=sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8 \ --hash=sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # beautifulsoup4 sqlparse==0.5.0 \ --hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \ --hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663 # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # django # django-debug-toolbar tomli==2.2.1 \ @@ -872,7 +878,7 @@ typing-extensions==4.5.0 \ --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \ --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4 # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # dj-database-url # jwcrypto unicodecsv==0.14.1 \ @@ -882,14 +888,14 @@ urllib3[socks]==1.26.19 \ --hash=sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3 \ --hash=sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429 # via - # -r requirements/requirements.in + # -r /code/requirements/requirements.in # botocore # requests # selenium voluptuous==0.13.1 \ --hash=sha256:4b838b185f5951f2d6e8752b68fcf18bd7a9c26ded8f143f92d6d28f3921a3e6 \ --hash=sha256:e8d31c20601d6773cb14d4c0f42aee29c6821bbd1018039aac7ac5605b489723 - # via -r requirements/requirements.in + # via -r /code/requirements/requirements.in werkzeug==3.0.6 \ --hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \ --hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d diff --git a/requirements/requirements.in b/requirements/requirements.in index dc931c9b4..4a0372f68 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -41,7 +41,7 @@ pyrsistent typing_extensions importlib-metadata certifi - +django-ipware >= 7.0.1 # notifications django-ses diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 470bff62c..3437b7ee9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -258,10 +258,12 @@ django-getenv==1.3.2 \ --hash=sha256:cede44ed68570aefe91a32925b28f1d111c93023bb387c5adb36bbd42c0e5739 \ --hash=sha256:d292571cefb84904a25d163689fc8f451c3cd090be72c6b3dc60a9c44730baee # via -r requirements/requirements.in -django-ipware==5.0.0 \ - --hash=sha256:4fa5607ee85e12ee5e158bc7569ff1e134fb1579681aa1ff3f0ed04be21be153 \ - --hash=sha256:80b52a3f571a371519cc552798f1015b934dd5dd7738bfad87e101e861bd21b8 - # via django-axes +django-ipware==7.0.1 \ + --hash=sha256:d9ec43d2bf7cdf216fed8d494a084deb5761a54860a53b2e74346a4f384cff47 \ + --hash=sha256:db16bbee920f661ae7f678e4270460c85850f03c6761a4eaeb489bdc91f64709 + # via + # -r requirements/requirements.in + # django-axes django-localflavor==4.0 \ --hash=sha256:11859e522dba74aa6dde5a659242b1fbc5efb4dea08e9b77315402bdeca5194e \ --hash=sha256:7a5b1df03ca8e10df9d1b3c2e4314e43383067868183cdf41ab4e7a973694a8b @@ -536,6 +538,10 @@ python-dotenv==1.0.0 \ --hash=sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba \ --hash=sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a # via -r requirements/requirements.in +python-ipware==3.0.0 \ + --hash=sha256:9117b1c4dddcb5d5ca49e6a9617de2fc66aec2ef35394563ac4eecabdf58c062 \ + --hash=sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60 + # via django-ipware python-stdnum==1.18 \ --hash=sha256:bcc763d9c49ae23da5d2b7a686d5fd1deec9d9051341160a10d1ac723a26bec0 \ --hash=sha256:d7f2a3c7ef4635c957b9cbdd9b1993d1f6ee3a2959f03e172c45440d99f296eb diff --git a/templates/include/top-nav-responsive.html b/templates/include/top-nav-responsive.html index f3c2dab2e..88a54a5c8 100644 --- a/templates/include/top-nav-responsive.html +++ b/templates/include/top-nav-responsive.html @@ -33,7 +33,9 @@ API Reference {% endswitch %} Production Access Guide - Test Client + Test Client + MyApp + ClinicaBleu {% if user.is_authenticated %} Account diff --git a/vendor/django_ipware-7.0.1-py2.py3-none-any.whl b/vendor/django_ipware-7.0.1-py2.py3-none-any.whl new file mode 100644 index 000000000..9268076f6 Binary files /dev/null and b/vendor/django_ipware-7.0.1-py2.py3-none-any.whl differ diff --git a/vendor/python_ipware-3.0.0-py3-none-any.whl b/vendor/python_ipware-3.0.0-py3-none-any.whl new file mode 100644 index 000000000..9cdbd93e6 Binary files /dev/null and b/vendor/python_ipware-3.0.0-py3-none-any.whl differ