3 Commits

Author SHA1 Message Date
29c732faa0 Release v25.9.12
All checks were successful
CI / Checks (push) Successful in 15m7s
2025-09-11 20:50:18 +02:00
dcebccf947 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>
2025-09-11 15:57:11 +00:00
67138c7035 BTHLABS-0000: Use absolute URLs in ui.meta.manifest_json 2025-09-09 15:21:45 +02:00
28 changed files with 523 additions and 86 deletions

View File

@@ -66,7 +66,7 @@ $ docker run --rm -it \
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \ -e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \ -e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \
-p 8000:8000 \ -p 8000:8000 \
docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.9.8-01 docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.9.12-01
``` ```
The command above will set up and start the application. The SQLite file will The command above will set up and start the application. The SQLite file will

View File

@@ -1,6 +1,6 @@
services: services:
backend: backend:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.9.8-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.9.12-01"
environment: environment:
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright" HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket" HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"

View File

@@ -8,7 +8,7 @@ x-backend-environment: &x-backend-environment
services: services:
webapp: webapp:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.8-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.12-01"
environment: environment:
<<: *x-backend-environment <<: *x-backend-environment
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net" HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
@@ -21,7 +21,7 @@ services:
restart: "unless-stopped" restart: "unless-stopped"
admin: admin:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.8-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.12-01"
environment: environment:
<<: *x-backend-environment <<: *x-backend-environment
HOTPOCKET_BACKEND_APP: "admin" HOTPOCKET_BACKEND_APP: "admin"
@@ -35,7 +35,7 @@ services:
restart: "unless-stopped" restart: "unless-stopped"
celery-worker: celery-worker:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.8-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.12-01"
command: command:
- "/srv/venv/bin/celery" - "/srv/venv/bin/celery"
- "-A" - "-A"
@@ -57,7 +57,7 @@ services:
restart: "unless-stopped" restart: "unless-stopped"
celery-beat: celery-beat:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.8-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.9.12-01"
command: command:
- "/srv/venv/bin/celery" - "/srv/venv/bin/celery"
- "-A" - "-A"

View File

@@ -462,7 +462,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2025091201;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist"; INFOPLIST_FILE = "iOS (Extension)/Info.plist";
@@ -474,12 +474,12 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 25.9.8; MARKETING_VERSION = 25.9.12;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-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;
@@ -492,7 +492,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2025091201;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist"; INFOPLIST_FILE = "iOS (Extension)/Info.plist";
@@ -504,12 +504,12 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 25.9.8; MARKETING_VERSION = 25.9.12;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-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,31 +525,36 @@
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 = 2025091201;
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";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket; INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
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)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 25.9.8; MARKETING_VERSION = 25.9.12;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
"-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,31 +566,36 @@
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 = 2025091201;
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";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket; INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
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)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 25.9.8; MARKETING_VERSION = 25.9.12;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
"-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;
@@ -598,7 +608,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements"; CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2025091201;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -612,12 +622,12 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 25.9.8; MARKETING_VERSION = 25.9.12;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-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,9 +639,9 @@
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 = 2025091201;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -645,12 +655,12 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 25.9.8; MARKETING_VERSION = 25.9.12;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-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;
@@ -666,12 +676,13 @@
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements"; CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2025091201;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = HotPocket; INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication; INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -679,14 +690,14 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.9.8; MARKETING_VERSION = 25.9.12;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
"-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,14 +711,15 @@
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 = 2025091201;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = HotPocket; INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
INFOPLIST_KEY_NSMainStoryboardFile = Main; INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication; INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -715,14 +727,14 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.9.8; MARKETING_VERSION = 25.9.12;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
"-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;

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
version = '25.9.8' version = '25.9.12'

View File

@@ -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

View File

@@ -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

View File

