BTHLABS-50: Safari Web Extension: Reloaded
Turns out, getting this thing out into the wild isn't as simple as I thought :D Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl> Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
This commit is contained in:
parent
67138c7035
commit
dcebccf947
|
@ -479,7 +479,7 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS.Extension;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
|
||||||
PRODUCT_NAME = "HotPocket Extension";
|
PRODUCT_NAME = "HotPocket Extension";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -509,7 +509,7 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS.Extension;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
|
||||||
PRODUCT_NAME = "HotPocket Extension";
|
PRODUCT_NAME = "HotPocket Extension";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -525,7 +525,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "iOS (App)/Info.plist";
|
INFOPLIST_FILE = "iOS (App)/Info.plist";
|
||||||
|
@ -534,7 +534,7 @@
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -547,9 +547,13 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
WebKit,
|
WebKit,
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
|
||||||
PRODUCT_NAME = HotPocket;
|
PRODUCT_NAME = HotPocket;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
|
@ -561,7 +565,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "iOS (App)/Info.plist";
|
INFOPLIST_FILE = "iOS (App)/Info.plist";
|
||||||
|
@ -570,7 +574,7 @@
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -583,9 +587,13 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
WebKit,
|
WebKit,
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
|
||||||
PRODUCT_NAME = HotPocket;
|
PRODUCT_NAME = HotPocket;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
|
@ -617,7 +625,7 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS.Extension;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
|
||||||
PRODUCT_NAME = "HotPocket Extension";
|
PRODUCT_NAME = "HotPocket Extension";
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -629,7 +637,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
|
@ -650,7 +658,7 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
SafariServices,
|
SafariServices,
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS.Extension;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
|
||||||
PRODUCT_NAME = "HotPocket Extension";
|
PRODUCT_NAME = "HotPocket Extension";
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -686,7 +694,7 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
WebKit,
|
WebKit,
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
|
||||||
PRODUCT_NAME = HotPocket;
|
PRODUCT_NAME = HotPocket;
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
@ -700,7 +708,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 648728X64K;
|
DEVELOPMENT_TEAM = 648728X64K;
|
||||||
|
@ -722,7 +730,7 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
WebKit,
|
WebKit,
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS;
|
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
|
||||||
PRODUCT_NAME = HotPocket;
|
PRODUCT_NAME = HotPocket;
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
|
|
@ -11,7 +11,10 @@ import uuid6
|
||||||
|
|
||||||
from hotpocket_backend.apps.accounts.models import AccessToken
|
from hotpocket_backend.apps.accounts.models import AccessToken
|
||||||
from hotpocket_backend.apps.core.conf import settings
|
from hotpocket_backend.apps.core.conf import settings
|
||||||
from hotpocket_soa.dto.accounts import AccessTokensQuery
|
from hotpocket_soa.dto.accounts import (
|
||||||
|
AccessTokenMetaUpdateIn,
|
||||||
|
AccessTokensQuery,
|
||||||
|
)
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -54,6 +57,16 @@ class AccessTokensService:
|
||||||
f'Access Token not found: pk=`{pk}`',
|
f'Access Token not found: pk=`{pk}`',
|
||||||
) from exception
|
) from exception
|
||||||
|
|
||||||
|
def get_by_key(self, *, key: str) -> AccessToken:
|
||||||
|
try:
|
||||||
|
query_set = AccessToken.active_objects
|
||||||
|
|
||||||
|
return query_set.get(key=key)
|
||||||
|
except AccessToken.DoesNotExist as exception:
|
||||||
|
raise self.AccessTokenNotFound(
|
||||||
|
f'Access Token not found: key=`{key}`',
|
||||||
|
) from exception
|
||||||
|
|
||||||
def search(self,
|
def search(self,
|
||||||
*,
|
*,
|
||||||
query: AccessTokensQuery,
|
query: AccessTokensQuery,
|
||||||
|
@ -79,3 +92,27 @@ class AccessTokensService:
|
||||||
access_token.soft_delete()
|
access_token.soft_delete()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def update_meta(self,
|
||||||
|
*,
|
||||||
|
pk: uuid.UUID,
|
||||||
|
update: AccessTokenMetaUpdateIn,
|
||||||
|
) -> AccessToken:
|
||||||
|
access_token = AccessToken.active_objects.get(pk=pk)
|
||||||
|
|
||||||
|
next_meta = {
|
||||||
|
**(access_token.meta or {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.version is not None:
|
||||||
|
next_meta['version'] = update.version
|
||||||
|
|
||||||
|
if update.platform is not None:
|
||||||
|
next_meta['platform'] = update.platform
|
||||||
|
|
||||||
|
access_token.meta = next_meta
|
||||||
|
access_token.save()
|
||||||
|
|
||||||
|
access_token.refresh_from_db()
|
||||||
|
|
||||||
|
return access_token
|
||||||
|
|
|
@ -1,10 +1,62 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from bthlabs_jsonrpc_core import register_method
|
from bthlabs_jsonrpc_core import register_method
|
||||||
|
from django import db
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
from hotpocket_soa.dto.accounts import AccessTokenMetaUpdateIn
|
||||||
|
from hotpocket_soa.services import AccessTokensService
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@register_method('accounts.auth.check')
|
@register_method('accounts.auth.check')
|
||||||
def check(request: HttpRequest) -> bool:
|
def check(request: HttpRequest) -> bool:
|
||||||
return request.user.is_anonymous is False
|
return request.user.is_anonymous is False
|
||||||
|
|
||||||
|
|
||||||
|
@register_method('accounts.auth.check_access_token')
|
||||||
|
def check_access_token(request: HttpRequest,
|
||||||
|
access_token: str,
|
||||||
|
meta: dict | None = None,
|
||||||
|
) -> bool:
|
||||||
|
result = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_tokens_service = AccessTokensService()
|
||||||
|
|
||||||
|
with db.transaction.atomic():
|
||||||
|
access_token_object = access_tokens_service.get_by_key(
|
||||||
|
account_uuid=request.user.pk,
|
||||||
|
key=access_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
meta_update = AccessTokenMetaUpdateIn.model_validate(
|
||||||
|
(meta or {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
_ = access_tokens_service.update_meta(
|
||||||
|
access_token=access_token_object,
|
||||||
|
update=meta_update,
|
||||||
|
)
|
||||||
|
except AccessTokensService.AccessTokenNotFound as exception:
|
||||||
|
LOGGER.error(
|
||||||
|
'Access Token not found: account_uuid=`%s` key=`%s`',
|
||||||
|
request.user.pk,
|
||||||
|
access_token,
|
||||||
|
exc_info=exception,
|
||||||
|
)
|
||||||
|
result = False
|
||||||
|
except AccessTokensService.AccessTokenAccessDenied as exception:
|
||||||
|
LOGGER.error(
|
||||||
|
'Access Token access denied: account_uuid=`%s` key=`%s`',
|
||||||
|
request.user.pk,
|
||||||
|
access_token,
|
||||||
|
exc_info=exception,
|
||||||
|
)
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
|
@ -11,7 +11,7 @@ from hotpocket_backend.apps.accounts.models import AccessToken
|
||||||
def AccessTokenMetaFactory() -> dict:
|
def AccessTokenMetaFactory() -> dict:
|
||||||
return {
|
return {
|
||||||
'platform': 'MacIntel',
|
'platform': 'MacIntel',
|
||||||
'version': '1987.10.03',
|
'version': '1985.12.12',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class AccessTokenFactory(factory.django.DjangoModelFactory):
|
||||||
account_uuid = None
|
account_uuid = None
|
||||||
key = factory.LazyFunction(lambda: str(uuid.uuid4()))
|
key = factory.LazyFunction(lambda: str(uuid.uuid4()))
|
||||||
origin = factory.LazyFunction(
|
origin = factory.LazyFunction(
|
||||||
lambda: f'safari-web-extension//{uuid.uuid4()}',
|
lambda: f'safari-web-extension://{uuid.uuid4()}',
|
||||||
)
|
)
|
||||||
meta = factory.LazyFunction(AccessTokenMetaFactory)
|
meta = factory.LazyFunction(AccessTokenMetaFactory)
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
from .access_token import * # noqa: F401,F403
|
from .access_token import * # noqa: F401,F403
|
||||||
from .account import * # noqa: F401,F403
|
from .account import * # noqa: F401,F403
|
||||||
|
from .apps import * # noqa: F401,F403
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# type: ignore
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def safari_extension_origin():
|
||||||
|
return f'safari-web-extension://{uuid.uuid4()}'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def safari_extension_meta():
|
||||||
|
return {
|
||||||
|
'platform': 'MacIntel',
|
||||||
|
'version': '1987.10.03',
|
||||||
|
}
|
|
@ -24,15 +24,28 @@ class AccessTokensTestingService:
|
||||||
assert access_token.updated_at is not None
|
assert access_token.updated_at is not None
|
||||||
|
|
||||||
def assert_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
def assert_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
||||||
association = AccessToken.objects.get(pk=pk)
|
access_token = AccessToken.objects.get(pk=pk)
|
||||||
assert association.deleted_at is not None
|
assert access_token.deleted_at is not None
|
||||||
|
|
||||||
if reference is not None:
|
if reference is not None:
|
||||||
assert association.updated_at > reference.updated_at
|
assert access_token.updated_at > reference.updated_at
|
||||||
|
|
||||||
def assert_not_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
def assert_not_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
|
||||||
association = AccessToken.objects.get(pk=pk)
|
access_token = AccessToken.objects.get(pk=pk)
|
||||||
assert association.deleted_at is None
|
assert access_token.deleted_at is None
|
||||||
|
|
||||||
if reference is not None:
|
if reference is not None:
|
||||||
assert association.updated_at == reference.updated_at
|
assert access_token.updated_at == reference.updated_at
|
||||||
|
|
||||||
|
def assert_meta_updated(self, *, pk: uuid.UUID, meta_update: dict, reference: typing.Any = None):
|
||||||
|
access_token = AccessToken.objects.get(pk=pk)
|
||||||
|
|
||||||
|
if len(meta_update) > 0 and reference is not None:
|
||||||
|
expected_meta = {
|
||||||
|
**reference.meta,
|
||||||
|
**meta_update,
|
||||||
|
}
|
||||||
|
assert access_token.meta == expected_meta
|
||||||
|
|
||||||
|
if reference is not None:
|
||||||
|
assert access_token.updated_at > reference.updated_at
|
||||||
|
|
|
@ -14,29 +14,16 @@ from hotpocket_backend_testing.services.accounts import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def origin():
|
|
||||||
return f'safari-web-extension://{uuid.uuid4()}'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def auth_key():
|
def auth_key():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def meta():
|
def call(rpc_call_factory, auth_key, safari_extension_meta):
|
||||||
return {
|
|
||||||
'platform': 'MacIntel',
|
|
||||||
'version': '1987.10.03',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def call(rpc_call_factory, auth_key, meta):
|
|
||||||
return rpc_call_factory(
|
return rpc_call_factory(
|
||||||
'accounts.access_tokens.create',
|
'accounts.access_tokens.create',
|
||||||
[auth_key, meta],
|
[auth_key, safari_extension_meta],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,9 +31,9 @@ def call(rpc_call_factory, auth_key, meta):
|
||||||
def test_ok(authenticated_client: Client,
|
def test_ok(authenticated_client: Client,
|
||||||
auth_key,
|
auth_key,
|
||||||
call,
|
call,
|
||||||
origin,
|
safari_extension_origin,
|
||||||
account,
|
account,
|
||||||
meta,
|
safari_extension_meta,
|
||||||
):
|
):
|
||||||
# Given
|
# Given
|
||||||
session = authenticated_client.session
|
session = authenticated_client.session
|
||||||
|
@ -59,7 +46,7 @@ def test_ok(authenticated_client: Client,
|
||||||
data=call,
|
data=call,
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
headers={
|
headers={
|
||||||
'Origin': origin,
|
'Origin': safari_extension_origin,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -72,8 +59,8 @@ def test_ok(authenticated_client: Client,
|
||||||
AccessTokensTestingService().assert_created(
|
AccessTokensTestingService().assert_created(
|
||||||
key=call_result['result'],
|
key=call_result['result'],
|
||||||
account_uuid=account.pk,
|
account_uuid=account.pk,
|
||||||
origin=origin,
|
origin=safari_extension_origin,
|
||||||
meta=meta,
|
meta=safari_extension_meta,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert 'extension_auth_key' not in authenticated_client.session
|
assert 'extension_auth_key' not in authenticated_client.session
|
||||||
|
@ -82,7 +69,7 @@ def test_ok(authenticated_client: Client,
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_auth_key_missing(authenticated_client: Client,
|
def test_auth_key_missing(authenticated_client: Client,
|
||||||
call,
|
call,
|
||||||
origin,
|
safari_extension_origin,
|
||||||
):
|
):
|
||||||
# When
|
# When
|
||||||
result = authenticated_client.post(
|
result = authenticated_client.post(
|
||||||
|
@ -90,7 +77,7 @@ def test_auth_key_missing(authenticated_client: Client,
|
||||||
data=call,
|
data=call,
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
headers={
|
headers={
|
||||||
'Origin': origin,
|
'Origin': safari_extension_origin,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -105,7 +92,7 @@ def test_auth_key_missing(authenticated_client: Client,
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_auth_key_mismatch(authenticated_client: Client,
|
def test_auth_key_mismatch(authenticated_client: Client,
|
||||||
call,
|
call,
|
||||||
origin,
|
safari_extension_origin,
|
||||||
):
|
):
|
||||||
# Given
|
# Given
|
||||||
session = authenticated_client.session
|
session = authenticated_client.session
|
||||||
|
@ -118,7 +105,7 @@ def test_auth_key_mismatch(authenticated_client: Client,
|
||||||
data=call,
|
data=call,
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
headers={
|
headers={
|
||||||
'Origin': origin,
|
'Origin': safari_extension_origin,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# type: ignore
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import http
|
||||||
|
|
||||||
|
from django.test import Client
|
||||||
|
from django.urls import reverse
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from hotpocket_backend_testing.services.accounts import (
|
||||||
|
AccessTokensTestingService,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def call_factory(request: pytest.FixtureRequest, rpc_call_factory):
|
||||||
|
default_access_token = request.getfixturevalue('access_token_out')
|
||||||
|
default_meta_update = request.getfixturevalue('safari_extension_meta')
|
||||||
|
|
||||||
|
def factory(access_token=None, meta_update=None):
|
||||||
|
return rpc_call_factory(
|
||||||
|
'accounts.auth.check_access_token',
|
||||||
|
[
|
||||||
|
(
|
||||||
|
access_token.key
|
||||||
|
if access_token is not None
|
||||||
|
else default_access_token.key
|
||||||
|
),
|
||||||
|
(
|
||||||
|
meta_update
|
||||||
|
if meta_update is not None
|
||||||
|
else default_meta_update
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return factory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def call(call_factory):
|
||||||
|
return call_factory()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_ok(authenticated_client: Client,
|
||||||
|
call,
|
||||||
|
access_token_out,
|
||||||
|
safari_extension_meta,
|
||||||
|
):
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.rpc'),
|
||||||
|
data=call,
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
|
call_result = result.json()
|
||||||
|
assert 'error' not in call_result
|
||||||
|
assert call_result['result'] is True
|
||||||
|
|
||||||
|
AccessTokensTestingService().assert_meta_updated(
|
||||||
|
pk=access_token_out.pk,
|
||||||
|
meta_update=safari_extension_meta,
|
||||||
|
reference=access_token_out,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'meta_keys_to_pop',
|
||||||
|
[
|
||||||
|
('platform',),
|
||||||
|
('version',),
|
||||||
|
('platform', 'version'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_ok_with_partial_meta_update(meta_keys_to_pop,
|
||||||
|
safari_extension_meta,
|
||||||
|
authenticated_client: Client,
|
||||||
|
call_factory,
|
||||||
|
access_token_out,
|
||||||
|
):
|
||||||
|
# Given
|
||||||
|
meta_update = {**safari_extension_meta}
|
||||||
|
for meta_key_to_pop in meta_keys_to_pop:
|
||||||
|
meta_update.pop(meta_key_to_pop)
|
||||||
|
|
||||||
|
call = call_factory(meta_update=meta_update)
|
||||||
|
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.rpc'),
|
||||||
|
data=call,
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
|
call_result = result.json()
|
||||||
|
assert 'error' not in call_result
|
||||||
|
assert call_result['result'] is True
|
||||||
|
|
||||||
|
AccessTokensTestingService().assert_meta_updated(
|
||||||
|
pk=access_token_out.pk,
|
||||||
|
meta_update=meta_update,
|
||||||
|
reference=access_token_out,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_invalid_access_token(authenticated_client: Client,
|
||||||
|
call,
|
||||||
|
):
|
||||||
|
# Given
|
||||||
|
call['params'][0] = 'thisisntright'
|
||||||
|
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.rpc'),
|
||||||
|
data=call,
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
|
call_result = result.json()
|
||||||
|
assert 'error' not in call_result
|
||||||
|
assert call_result['result'] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_deleted_access_token(call_factory,
|
||||||
|
deleted_access_token_out,
|
||||||
|
authenticated_client: Client,
|
||||||
|
):
|
||||||
|
# Given
|
||||||
|
call = call_factory(access_token=deleted_access_token_out)
|
||||||
|
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.rpc'),
|
||||||
|
data=call,
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
|
call_result = result.json()
|
||||||
|
assert 'error' not in call_result
|
||||||
|
assert call_result['result'] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_other_account_access_token(call_factory,
|
||||||
|
other_account_access_token_out,
|
||||||
|
authenticated_client: Client,
|
||||||
|
):
|
||||||
|
# Given
|
||||||
|
call = call_factory(access_token=other_account_access_token_out)
|
||||||
|
|
||||||
|
# When
|
||||||
|
result = authenticated_client.post(
|
||||||
|
reverse('ui.rpc'),
|
||||||
|
data=call,
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.OK
|
||||||
|
|
||||||
|
call_result = result.json()
|
||||||
|
assert 'error' not in call_result
|
||||||
|
assert call_result['result'] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_inactive_account(inactive_account_client: Client, call):
|
||||||
|
# When
|
||||||
|
result = inactive_account_client.post(
|
||||||
|
reverse('ui.rpc'),
|
||||||
|
data=call,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_anonymous(client: Client, call):
|
||||||
|
# When
|
||||||
|
result = client.post(
|
||||||
|
reverse('ui.rpc'),
|
||||||
|
data=call,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert result.status_code == http.HTTPStatus.FORBIDDEN
|
12
services/dotcom/webroot/privacy.html
Normal file
12
services/dotcom/webroot/privacy.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>hotpocket.app</title>
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>SOON</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -66,6 +66,10 @@ const manifestJsonOutputPlugin = () => {
|
||||||
|
|
||||||
result.version = packageJSON.version;
|
result.version = packageJSON.version;
|
||||||
|
|
||||||
|
if (IS_PRODUCTION === false) {
|
||||||
|
result.name = 'HotPocket Development';
|
||||||
|
}
|
||||||
|
|
||||||
return JSON.stringify(result, null, 2);
|
return JSON.stringify(result, null, 2);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,9 +29,12 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
|
||||||
let result = null;
|
let result = null;
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
const effectiveURL = new URL(url);
|
||||||
|
effectiveURL.searchParams.append('method', call.method);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
url,
|
effectiveURL.toString(),
|
||||||
{
|
{
|
||||||
body: JSON.stringify(call),
|
body: JSON.stringify(call),
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
@ -78,6 +81,13 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
|
||||||
return [result, error];
|
return [result, error];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAccessTokenMeta = () => {
|
||||||
|
return {
|
||||||
|
platform: navigator.platform,
|
||||||
|
version: HotPocketExtension.version,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const doSave = async (accessToken, tab) => {
|
const doSave = async (accessToken, tab) => {
|
||||||
const call = makeJSONRPCCall('saves.create', [tab.url]);
|
const call = makeJSONRPCCall('saves.create', [tab.url]);
|
||||||
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {accessToken});
|
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {accessToken});
|
||||||
|
@ -97,10 +107,7 @@ const doCreateAndStoreAccessToken = async (authKey) => {
|
||||||
'accounts.access_tokens.create',
|
'accounts.access_tokens.create',
|
||||||
[
|
[
|
||||||
authKey,
|
authKey,
|
||||||
{
|
getAccessTokenMeta(),
|
||||||
platform: navigator.platform,
|
|
||||||
version: HotPocketExtension.version,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -163,7 +170,10 @@ const doCheckAuth = async (accessToken) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const call = makeJSONRPCCall('accounts.auth.check');
|
const call = makeJSONRPCCall(
|
||||||
|
'accounts.auth.check_access_token',
|
||||||
|
[accessToken, getAccessTokenMeta()],
|
||||||
|
);
|
||||||
|
|
||||||
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {
|
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {
|
||||||
accessToken,
|
accessToken,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"background-bundle.js"
|
"background-bundle.js"
|
||||||
],
|
],
|
||||||
"type": "module"
|
"type": "module",
|
||||||
|
"persistent": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,3 +36,8 @@ class AccessTokenOut(ModelOut):
|
||||||
class AccessTokensQuery(Query):
|
class AccessTokensQuery(Query):
|
||||||
account_uuid: uuid.UUID
|
account_uuid: uuid.UUID
|
||||||
before: uuid.UUID | None = pydantic.Field(default=None)
|
before: uuid.UUID | None = pydantic.Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenMetaUpdateIn(pydantic.BaseModel):
|
||||||
|
version: str | None = None
|
||||||
|
platform: str | None = None
|
||||||
|
|
|
@ -6,7 +6,11 @@ import uuid
|
||||||
from hotpocket_backend.apps.accounts.services import (
|
from hotpocket_backend.apps.accounts.services import (
|
||||||
AccessTokensService as BackendAccessTokensService,
|
AccessTokensService as BackendAccessTokensService,
|
||||||
)
|
)
|
||||||
from hotpocket_soa.dto.accounts import AccessTokenOut, AccessTokensQuery
|
from hotpocket_soa.dto.accounts import (
|
||||||
|
AccessTokenMetaUpdateIn,
|
||||||
|
AccessTokenOut,
|
||||||
|
AccessTokensQuery,
|
||||||
|
)
|
||||||
|
|
||||||
from .base import ProxyService, SOAError
|
from .base import ProxyService, SOAError
|
||||||
|
|
||||||
|
@ -76,6 +80,33 @@ class AccessTokensService(ProxyService):
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def get_by_key(self,
|
||||||
|
*,
|
||||||
|
account_uuid: uuid.UUID,
|
||||||
|
key: str,
|
||||||
|
) -> AccessTokenOut:
|
||||||
|
try:
|
||||||
|
result = AccessTokenOut.model_validate(
|
||||||
|
self.call(
|
||||||
|
self.backend_access_tokens_service,
|
||||||
|
'get_by_key',
|
||||||
|
key=key,
|
||||||
|
),
|
||||||
|
from_attributes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.account_uuid != account_uuid:
|
||||||
|
raise self.AccessTokenAccessDenied(
|
||||||
|
f'account_uuid=`{account_uuid}` key=`{key}`',
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
except SOAError as exception:
|
||||||
|
if isinstance(exception.__cause__, BackendAccessTokensService.AccessTokenNotFound) is True:
|
||||||
|
raise self.AccessTokenNotFound(f'account_uuid=`{account_uuid}` pk=`{key}`') from exception
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
def search(self,
|
def search(self,
|
||||||
*,
|
*,
|
||||||
query: AccessTokensQuery,
|
query: AccessTokensQuery,
|
||||||
|
@ -98,3 +129,18 @@ class AccessTokensService(ProxyService):
|
||||||
'delete',
|
'delete',
|
||||||
pk=access_token.pk,
|
pk=access_token.pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_meta(self,
|
||||||
|
*,
|
||||||
|
access_token: AccessTokenOut,
|
||||||
|
update: AccessTokenMetaUpdateIn,
|
||||||
|
) -> AccessTokenOut:
|
||||||
|
return AccessTokenOut.model_validate(
|
||||||
|
self.call(
|
||||||
|
self.backend_access_tokens_service,
|
||||||
|
'update_meta',
|
||||||
|
pk=access_token.pk,
|
||||||
|
update=update,
|
||||||
|
),
|
||||||
|
from_attributes=True,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user