BTHLABS-58: Share Extension in Apple Apps

This commit is contained in:
2025-10-04 08:02:13 +02:00
parent 0c12f52569
commit 99e9226338
122 changed files with 5488 additions and 411 deletions

View File

@@ -23,3 +23,11 @@ class UIAccessTokenOriginApp(enum.Enum):
SAFARI_WEB_EXTENSION = _('Safari Web Extension')
CHROME_EXTENSION = _('Chrome Extension')
FIREFOX_EXTENSION = _('Firefox Extension')
HOTPOCKET_DESKTOP = _('HotPocket Desktop')
HOTPOCKET_MOBILE = _('HotPocket Mobile')
class AuthSource(enum.Enum):
BROWSER_EXTENSION = 'HotPocketExtension'
DESKTOP = 'HotPocketDesktop'
MOBILE = 'HotPocketMobile'

View File

@@ -7,23 +7,37 @@ from bthlabs_jsonrpc_core import register_method
from django import db
from django.http import HttpRequest
from hotpocket_soa.services import AccessTokensService
from hotpocket_soa.services import (
AccessTokensService,
AccountsService,
AuthKeysService,
)
LOGGER = logging.getLogger(__name__)
@register_method('accounts.access_tokens.create')
@register_method('accounts.access_tokens.create', namespace='accounts')
def create(request: HttpRequest,
auth_key: str,
meta: dict,
) -> str:
with db.transaction.atomic():
try:
assert 'extension_auth_key' in request.session, 'Auth key missing'
assert request.session['extension_auth_key'] == auth_key, (
'Auth key mismatch'
auth_key_object = AuthKeysService().get_by_key(
account_uuid=None,
key=auth_key,
)
except AssertionError as exception:
except AuthKeysService.AuthKeyNotFound as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,
exc_info=exception,
)
raise
try:
account = AccountsService().get(pk=auth_key_object.account_uuid)
except AccountsService.AccountNotFound as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,
@@ -32,12 +46,9 @@ def create(request: HttpRequest,
raise
access_token = AccessTokensService().create(
account_uuid=request.user.pk,
account_uuid=account.pk,
origin=request.META['HTTP_ORIGIN'],
meta=meta,
)
request.session.pop('extension_auth_key')
request.session.save()
return access_token.key

View File

@@ -13,16 +13,18 @@ from hotpocket_soa.services import AccessTokensService
LOGGER = logging.getLogger(__name__)
@register_method('accounts.auth.check')
@register_method('accounts.auth.check', namespace='accounts')
def check(request: HttpRequest) -> bool:
return request.user.is_anonymous is False
@register_method('accounts.auth.check_access_token')
@register_method('accounts.auth.check_access_token', namespace='accounts')
def check_access_token(request: HttpRequest,
access_token: str,
meta: dict | None = None,
) -> bool:
assert request.user.is_anonymous is False, 'Not authenticated'
result = True
try:

View File

@@ -11,8 +11,27 @@
<div class="alert alert-success mt-3" role="alert">
<h4 class="alert-heading">{% translate 'Done!' %}</h4>
<p class="lead mb-0">
{% translate "You've successfully logged in to the extension." %}
{% if app_redirect_url %}
{% translate "You've successfully logged in to the application." %}
{% else %}
{% translate "You've successfully logged in to the extension." %}
{% endif %}
</p>
</div>
</div>
{% endblock %}
{% block page_scripts %}
{% if app_redirect_url %}
<script type="text/javascript">
(() => {
window.setTimeout(
() => {
window.location.replace('{{ app_redirect_url|safe }}');
},
1000,
);
})();
</script>
{% endif %}
{% endblock %}

View File

@@ -137,6 +137,8 @@ def render_access_token_app(access_token: AccessTokenOut) -> str:
AccessTokenOriginApp.SAFARI_WEB_EXTENSION,
AccessTokenOriginApp.CHROME_EXTENSION,
AccessTokenOriginApp.FIREFOX_EXTENSION,
AccessTokenOriginApp.HOTPOCKET_DESKTOP,
AccessTokenOriginApp.HOTPOCKET_MOBILE,
)
if origin_app in extension_origin_apps:
app = UIAccessTokenOriginApp[origin_app.value].value
@@ -152,7 +154,7 @@ def render_access_token_app(access_token: AccessTokenOut) -> str:
@register.filter(name='render_access_token_platform')
def render_access_token_platform(access_token: AccessTokenOut) -> str:
match access_token.meta.get('platform', None):
case 'MacIntel':
case 'MacIntel' | 'macOS':
return 'macOS'
case 'iPhone':

