diff --git a/README.md b/README.md index dd451fd..dcf3d89 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,23 @@ # HotPocket by BTHLabs -This repository contains the _HotPocket_ project. +Minimal self-hosted bookmarking app :). + +## The what, the why and the ugly + +HotPocket is a minimal self-hosted bookmarking app. It combines a Web +application, companion apps, browser extensions to give you a way to quickly +save links for later. + +HotPocket came to be to fill in the blank left by Pocket, after Mozilla shut it +down. I looked at the existing alternatives and found them either too +feature-rich, too involved to self-host or otherwise not to my liking. So I +decided to sit down and build something for myself. + +With the what and why out of the way, let's talk about the ugly... At its core +HotPocket is a personal project. I built it by myself and for myself. It may +or may not fit your needs. If it does, happy saving! + +If you're feeling up for an adventure, continue reading below :). ## Development setup @@ -128,6 +145,7 @@ that can be used to configure the services. | `HOTPOCKET_BACKEND_RUN_MIGRATIONS` | `false` or `true` | Set to `true` to run database muigrations when the container starts. | | `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME` | N/A | Username for the initial account. | | `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD` | N/A | Password for the initial account. | +| `HOTPOCKET_BACKEND_OPERATOR_EMAIL` | N/A | Instance operator's e-mail. Used to display extra language on login page. | **Env and App settings** diff --git a/deployment/aio/docker-compose.yaml b/deployment/aio/docker-compose.yaml index 353a4be..c0f6d1e 100644 --- a/deployment/aio/docker-compose.yaml +++ b/deployment/aio/docker-compose.yaml @@ -1,6 +1,6 @@ services: backend: - image: "hotpocket/backend:aio-v25.11.19-01" + image: "bthlabs/hotpocket:aio-v25.11.19-01" environment: HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright" HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket" diff --git a/deployment/fullstack/docker-compose.yaml b/deployment/fullstack/docker-compose.yaml index 76106db..a1b8de4 100644 --- a/deployment/fullstack/docker-compose.yaml +++ b/deployment/fullstack/docker-compose.yaml @@ -8,7 +8,7 @@ x-backend-environment: &x-backend-environment services: webapp: - image: "hotpocket/backend:deployment-v25.11.19-01" + image: "bthlabs/hotpocket:deployment-v25.11.19-01" environment: <<: *x-backend-environment HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net" @@ -21,7 +21,7 @@ services: restart: "unless-stopped" admin: - image: "hotpocket/backend:deployment-v25.11.19-01" + image: "bthlabs/hotpocket:deployment-v25.11.19-01" environment: <<: *x-backend-environment HOTPOCKET_BACKEND_APP: "admin" @@ -35,7 +35,7 @@ services: restart: "unless-stopped" celery-worker: - image: "hotpocket/backend:deployment-v25.11.19-01" + image: "bthlabs/hotpocket:deployment-v25.11.19-01" command: - "/srv/venv/bin/celery" - "-A" @@ -57,7 +57,7 @@ services: restart: "unless-stopped" celery-beat: - image: "hotpocket/backend:deployment-v25.11.19-01" + image: "bthlabs/hotpocket:deployment-v25.11.19-01" command: - "/srv/venv/bin/celery" - "-A" diff --git a/services/apple/Shared (App)/HPAuthFlow.m b/services/apple/Shared (App)/HPAuthFlow.m index cadddd3..a0f94a2 100644 --- a/services/apple/Shared (App)/HPAuthFlow.m +++ b/services/apple/Shared (App)/HPAuthFlow.m @@ -10,6 +10,7 @@ #import "HPAPI.h" #import "HPCredentialsHelper.h" #import "HPRPCClient.h" +#import "NSBundle+HotPocketExtensions.h" #import "NSURL+HotPocketExtensions.h" @implementation HPAuthParams @@ -77,18 +78,19 @@ return nil; } - NSDictionary *postAuthenticateURLParams = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"]; - if (postAuthenticateURLParams == nil) { + NSString *expectedScheme = [NSBundle postAuthenticateURLScheme]; + NSString *expectedHost = [NSBundle postAuthenticateURLHost]; + if (expectedScheme == nil || expectedHost == nil) { return nil; } NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; - if ([urlComponents.scheme isEqualToString:[postAuthenticateURLParams valueForKey:@"scheme"]] == NO) { + if ([urlComponents.scheme isEqualToString:expectedScheme] == NO) { return nil; } - if ([urlComponents.host isEqualToString:[postAuthenticateURLParams valueForKey:@"host"]] == NO) { + if ([urlComponents.host isEqualToString:expectedHost] == NO) { return nil; } diff --git a/services/apple/Shared (App)/NSBundle+HotPocketExtensions.h b/services/apple/Shared (App)/NSBundle+HotPocketExtensions.h index 772f3ac..3e27fe6 100644 --- a/services/apple/Shared (App)/NSBundle+HotPocketExtensions.h +++ b/services/apple/Shared (App)/NSBundle+HotPocketExtensions.h @@ -12,6 +12,9 @@ NS_ASSUME_NONNULL_BEGIN @interface NSBundle (HotPocketExtensions) +(NSString *)uname; ++(NSDictionary *)postAuthenticateURLParams; ++(NSString *)postAuthenticateURLScheme; ++(NSString *)postAuthenticateURLHost; @end diff --git a/services/apple/Shared (App)/NSBundle+HotPocketExtensions.m b/services/apple/Shared (App)/NSBundle+HotPocketExtensions.m index ee3eb8f..2513451 100644 --- a/services/apple/Shared (App)/NSBundle+HotPocketExtensions.m +++ b/services/apple/Shared (App)/NSBundle+HotPocketExtensions.m @@ -14,4 +14,23 @@ return [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]]; } ++(NSDictionary *)postAuthenticateURLParams { + NSDictionary *result = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"]; + if (result == nil) { + return [NSDictionary dictionary]; + } + + return result; +} + ++(NSString *)postAuthenticateURLScheme { + NSDictionary *params = [self postAuthenticateURLParams]; + return [params valueForKey:@"scheme"]; +} + ++(NSString *)postAuthenticateURLHost { + NSDictionary *params = [self postAuthenticateURLParams]; + return [params valueForKey:@"host"]; +} + @end diff --git a/services/apple/iOS (App)/AuthorizationProgressViewController.h b/services/apple/iOS (App)/AuthorizationProgressViewController.h index dcae411..5792abe 100644 --- a/services/apple/iOS (App)/AuthorizationProgressViewController.h +++ b/services/apple/iOS (App)/AuthorizationProgressViewController.h @@ -6,15 +6,19 @@ // #import +#import NS_ASSUME_NONNULL_BEGIN @class MultilineLabel; -@interface AuthorizationProgressViewController : UIViewController +@interface AuthorizationProgressViewController : UIViewController @property IBOutlet UIActivityIndicatorView *progressIndicator; @property IBOutlet MultilineLabel *progressLabel; +@property (strong, nullable) NSURL *authorizationURL; +@property (strong, nullable) ASWebAuthenticationSession *webAuthenticationSession; +@property BOOL userCancelledSession; @end diff --git a/services/apple/iOS (App)/AuthorizationProgressViewController.m b/services/apple/iOS (App)/AuthorizationProgressViewController.m index c263e22..b4a7fec 100644 --- a/services/apple/iOS (App)/AuthorizationProgressViewController.m +++ b/services/apple/iOS (App)/AuthorizationProgressViewController.m @@ -8,22 +8,36 @@ #import "AuthorizationProgressViewController.h" #import "AppDelegate.h" +#import "HPAuthFlow.h" #import "HPCredentialsHelper.h" #import "MultilineLabel.h" +#import "NSBundle+HotPocketExtensions.h" @interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate) #pragma mark - Private interface +-(void)presentAuthorizationError; + @end @implementation AuthorizationProgressViewController #pragma mark - View lifecycle +-(instancetype)initWithCoder:(NSCoder *)coder { + if (self = [super initWithCoder:coder]) { + self.authorizationURL = nil; + self.webAuthenticationSession = nil; + self.userCancelledSession = NO; + } + + return self; +} + -(void)viewDidLoad { [super viewDidLoad]; - self.progressLabel.text = NSLocalizedString(@"Continue to sign out in your browser...", @"Continue to sign out in your browser..."); + self.progressLabel.text = NSLocalizedString(@"Continue to sign in in your browser...", @"Continue to sign in in your browser..."); } -(void)viewWillAppear:(BOOL)animated { @@ -42,14 +56,79 @@ object:appDelegate.authFlow]; } --(void)viewWillDisappear:(BOOL)animated { - [super viewWillDisappear:animated]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; +-(void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate]; + + ASWebAuthenticationSessionCompletionHandler completionHandler = ^(NSURL *url, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (error != nil) { +#ifdef DEBUG + NSLog(@"[AuthorizationViewController.session completionHandler] error=`%@`", error); +#endif + if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) { + self.userCancelledSession = YES; + } + [self presentAuthorizationError]; + } else { + HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url]; + if (receivedAuthParams != nil) { + [appDelegate.authFlow handleAuthParams:receivedAuthParams]; + } else { + [self presentAuthorizationError]; + } + } + + self.webAuthenticationSession = nil; + }); + }; + + ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithCustomScheme:[NSBundle postAuthenticateURLScheme]]; + self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:self.authorizationURL + callback:callback + completionHandler:completionHandler]; + self.webAuthenticationSession.presentationContextProvider = self; +#ifdef DEBUG + self.webAuthenticationSession.prefersEphemeralWebBrowserSession = YES; +#endif + + if (self.webAuthenticationSession.canStart == NO) { + [self presentAuthorizationError]; + return; + } + + [self.webAuthenticationSession start]; } -(void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; + self.webAuthenticationSession = nil; + [self.progressIndicator stopAnimating]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +# pragma mark - Private interface + +-(void)presentAuthorizationError { + if (self.userCancelledSession == NO) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Oops!", @"Oops!") + message:NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.") + preferredStyle:UIAlertControllerStyleAlert]; + + [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Oh well", @"Oh well") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [alert dismissViewControllerAnimated:YES completion:^{ + [self.navigationController popViewControllerAnimated:YES]; + }]; + }]]; + + [self presentViewController:alert animated:YES completion:nil]; + } else { + [self.navigationController popViewControllerAnimated:YES]; + } } #pragma mark - Notification handlers @@ -62,19 +141,7 @@ HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials]; if (credentials.usable == NO) { - UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Oops!", @"Oops!") - message:NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.") - preferredStyle:UIAlertControllerStyleAlert]; - - [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Oh well", @"Oh well") - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [alert dismissViewControllerAnimated:YES completion:^{ - [self.navigationController popViewControllerAnimated:YES]; - }]; - }]]; - - [self presentViewController:alert animated:YES completion:nil]; + [self presentAuthorizationError]; } else { [self.navigationController popToRootViewControllerAnimated:YES]; } @@ -85,4 +152,10 @@ self.progressLabel.text = NSLocalizedString(@"Processing authorization...", @"Processing authorization..."); } +# pragma mark - ASWebAuthenticationPresentationContextProviding implementation + +-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session { + return self.view.window; +} + @end diff --git a/services/apple/iOS (App)/AuthorizationViewController.m b/services/apple/iOS (App)/AuthorizationViewController.m index 2353a8a..32da079 100644 --- a/services/apple/iOS (App)/AuthorizationViewController.m +++ b/services/apple/iOS (App)/AuthorizationViewController.m @@ -73,14 +73,9 @@ return; } - if ([application canOpenURL:authURL] == YES) { - [application openURL:authURL options:@{} completionHandler:^(BOOL result) { - if (result == YES) { - AuthorizationProgressViewController *authorizationProgressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationProgressViewController"]; - [self.navigationController pushViewController:authorizationProgressViewController animated:YES]; - } - }]; - } + AuthorizationProgressViewController *authorizationProgressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationProgressViewController"]; + authorizationProgressViewController.authorizationURL = authURL; + [self.navigationController pushViewController:authorizationProgressViewController animated:YES]; } #pragma mark - Event handlers diff --git a/services/apple/macOS (App)/AppDelegate.m b/services/apple/macOS (App)/AppDelegate.m index f57ea68..335f267 100644 --- a/services/apple/macOS (App)/AppDelegate.m +++ b/services/apple/macOS (App)/AppDelegate.m @@ -21,19 +21,6 @@ } -(void)application:(NSApplication *)application openURLs:(NSArray *)urls { - HPAuthParams *receivedAuthParams = nil; - for (NSURL *url in urls) { - receivedAuthParams = [self.authFlow handlePostAuthenticateURL:url]; - - if (receivedAuthParams != nil) { - break; - } - } - - if (receivedAuthParams != nil) { - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - [self.authFlow handleAuthParams:receivedAuthParams]; - } } @end diff --git a/services/apple/macOS (App)/AuthorizationProgressViewController.h b/services/apple/macOS (App)/AuthorizationProgressViewController.h index ef3d987..1ffcbc8 100644 --- a/services/apple/macOS (App)/AuthorizationProgressViewController.h +++ b/services/apple/macOS (App)/AuthorizationProgressViewController.h @@ -6,13 +6,19 @@ // #import +#import NS_ASSUME_NONNULL_BEGIN -@interface AuthorizationProgressViewController : NSViewController +@interface AuthorizationProgressViewController : NSViewController @property IBOutlet NSProgressIndicator *progressIndicator; @property NSString *progressLabelTitle; +@property (nullable, strong) NSURL *authorizationURL; +@property (nullable, strong) ASWebAuthenticationSession *webAuthenticationSession; +@property BOOL userCancelledSession; + +-(IBAction)doCancel:(id)sender; @end diff --git a/services/apple/macOS (App)/AuthorizationProgressViewController.m b/services/apple/macOS (App)/AuthorizationProgressViewController.m index 30d12e4..f011050 100644 --- a/services/apple/macOS (App)/AuthorizationProgressViewController.m +++ b/services/apple/macOS (App)/AuthorizationProgressViewController.m @@ -9,27 +9,42 @@ #import "AppDelegate.h" #import "AuthorizationViewController.h" +#import "HPAuthFlow.h" #import "HPCredentialsHelper.h" #import "MainViewController.h" +#import "NSBundle+HotPocketExtensions.h" #import "ReplaceAnimator.h" @interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate) #pragma mark - Private interface +-(void)presentAuthorizationError; + @end @implementation AuthorizationProgressViewController #pragma mark - View lifecycle +-(instancetype)initWithCoder:(NSCoder *)coder { + if (self = [super initWithCoder:coder]) { + self.authorizationURL = nil; + self.webAuthenticationSession = nil; + self.userCancelledSession = NO; + } + + return self; +} + -(void)viewDidLoad { [super viewDidLoad]; - self.progressLabelTitle = NSLocalizedString(@"Continue to sign out in your browser...", @"Continue to sign out in your browser..."); + self.progressLabelTitle = NSLocalizedString(@"Continue to sign in in your browser...", @"Continue to sign in in your browser..."); } -(void)viewWillAppear { AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAuthFlowDidFinish:) name:@"AuthFlowDidFinish" @@ -43,12 +58,84 @@ [self.progressIndicator startAnimation:self]; } +-(void)viewDidAppear { + [super viewDidAppear]; + AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate]; + + ASWebAuthenticationSessionCompletionHandler completionHandler = ^(NSURL *url, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (error != nil) { +#ifdef DEBUG + NSLog(@"[AuthorizationViewController.session completionHandler] error=`%@`", error); +#endif + if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) { + self.userCancelledSession = YES; + } + [self presentAuthorizationError]; + } else { + HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url]; + if (receivedAuthParams != nil) { + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + [appDelegate.authFlow handleAuthParams:receivedAuthParams]; + } else { + [self presentAuthorizationError]; + } + } + + self.webAuthenticationSession = nil; + }); + }; + + ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithCustomScheme:[NSBundle postAuthenticateURLScheme]]; + self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:self.authorizationURL + callback:callback + completionHandler:completionHandler]; + self.webAuthenticationSession.presentationContextProvider = self; +#ifdef DEBUG + self.webAuthenticationSession.prefersEphemeralWebBrowserSession = YES; +#endif + + if (self.webAuthenticationSession.canStart == NO) { + [self presentAuthorizationError]; + return; + } + + [self.webAuthenticationSession start]; +} + -(void)viewDidDisappear { + [super viewDidDisappear]; + self.webAuthenticationSession = nil; + [self.progressIndicator stopAnimation:self]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } +#pragma mark - Actions + +-(IBAction)doCancel:(id)sender { + [self.webAuthenticationSession cancel]; +} + +#pragma mark - Private interface + +-(void)presentAuthorizationError { + if (self.userCancelledSession == NO) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleCritical; + alert.messageText = NSLocalizedString(@"Oops!", @"Oops!"); + alert.informativeText = NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation."); + [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response) { + AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"]; + [self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]]; + }]; + } else { + AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"]; + [self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]]; + } +} + #pragma mark - Notification handlers -(void)onAuthFlowDidFinish:(NSNotification *)notification { @@ -59,14 +146,7 @@ [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; if (credentials.usable == NO) { - NSAlert *alert = [[NSAlert alloc] init]; - alert.alertStyle = NSAlertStyleCritical; - alert.messageText = NSLocalizedString(@"Oops!", @"Oops!"); - alert.informativeText = NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation."); - [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response) { - AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"]; - [self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]]; - }]; + [self presentAuthorizationError]; } else { MainViewController *mainViewController = [self.storyboard instantiateControllerWithIdentifier:@"MainViewController"]; [self presentViewController:mainViewController animator:[[ReplaceAnimator alloc] init]]; @@ -78,4 +158,10 @@ self.progressLabelTitle = NSLocalizedString(@"Processing authorization...", @"Processing authorization..."); } +# pragma mark - ASWebAuthenticationPresentationContextProviding implementation + +-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session { + return self.view.window; +} + @end diff --git a/services/apple/macOS (App)/AuthorizationViewController.m b/services/apple/macOS (App)/AuthorizationViewController.m index 29c0899..340731c 100644 --- a/services/apple/macOS (App)/AuthorizationViewController.m +++ b/services/apple/macOS (App)/AuthorizationViewController.m @@ -31,31 +31,24 @@ #pragma mark - Actions -(IBAction)doStartAuthorizationFlow:(id)sender { - NSAlert *alert = [[NSAlert alloc] init]; - alert.alertStyle = NSAlertStyleInformational; - alert.messageText = NSLocalizedString(@"Continue in the browser", @"Continue in the browser"); - alert.informativeText = NSLocalizedString(@"HotPocket will now open the instance in your browser and you can continue to sign in.", @"HotPocket will now open the instance in your browser and you can continue to sign in."); + AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate]; + appDelegate.authFlow.baseURL = [NSURL URLWithString:self.baseURL]; - [alert addButtonWithTitle:NSLocalizedString(@"Let's go!", @"Let's go!")]; - [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Cancel")]; + NSURL *authURL = [appDelegate.authFlow start]; + if (authURL == nil) { + NSBeep(); + return; + } - [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response) { - if (response == NSAlertFirstButtonReturn) { - AppDelegate *appDeleate = [[NSApplication sharedApplication] delegate]; - appDeleate.authFlow.baseURL = [NSURL URLWithString:self.baseURL]; - - NSURL *authURL = [appDeleate.authFlow start]; - if (authURL == nil) { - NSBeep(); - return; - } + AuthorizationProgressViewController *authProgressViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationProgressViewController"]; + authProgressViewController.authorizationURL = authURL; + [self presentViewController:authProgressViewController animator:[[ReplaceAnimator alloc] init]]; +} - AuthorizationProgressViewController *authProgressViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationProgressViewController"]; - [self presentViewController:authProgressViewController animator:[[ReplaceAnimator alloc] init]]; - - [[NSWorkspace sharedWorkspace] openURL:authURL]; - } - }]; +# pragma mark - ASWebAuthenticationPresentationContextProviding implementation + +-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session { + return self.view.window; } @end diff --git a/services/apple/macOS (App)/Base.lproj/Main.storyboard b/services/apple/macOS (App)/Base.lproj/Main.storyboard index 5657a0c..34e75b4 100644 --- a/services/apple/macOS (App)/Base.lproj/Main.storyboard +++ b/services/apple/macOS (App)/Base.lproj/Main.storyboard @@ -207,6 +207,20 @@ + diff --git a/services/backend/hotpocket_backend/apps/core/types.py b/services/backend/hotpocket_backend/apps/core/types.py index b358bb6..de11b61 100644 --- a/services/backend/hotpocket_backend/apps/core/types.py +++ b/services/backend/hotpocket_backend/apps/core/types.py @@ -38,3 +38,5 @@ class PSettings(typing.Protocol): UI_PAGE_HEAD_INCLUDES: list UI_PAGE_SCRIPT_INCLUDES: list + + OPERATOR_EMAIL: str | None diff --git a/services/backend/hotpocket_backend/apps/ui/context_processors.py b/services/backend/hotpocket_backend/apps/ui/context_processors.py index 223cbcb..96a21b2 100644 --- a/services/backend/hotpocket_backend/apps/ui/context_processors.py +++ b/services/backend/hotpocket_backend/apps/ui/context_processors.py @@ -100,3 +100,9 @@ def page_includes(request: HttpRequest) -> dict: 'script': settings.UI_PAGE_SCRIPT_INCLUDES, }, } + + +def operator_email(request: HttpRequest) -> dict: + return { + 'OPERATOR_EMAIL': settings.OPERATOR_EMAIL, + } diff --git a/services/backend/hotpocket_backend/apps/ui/dto/rpc.py b/services/backend/hotpocket_backend/apps/ui/dto/rpc.py new file mode 100644 index 0000000..a5d59d5 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/dto/rpc.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import uuid + +import pydantic + + +class SavesCreateOut(pydantic.BaseModel): + id: uuid.UUID + target_uuid: uuid.UUID + url: pydantic.AnyHttpUrl + + def to_rpc(self) -> dict: + return { + 'id': self.id, + 'target_uuid': self.target_uuid, + 'url': str(self.url), + } diff --git a/services/backend/hotpocket_backend/apps/ui/rpc_methods/saves.py b/services/backend/hotpocket_backend/apps/ui/rpc_methods/saves.py index e62dc6e..f61d8c0 100644 --- a/services/backend/hotpocket_backend/apps/ui/rpc_methods/saves.py +++ b/services/backend/hotpocket_backend/apps/ui/rpc_methods/saves.py @@ -3,19 +3,28 @@ from __future__ import annotations from bthlabs_jsonrpc_core import register_method from django.http import HttpRequest +from django.urls import reverse from hotpocket_backend.apps.core.rpc import wrap_soa_errors +from hotpocket_backend.apps.ui.dto.rpc import SavesCreateOut from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow -from hotpocket_soa.dto.associations import AssociationOut @register_method(method='saves.create') @wrap_soa_errors -def create(request: HttpRequest, url: str) -> AssociationOut: +def create(request: HttpRequest, url: str) -> SavesCreateOut: association = CreateSaveWorkflow().run_rpc( request=request, account=request.user, url=url, ) - return association + result = SavesCreateOut.model_validate({ + 'id': association.pk, + 'target_uuid': association.target_uuid, + 'url': request.build_absolute_uri(reverse( + 'ui.associations.view', args=(association.pk,), + )), + }) + + return result diff --git a/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/login.html b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/login.html index 80aa5be..c130de3 100644 --- a/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/login.html +++ b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/login.html @@ -36,6 +36,12 @@ {% blocktranslate %}Log in with {{ HOTPOCKET_OIDC_DISPLAY_NAME }}{% endblocktranslate %} {% endif %} + + {% if OPERATOR_EMAIL %} +