@@ -12,24 +12,32 @@ def manifest_json(request: HttpRequest) -> JsonResponse:
result = { result = {
'name': settings.SITE_TITLE, 'name': settings.SITE_TITLE,
'short_name': settings.SITE_SHORT_TITLE, 'short_name': settings.SITE_SHORT_TITLE,
'start_url': reverse('ui.associations.browse'), 'start_url': request.build_absolute_uri(
reverse('ui.associations.browse'),
),
'display': 'standalone', 'display': 'standalone',
'background_color': '#212529', 'background_color': '#212529',
'theme_color': '#2b3035', 'theme_color': '#2b3035',
'icons': [ 'icons': [
{ {
'src': static('ui/img/icon-192.png'), 'src': request.build_absolute_uri(
static('ui/img/icon-192.png'),
),
'sizes': '192x192', 'sizes': '192x192',
'type': 'image/png', 'type': 'image/png',
}, },
{ {
'src': static('ui/img/icon-512.png'), 'src': request.build_absolute_uri(
static('ui/img/icon-512.png'),
),
'sizes': '512x512', 'sizes': '512x512',
'type': 'image/png', 'type': 'image/png',
}, },
], ],
'share_target': { 'share_target': {
'action': reverse('ui.integrations.android.share_sheet'), 'action': request.build_absolute_uri(
reverse('ui.integrations.android.share_sheet'),
),
'method': 'POST', 'method': 'POST',
'enctype': 'multipart/form-data', 'enctype': 'multipart/form-data',
'params': { 'params': {

View File

@@ -13,7 +13,7 @@ cat <<EOF
|_| |_|
production production
HotPocket v25.9.8 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/) HotPocket v25.9.12 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/)
Copyright 2025-present by BTHLabs. All rights reserved. (https://bthlabs.pl/) Copyright 2025-present by BTHLabs. All rights reserved. (https://bthlabs.pl/)
Licensed under Apache-2.0 Licensed under Apache-2.0
EOF EOF

View File

@@ -1,6 +1,6 @@
{ {
"name": "hotpocket-backend", "name": "hotpocket-backend",
"version": "25.9.8", "version": "25.9.12",
"description": "HotPocket Backend", "description": "HotPocket Backend",
"main": "hotpocket_backend/apps/frontend/src/index.js", "main": "hotpocket_backend/apps/frontend/src/index.js",
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket", "repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "hotpocket-backend" name = "hotpocket-backend"
version = "25.9.8" version = "25.9.12"
description = "HotPocket Backend" description = "HotPocket Backend"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"] authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0" license = "Apache-2.0"

View File

@@ -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)

View File

@@ -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

View File

@@ -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',
}

View File

@@ -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

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# type: ignore
from __future__ import annotations
import http
from django.test import Client
from django.urls import reverse
def test_ok(client: Client, settings):
# When
result = client.get(reverse('ui.meta.manifest_json'))
# Then
assert result.status_code == http.HTTPStatus.OK
payload = result.json()
assert payload['name'] == settings.SITE_TITLE
assert payload['short_name'] == settings.SITE_SHORT_TITLE
assert payload['start_url'] == f"http://testserver{reverse('ui.associations.browse')}"
assert payload['share_target']['action'] == (
f"http://testserver{reverse('ui.integrations.android.share_sheet')}"
)

View File

@@ -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,
}, },
) )

View File

@@ -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

View 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>

View File

@@ -1,6 +1,6 @@
{ {
"name": "hotpocket-extension", "name": "hotpocket-extension",
"version": "25.9.8", "version": "25.9.12",
"description": "HotPocket Extension", "description": "HotPocket Extension",
"main": "src/index.js", "main": "src/index.js",
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket", "repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "hotpocket-extension" name = "hotpocket-extension"
version = "25.9.8" version = "25.9.12"
description = "HotPocket Extension" description = "HotPocket Extension"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"] authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0" license = "Apache-2.0"

View File

@@ -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);
}, },
}; };

View File

@@ -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,

View File

@@ -3,7 +3,7 @@
"default_locale": "en", "default_locale": "en",
"name": "__MSG_extension_name__", "name": "__MSG_extension_name__",
"description": "__MSG_extension_description__", "description": "__MSG_extension_description__",
"version": "25.9.8", "version": "25.9.12",
"icons": { "icons": {
"48": "images/icon-48.png", "48": "images/icon-48.png",
"64": "images/icon-64.png", "64": "images/icon-64.png",

View File

@@ -4,6 +4,7 @@
"scripts": [ "scripts": [
"background-bundle.js" "background-bundle.js"
], ],
"type": "module" "type": "module",
"persistent": false
} }
} }

View File

@@ -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

View File

@@ -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,
)