Skip to content

Commit

Permalink
Add support for managed install
Browse files Browse the repository at this point in the history
  • Loading branch information
stlk committed Oct 2, 2024
1 parent cbfa7ef commit c0f9db9
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
django: ["Django<4.0", "Django<4.1", "Django<4.2"]
python-version: ["3.9", "3.10", "3.11", "3.12"]
django: ["Django<5.0", "Django<5.1", "Django<5.2"]

steps:
- uses: actions/checkout@v3
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ django >=3.2
ShopifyAPI >=8.0.0
ua-parser
python-jose
requests
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
},

install_requires=[
'django >=3.2',
'django >=4.2',
'ShopifyAPI >=8.0.0',
'setuptools >=5.7',
'python-jose >=3.2.0'
'python-jose >=3.2.0',
'requests >=2.0.0',
],

tests_require=[],
Expand Down
2 changes: 2 additions & 0 deletions shopify_auth/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def ready(self):
The ready() method is called after Django setup.
"""
initialise_shopify_session()
import shopify_auth.checks



def initialise_shopify_session():
Expand Down
23 changes: 23 additions & 0 deletions shopify_auth/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.core.checks import Error, register
from django.conf import settings

@register()
def check_shopify_auth_bounce_page_url(app_configs, **kwargs):
errors = []
if not hasattr(settings, 'SHOPIFY_AUTH_BOUNCE_PAGE_URL'):
errors.append(
Error(
'SHOPIFY_AUTH_BOUNCE_PAGE_URL is not set in settings.',
hint='Set SHOPIFY_AUTH_BOUNCE_PAGE_URL in your settings file or environment variables.',
id='shopify_auth.E001',
)
)
elif not settings.SHOPIFY_AUTH_BOUNCE_PAGE_URL:
errors.append(
Error(
'SHOPIFY_AUTH_BOUNCE_PAGE_URL is empty.',
hint='Provide a valid URL for SHOPIFY_AUTH_BOUNCE_PAGE_URL in your settings file or environment variables.',
id='shopify_auth.E002',
)
)
return errors
3 changes: 3 additions & 0 deletions shopify_auth/session_tokens/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ This app takes care of the installation and provides middleware that adds a user

I created a [demo app](https://github.com/digismoothie/django-session-token-auth-demo) that uses Hotwire, successor of Turbolinks.

> [!NOTE]
> Managed installation is much more involved because there's no speficic install entrypoint. The main entrypoint is used instead. For now you can use managed_install.py and session_token_bounce view.
### Instalation

### 1. Install package
Expand Down
51 changes: 51 additions & 0 deletions shopify_auth/session_tokens/managed_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import logging
from django.core.exceptions import ImproperlyConfigured

import requests
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect

logger = logging.getLogger(__name__)

from typing import TypedDict


class ResponseData(TypedDict):
access_token: str
scope: list[str]


# From https://shopify.dev/docs/apps/build/authentication-authorization/get-access-tokens/exchange-tokens#step-2-get-an-access-token
def retrieve_api_token(shop: str, session_token: str) -> ResponseData:
url = f"https://{shop}/admin/oauth/access_token"
payload = {
"client_id": settings.SHOPIFY_APP_API_KEY,
"client_secret": settings.SHOPIFY_APP_API_SECRET,
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": session_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
"requested_token_type": "urn:shopify:params:oauth:token-type:offline-access-token",
}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
response_data_raw = response.json()
response_data: ResponseData = {
"access_token": response_data_raw["access_token"],
"scope": [scope.strip() for scope in response_data_raw["scope"].split(",")],
}
return response_data


def session_token_bounce_page_url(request: HttpRequest) -> str:
search_params = request.GET.copy()
search_params.pop("id_token", None)
search_params["shopify-reload"] = f"{request.path}?{search_params.urlencode()}"

bounce_page_url = settings.SHOPIFY_AUTH_BOUNCE_PAGE_URL
return f"{bounce_page_url}?{search_params.urlencode()}"


def redirect_to_session_token_bounce_page(request: HttpRequest) -> HttpResponse:
return redirect(session_token_bounce_page_url(request))
50 changes: 50 additions & 0 deletions shopify_auth/session_tokens/tests/test_managed_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django.test import TestCase, RequestFactory
from django.core.exceptions import ImproperlyConfigured
from unittest.mock import patch

from ..managed_install import (
retrieve_api_token,
session_token_bounce_page_url,
redirect_to_session_token_bounce_page,
)

class ManagedInstallTestCase(TestCase):
def setUp(self):
self.factory = RequestFactory()

@patch('requests.post')
def test_retrieve_api_token(self, mock_post):
# Mock the response from Shopify
mock_response = mock_post.return_value
mock_response.json.return_value = {
'access_token': 'test_token',
'scope': 'read_products,write_orders'
}

result = retrieve_api_token('test-shop.myshopify.com', 'test_session_token')

self.assertEqual(result['access_token'], 'test_token')
self.assertEqual(result['scope'], ['read_products', 'write_orders'])

# Check if the request was made with correct parameters
mock_post.assert_called_once()
args, kwargs = mock_post.call_args
self.assertEqual(args[0], 'https://test-shop.myshopify.com/admin/oauth/access_token')

def test_session_token_bounce_page_url(self):
request = self.factory.get('/test-path/?param1=value1&id_token=test_token')

with self.settings(SHOPIFY_AUTH_BOUNCE_PAGE_URL='/bounce/'):
url = session_token_bounce_page_url(request)

expected_url = '/bounce/?param1=value1&shopify-reload=%2Ftest-path%2F%3Fparam1%3Dvalue1'
self.assertEqual(url, expected_url)

def test_redirect_to_session_token_bounce_page(self):
request = self.factory.get('/test-path/')

with self.settings(SHOPIFY_AUTH_BOUNCE_PAGE_URL='/bounce/'):
response = redirect_to_session_token_bounce_page(request)

self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.startswith('/bounce/'))
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.test import TestCase, RequestFactory
from django.conf import settings
from django.http import HttpResponse

from ..views import session_token_bounce

class SessionTokenBounceTestCase(TestCase):
def test_session_token_bounce(self):
request = RequestFactory().get('/bounce/')
response = session_token_bounce(request)

self.assertIsInstance(response, HttpResponse)
self.assertEqual(response['Content-Type'], 'text/html')
self.assertIn(settings.SHOPIFY_APP_API_KEY, response.content.decode())
self.assertIn('https://cdn.shopify.com/shopifycloud/app-bridge.js', response.content.decode())
1 change: 1 addition & 0 deletions shopify_auth/session_tokens/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
urlpatterns = [
path("finalize", views.FinalizeAuthView.as_view(), name="finalize"),
path("authenticate", views.get_scope_permission, name="authenticate"),
path("session-token-bounce", views.session_token_bounce, name="session-token-bounce"),
]
15 changes: 15 additions & 0 deletions shopify_auth/session_tokens/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,18 @@ def get(self, request):
return HttpResponseRedirect(
f"https://{myshopify_domain}/admin/apps/{settings.SHOPIFY_APP_API_KEY}"
)


def session_token_bounce(request) -> HttpResponse:
"""
The entire flow is documented on https://shopify.dev/docs/apps/build/authentication-authorization/set-embedded-app-authorization?extension=javascript#session-token-in-the-url-parameter
"""
response = HttpResponse(content_type="text/html")
html = f"""
<head>
<meta name="shopify-api-key" content="{settings.SHOPIFY_APP_API_KEY}" />
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
</head>
"""
response.write(html)
return response
1 change: 1 addition & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'SHOPIFY_APP_API_SCOPE': ['read_products'],
'SHOPIFY_APP_DEV_MODE': False,
'SHOPIFY_APP_THIRD_PARTY_COOKIE_CHECK': True,
'SHOPIFY_AUTH_BOUNCE_PAGE_URL': '/',
'SECRET_KEY': 'uq8e140t1rm3^kk&blqxi*y9h_j5yd9ghjv+fd1p%08g4%t6%i',
'MIDDLEWARE': [
'django.middleware.common.CommonMiddleware',
Expand Down

0 comments on commit c0f9db9

Please sign in to comment.