+ {% blocktranslate %}For support with your account, contact the instance operator.{% endblocktranslate %} +

+ {% endif %} {% include "ui/ui/partials/uname.html" %} diff --git a/services/backend/hotpocket_backend/settings/base.py b/services/backend/hotpocket_backend/settings/base.py index 4dafd13..6d564fd 100644 --- a/services/backend/hotpocket_backend/settings/base.py +++ b/services/backend/hotpocket_backend/settings/base.py @@ -72,6 +72,7 @@ TEMPLATES = [ 'hotpocket_backend.apps.ui.context_processors.debug', 'hotpocket_backend.apps.ui.context_processors.version', 'hotpocket_backend.apps.ui.context_processors.appearance_settings', + 'hotpocket_backend.apps.ui.context_processors.operator_email', ], }, }, @@ -295,3 +296,5 @@ SAVES_ASSOCIATION_ADAPTER = os.environ.get( UPLOADS_PATH = None SITE_SHORT_TITLE = 'HotPocket' + +OPERATOR_EMAIL = os.environ.get('HOTPOCKET_BACKEND_OPERATOR_EMAIL', None) diff --git a/services/backend/tests/ui/views/rpc/saves/test_create.py b/services/backend/tests/ui/views/rpc/saves/test_create.py index f9d293d..2688f21 100644 --- a/services/backend/tests/ui/views/rpc/saves/test_create.py +++ b/services/backend/tests/ui/views/rpc/saves/test_create.py @@ -54,8 +54,12 @@ def test_ok(authenticated_client: Client, call_result = result.json() assert 'error' not in call_result - save_pk = uuid.UUID(call_result['result']['target_uuid']) association_pk = uuid.UUID(call_result['result']['id']) + save_pk = uuid.UUID(call_result['result']['target_uuid']) + + assert call_result['result']['url'].endswith(reverse( + 'ui.associations.view', args=(association_pk,), + )) AssociationsTestingService().assert_created( pk=association_pk, diff --git a/services/extension/assets/_locales/en/messages.json b/services/extension/assets/_locales/en/messages.json index 13ca3ef..eb4cc31 100644 --- a/services/extension/assets/_locales/en/messages.json +++ b/services/extension/assets/_locales/en/messages.json @@ -30,5 +30,9 @@ "content_popup_content_error_message": { "message": "HotPocket couldn't complete this operation.", "description": "Title of the error content popup." + }, + "content_popup_content_view_button_message": { + "message": "View in HotPocket", + "description": "Title of the view link button" } } diff --git a/services/extension/src/background/main.js b/services/extension/src/background/main.js index 86fa370..2ca144c 100644 --- a/services/extension/src/background/main.js +++ b/services/extension/src/background/main.js @@ -117,10 +117,10 @@ const doSave = async (accessToken, tab) => { ); if (error !== null) { - return false; + return null; } - return true; + return result; }; const doCreateAndStoreAccessToken = async (authKey) => { diff --git a/services/extension/src/content/main.js b/services/extension/src/content/main.js index 5479892..3c3d6c9 100644 --- a/services/extension/src/content/main.js +++ b/services/extension/src/content/main.js @@ -26,7 +26,7 @@ class Popup { element.insertAdjacentHTML('beforeend', content); }; - setContent = (content) => { + setContent = (content, saveUrl) => { const shadow = this.container.shadowRoot; const body = shadow.querySelector('.hotpocket-extension-popup-body'); @@ -38,6 +38,13 @@ class Popup { i18nElement.dataset.message, ); } + + if (saveUrl) { + const viewLink = shadow.querySelector('.hotpocket-extension-popup-view'); + if (viewLink) { + viewLink.href = saveUrl; + } + } }; close = () => { this.clearCloseTimeout(); @@ -47,7 +54,7 @@ class Popup { this.container = null; } }; - show = (content) => { + show = (content, saveUrl) => { this.close(); this.container = document.createElement('div'); @@ -57,7 +64,7 @@ class Popup { const shadow = this.container.attachShadow({mode: 'open'}); shadow.setHTMLUnsafe(POPUP); - this.setContent(content); + this.setContent(content, saveUrl); const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close'); for (const closeElement of closeElements) { @@ -66,8 +73,8 @@ class Popup { document.body.appendChild(this.container); }; - update = (content) => { - this.setContent(content); + update = (content, saveUrl) => { + this.setContent(content, saveUrl); }; onCloseClick = (event) => { this.close(); @@ -86,16 +93,16 @@ const doHandleBrowserActionClickedMessage = (message) => { }; const doHandleSaveMessage = (message) => { - let content = POPUP_CONTENT_ERROR; - if (message.result === true) { - content = POPUP_CONTENT_SUCCESS; + let content = POPUP_CONTENT_SUCCESS; + if (message.result === null) { + content = POPUP_CONTENT_ERROR; } if (currentPopup === null) { currentPopup = new Popup(); currentPopup.show(content); } else { - currentPopup.update(content); + currentPopup.update(content, (message.result || {}).url); } }; diff --git a/services/extension/src/content/templates/popup.html b/services/extension/src/content/templates/popup.html index e96837a..77a2dd0 100644 --- a/services/extension/src/content/templates/popup.html +++ b/services/extension/src/content/templates/popup.html @@ -94,6 +94,26 @@ opacity: 0.83; transform: rotate(60deg); } +.hotpocket-extension-popup-view { + background-color: #1CBAED; + border: 1px solid #1CBAED; + border-radius: 0.25rem; + color: white; + display: inline-block; + font-size: 0.875rem; + line-height: 1.25; + margin-top: 0.25rem !important; + padding: 0.25rem 0.5rem; + text-decoration: none; +} +.hotpocket-extension-popup-view:hover { + background-color: #189ec9; + border-color: #1695be; +} +.hotpocket-extension-popup-view:active { + background-color: #1695be; + border-color: #158cb2; +} @keyframes hotpocket-extension-popup-loader-animation { 100% {transform: rotate(1turn)} } @@ -104,6 +124,6 @@ HotPocket by BTHLabs × -
+
diff --git a/services/extension/src/content/templates/popup_content_success.html b/services/extension/src/content/templates/popup_content_success.html index 21e953c..6477b42 100644 --- a/services/extension/src/content/templates/popup_content_success.html +++ b/services/extension/src/content/templates/popup_content_success.html @@ -2,4 +2,13 @@
+
+ +