View File

@@ -59,6 +59,13 @@ urlpatterns = [
accounts.apps.DeleteView.as_view(),
name='ui.accounts.apps.delete',
),
path(
'accounts/rpc/',
JSONRPCView.as_view(
namespace='accounts',
),
name='ui.accounts.rpc',
),
path('accounts/', accounts.index.index, name='ui.accounts.index'),
path(
'imports/pocket/',

View File

@@ -2,27 +2,56 @@
from __future__ import annotations
import logging
import urllib.parse
import uuid
from django import db
from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from hotpocket_backend.apps.ui.constants import AuthSource
from hotpocket_soa.services import AuthKeysService
LOGGER = logging.getLogger(__name__)
SOURCE_TO_REDIRECT_SCHEME = {
AuthSource.DESKTOP.value: 'hotpocket-desktop',
AuthSource.MOBILE.value: 'hotpocket-mobile',
}
def authenticate(request: HttpRequest) -> HttpResponse:
if request.user.is_anonymous is False:
auth_key = str(uuid.uuid4())
source = request.GET.get(
'source',
request.session.get('extension_source', AuthSource.BROWSER_EXTENSION.value),
)
session_token = request.GET.get(
'session_token', request.session.get('extension_session_token', None),
)
request.session['extension_auth_key'] = auth_key
request.session.save()
if source == AuthSource.BROWSER_EXTENSION.value:
session_token = str(uuid.uuid4())
elif source in (AuthSource.DESKTOP.value, AuthSource.MOBILE.value):
assert session_token not in ('', None), 'Session token missing'
else:
raise ValueError(f'Unknown source: `{source}`')
request.session['extension_source'] = source
request.session['extension_session_token'] = session_token
request.session.save()
if request.user.is_anonymous is False:
with db.transaction.atomic():
auth_key = AuthKeysService().create(
account_uuid=request.user.pk,
)
return redirect(reverse(
'ui.integrations.extension.post_authenticate',
query=[
('auth_key', auth_key),
('auth_key', auth_key.key),
],
))
@@ -36,12 +65,35 @@ def post_authenticate(request: HttpRequest) -> HttpResponse:
assert request.user.is_anonymous is False, 'Not authenticated'
auth_key = request.GET.get('auth_key', None)
assert request.session.get('extension_auth_key', None) == auth_key, (
'Auth key mismatch'
)
assert auth_key is not None, 'Auth key missing'
source = request.session.get('extension_source', None)
assert source is not None, 'Source is missing'
session_token = request.session.get('extension_session_token', None)
assert session_token is not None, 'Session token is missing'
app_redirect_url = None
if source in (AuthSource.DESKTOP.value, AuthSource.MOBILE.value):
app_redirect_url = urllib.parse.urlunsplit((
SOURCE_TO_REDIRECT_SCHEME[source],
'post-authenticate',
'/',
urllib.parse.urlencode([
('session_token', session_token),
('auth_key', auth_key),
]),
'',
))
request.session.pop('extension_source')
request.session.pop('extension_session_token')
request.session.save()
return render(
request, 'ui/integrations/extension/post_authenticate.html',
request,
'ui/integrations/extension/post_authenticate.html',
{
'app_redirect_url': app_redirect_url,
},
)
except AssertionError as exception:
LOGGER.error(