BTHLABS-57: Pre-auth page in the extension
This commit is contained in:
parent
46254730bd
commit
495255206e
2
services/apple/.gitignore
vendored
2
services/apple/.gitignore
vendored
|
@ -90,3 +90,5 @@ Shared (Extension)/Resources/images/
|
|||
Shared (Extension)/Resources/background-bundle.js
|
||||
Shared (Extension)/Resources/content-bundle.js
|
||||
Shared (Extension)/Resources/manifest.json
|
||||
Shared (Extension)/Resources/preauth.html
|
||||
Shared (Extension)/Resources/preauth.js
|
||||
|
|
|
@ -114,6 +114,8 @@
|
|||
"Resources/content-bundle.js",
|
||||
Resources/images,
|
||||
Resources/manifest.json,
|
||||
Resources/preauth.html,
|
||||
Resources/preauth.js,
|
||||
SafariWebExtensionHandler.m,
|
||||
);
|
||||
target = 4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */;
|
||||
|
@ -126,6 +128,8 @@
|
|||
"Resources/content-bundle.js",
|
||||
Resources/images,
|
||||
Resources/manifest.json,
|
||||
Resources/preauth.html,
|
||||
Resources/preauth.js,
|
||||
SafariWebExtensionHandler.m,
|
||||
);
|
||||
target = 4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */;
|
||||
|
|
|
@ -40,7 +40,9 @@ if (BASE_URL === null) {
|
|||
|
||||
const PLUGINS = [
|
||||
string({
|
||||
include: /\.html$/,
|
||||
include: [
|
||||
'src/content/templates/*.html',
|
||||
],
|
||||
}),
|
||||
replace({
|
||||
include: /\.js$/,
|
||||
|
@ -115,6 +117,13 @@ export default [
|
|||
src: 'assets/images',
|
||||
dest: OUTPUT_PATH,
|
||||
},
|
||||
{
|
||||
src: [
|
||||
'src/content/preauth.html',
|
||||
'src/content/preauth.js',
|
||||
],
|
||||
dest: OUTPUT_PATH,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
|
|
@ -1,8 +1,23 @@
|
|||
import HotPocketExtension from '../common';
|
||||
|
||||
const AUTH_URL = (new URL('/integrations/extension/authenticate/', HotPocketExtension.base_url)).toString();
|
||||
const POST_AUTH_URL = (new URL('/integrations/extension/post-authenticate/', HotPocketExtension.base_url)).toString();
|
||||
const RPC_URL = (new URL('/rpc/', HotPocketExtension.base_url)).toString();
|
||||
const POST_AUTH_PATH = '/integrations/extension/post-authenticate/';
|
||||
const RPC_PATH = '/rpc/';
|
||||
|
||||
let authSessionToken = null;
|
||||
let rpcURL = null;
|
||||
|
||||
const updateRpcURL = () => {
|
||||
rpcURL = null;
|
||||
if (HotPocketExtension.base_url !== null) {
|
||||
rpcURL = (new URL(RPC_PATH, HotPocketExtension.base_url)).toString();
|
||||
}
|
||||
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.background.updateRpcURL()',
|
||||
HotPocketExtension.base_url,
|
||||
rpcURL,
|
||||
);
|
||||
};
|
||||
|
||||
const makeJSONRPCCall = (method, params) => {
|
||||
return {
|
||||
|
@ -43,7 +58,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
|
|||
},
|
||||
);
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.executeJSONRPCCall()', response,
|
||||
'HotPocketExtension.background.executeJSONRPCCall()', response,
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
|
@ -56,7 +71,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
|
|||
const callResult = await response.json();
|
||||
if (callResult.error) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.content.executeJSONRPCCall(): RPC error',
|
||||
'HotPocketExtension.background.executeJSONRPCCall(): RPC error',
|
||||
callResult.error.code,
|
||||
callResult.error.message,
|
||||
callResult.error.data,
|
||||
|
@ -69,7 +84,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => {
|
|||
}
|
||||
} catch (exception) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.content.executeJSONRPCCall(): Fetch error', exception,
|
||||
'HotPocketExtension.background.executeJSONRPCCall(): Fetch error', exception,
|
||||
);
|
||||
error = {
|
||||
code: -32000,
|
||||
|
@ -90,9 +105,9 @@ const getAccessTokenMeta = () => {
|
|||
|
||||
const doSave = async (accessToken, tab) => {
|
||||
const call = makeJSONRPCCall('saves.create', [tab.url]);
|
||||
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {accessToken});
|
||||
const [result, error] = await executeJSONRPCCall(rpcURL, call, {accessToken});
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.doSave():', result, error,
|
||||
'HotPocketExtension.background.doSave():', result, error,
|
||||
);
|
||||
|
||||
if (error !== null) {
|
||||
|
@ -112,7 +127,7 @@ const doCreateAndStoreAccessToken = async (authKey) => {
|
|||
);
|
||||
|
||||
const [accessToken, error] = await executeJSONRPCCall(
|
||||
RPC_URL, accessTokenCall, {accessToken: null},
|
||||
rpcURL, accessTokenCall, {accessToken: null},
|
||||
);
|
||||
|
||||
if (error === null) {
|
||||
|
@ -125,15 +140,34 @@ const doCreateAndStoreAccessToken = async (authKey) => {
|
|||
};
|
||||
|
||||
const doHandleAuthFlow = (authTab) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.background.doHandleAuthFlow()', authTab,
|
||||
);
|
||||
let currentAuthTabId = authTab.id;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const onTabsUpdated = (tabId, changeInfo, updatedTab) => {
|
||||
if (tabId === authTab.id) {
|
||||
const changedURL = changeInfo.url;
|
||||
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.doHandleAuthFlow.onTabsUpdated()', updatedTab, changeInfo,
|
||||
'HotPocketExtension.background.doHandleAuthFlow.onTabsUpdated()',
|
||||
updatedTab,
|
||||
changedURL,
|
||||
(changedURL && changedURL.includes(POST_AUTH_PATH)),
|
||||
changeInfo,
|
||||
);
|
||||
|
||||
const changedURL = changeInfo.url;
|
||||
if (changedURL && changedURL.startsWith(POST_AUTH_URL)) {
|
||||
const expectedSessionTabQuery = `?authSessionToken=${authSessionToken}`;
|
||||
if (tabId !== currentAuthTabId && changedURL.includes(expectedSessionTabQuery)) {
|
||||
// When redirecting from the preauth page to the HotPocket instance,
|
||||
// Safari "replaces" the auth tab with a new one. This nasty hack will
|
||||
// allow the extension to keep track of it.
|
||||
// I hate computers.
|
||||
currentAuthTabId = tabId;
|
||||
}
|
||||
|
||||
if (tabId === currentAuthTabId) {
|
||||
if (changedURL && changedURL.includes(POST_AUTH_PATH)) {
|
||||
const parsedChangedURL = new URL(changedURL);
|
||||
const authKey = parsedChangedURL.searchParams.get('auth_key');
|
||||
|
||||
|
@ -154,8 +188,9 @@ const doHandleAuthFlow = (authTab) => {
|
|||
reject(error);
|
||||
}).
|
||||
finally(() => {
|
||||
authSessionToken = null;
|
||||
HotPocketExtension.api.tabs.onUpdated.removeListener(onTabsUpdated);
|
||||
HotPocketExtension.api.tabs.remove(authTab.id);
|
||||
HotPocketExtension.api.tabs.remove(currentAuthTabId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +210,7 @@ const doCheckAuth = async (accessToken) => {
|
|||
[accessToken, getAccessTokenMeta()],
|
||||
);
|
||||
|
||||
const [result, error] = await executeJSONRPCCall(RPC_URL, call, {
|
||||
const [result, error] = await executeJSONRPCCall(rpcURL, call, {
|
||||
accessToken,
|
||||
});
|
||||
|
||||
|
@ -196,15 +231,28 @@ const doCheckAuth = async (accessToken) => {
|
|||
return accessToken;
|
||||
};
|
||||
|
||||
const doGetAccessToken = async () => {
|
||||
let storageResult = await HotPocketExtension.api.storage.local.get('accessToken');
|
||||
let accessToken = await doCheckAuth(
|
||||
storageResult.accessToken || null,
|
||||
const doSetupRPC = async () => {
|
||||
let storageResult = await HotPocketExtension.api.storage.local.get(
|
||||
['accessToken', 'baseURL'],
|
||||
);
|
||||
|
||||
let accessToken = null;
|
||||
if (storageResult.baseURL) {
|
||||
HotPocketExtension.base_url = storageResult.baseURL;
|
||||
updateRpcURL();
|
||||
|
||||
accessToken = await doCheckAuth(
|
||||
storageResult.accessToken || null,
|
||||
);
|
||||
}
|
||||
|
||||
if (accessToken === null) {
|
||||
authSessionToken = crypto.randomUUID();
|
||||
|
||||
const authTab = await HotPocketExtension.api.tabs.create({
|
||||
url: AUTH_URL,
|
||||
url: HotPocketExtension.api.runtime.getURL(
|
||||
`preauth.html?authSessionToken=${authSessionToken}`,
|
||||
),
|
||||
});
|
||||
|
||||
accessToken = await doHandleAuthFlow(authTab);
|
||||
|
@ -217,24 +265,53 @@ const doSendTabMessage = (tab, message) => {
|
|||
HotPocketExtension.api.tabs.sendMessage(tab.id, message).
|
||||
then((result) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.content.doSendTabMessage(): message sent', message, result,
|
||||
'HotPocketExtension.background.doSendTabMessage(): message sent',
|
||||
message,
|
||||
result,
|
||||
);
|
||||
}).
|
||||
catch((error) => {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.content.doSendTabMessage(): could not send message', error,
|
||||
'HotPocketExtension.background.doSendTabMessage(): could not send message',
|
||||
error,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const doUpdateBaseURL = (nextBaseURL) => {
|
||||
HotPocketExtension.base_url = nextBaseURL;
|
||||
updateRpcURL();
|
||||
|
||||
HotPocketExtension.api.storage.local.
|
||||
set({
|
||||
baseURL: nextBaseURL,
|
||||
}).
|
||||
then(() => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.background.doUpdateBaseURL()', 'Base URL saved',
|
||||
);
|
||||
}).
|
||||
catch((error) => {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.background.doUpdateBaseURL()', error,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onTabCreated = (tab) => {
|
||||
HotPocketExtension.LOGGER.debug('HotPocketExtension.onTabCreated()', tab);
|
||||
HotPocketExtension.api.action.enable(tab.id);
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.background.onTabCreated()', tab,
|
||||
);
|
||||
HotPocketExtension.api.action.enable(tab.id).catch((error) => {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
'HotPocketExtension.background.onTabCreated()', tab.id, error,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onBrowserActionClicked = async (tab) => {
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.onBrowserActionClicked()', tab.url,
|
||||
'HotPocketExtension.background.onBrowserActionClicked()', tab.url,
|
||||
);
|
||||
|
||||
if (!tab.url) {
|
||||
|
@ -245,11 +322,11 @@ const onBrowserActionClicked = async (tab) => {
|
|||
let error = null;
|
||||
|
||||
try {
|
||||
let accessToken = await doGetAccessToken();
|
||||
let accessToken = await doSetupRPC();
|
||||
|
||||
result = await doSave(accessToken, tab);
|
||||
HotPocketExtension.LOGGER.debug(
|
||||
'HotPocketExtension.onBrowserActionClicked()', result,
|
||||
'HotPocketExtension.background.onBrowserActionClicked()', result,
|
||||
);
|
||||
} catch (exception) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
|
@ -276,10 +353,11 @@ const onMessage = (message, sender, sendResponse) => {
|
|||
let response = {ok: true};
|
||||
try {
|
||||
if (message.type === 'HotPocket:Extension:ping') {
|
||||
HotPocketExtension.LOGGER.debug(sender.tab.id);
|
||||
doSendTabMessage(sender.tab, {
|
||||
type: 'HotPocket:Extension:pong',
|
||||
});
|
||||
} else if (message.type === 'HotPocket:Extension:setBaseURL') {
|
||||
doUpdateBaseURL(message.result);
|
||||
}
|
||||
} catch (exception) {
|
||||
HotPocketExtension.LOGGER.error(
|
||||
|
@ -300,6 +378,8 @@ export default ({...configuration}) => {
|
|||
background: true,
|
||||
});
|
||||
|
||||
updateRpcURL();
|
||||
|
||||
HotPocketExtension.api.tabs.onCreated.addListener(onTabCreated);
|
||||
|
||||
HotPocketExtension.api.action.onClicked.addListener(onBrowserActionClicked);
|
||||
|
|
|
@ -8,7 +8,7 @@ const HotPocketExtension = {
|
|||
version: __HOTPOCKET_EXTENSION_VERSION__,
|
||||
debug: DEBUG,
|
||||
api: null,
|
||||
base_url: __HOTPOCKET_EXTENSION_BASE_URL__,
|
||||
base_url: null,
|
||||
LOGGER: {
|
||||
// eslint-disable-next-line no-console
|
||||
debug: (DEBUG === true) ? console.log : noop,
|
||||
|
|
91
services/extension/src/content/preauth.html
Normal file
91
services/extension/src/content/preauth.html
Normal file
File diff suppressed because one or more lines are too long
45
services/extension/src/content/preauth.js
Normal file
45
services/extension/src/content/preauth.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*!
|
||||
* HotPocket by BTHLabs (https://hotpocket.app/)
|
||||
* Copyright 2025-present BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const form = document.getElementById('PreauthForm');
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const inputBaseURL = document.getElementById('id_base_url');
|
||||
const baseURL = inputBaseURL.value;
|
||||
|
||||
let api = window.browser || null;
|
||||
if (api === null && window.chrome) {
|
||||
api = window.chrome;
|
||||
}
|
||||
|
||||
api.runtime.sendMessage({
|
||||
type: 'HotPocket:Extension:setBaseURL',
|
||||
result: baseURL,
|
||||
});
|
||||
|
||||
const loginURL = new URL('/integrations/extension/authenticate/', inputBaseURL.value);
|
||||
window.location.replace(loginURL.toString());
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
})();
|
|
@ -19,7 +19,7 @@
|
|||
"data_collection_permissions": {
|
||||
"required": [
|
||||
"websiteActivity",
|
||||
"technicalAndInteraction"
|
||||
"browsingActivity"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
48
tasks.py
48
tasks.py
|
@ -1,9 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
from invoke import task
|
||||
from invoke import Context, task
|
||||
|
||||
from hotpocket_workspace_tools import get_workspace_mode, WorkspaceMode
|
||||
|
||||
|
@ -63,7 +64,7 @@ WORKSPACE_MODE = get_workspace_mode()
|
|||
ALL_SERVICES = ['backend', 'packages', 'extension']
|
||||
|
||||
|
||||
def _run_in_service(ctx, service, command, **kwargs):
|
||||
def _run_in_service(ctx: Context, service, command, **kwargs):
|
||||
match WORKSPACE_MODE:
|
||||
case WorkspaceMode.DOCKER:
|
||||
return ctx.run(
|
||||
|
@ -75,7 +76,7 @@ def _run_in_service(ctx, service, command, **kwargs):
|
|||
ctx.run(f'direnv exec . {command}')
|
||||
|
||||
|
||||
def _get_version(ctx, service):
|
||||
def _get_version(ctx: Context, service):
|
||||
with ctx.cd(f'services/{service}'):
|
||||
run_result = ctx.run('poetry version -s', hide='out')
|
||||
|
||||
|
@ -88,27 +89,27 @@ def _get_head_sha(ctx):
|
|||
|
||||
|
||||
@task
|
||||
def clean(ctx, service):
|
||||
def clean(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv clean')
|
||||
|
||||
|
||||
@task
|
||||
def test(ctx, service):
|
||||
def test(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv test')
|
||||
|
||||
|
||||
@task
|
||||
def lint(ctx, service):
|
||||
def lint(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv lint')
|
||||
|
||||
|
||||
@task
|
||||
def typecheck(ctx, service):
|
||||
def typecheck(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv typecheck')
|
||||
|
||||
|
||||
@task
|
||||
def shell(ctx, service):
|
||||
def shell(ctx: Context, service):
|
||||
assert WORKSPACE_MODE == WorkspaceMode.DOCKER, (
|
||||
'Just `cd services/{service}` ;)'
|
||||
)
|
||||
|
@ -116,12 +117,21 @@ def shell(ctx, service):
|
|||
|
||||
|
||||
@task
|
||||
def django_shell(ctx, service):
|
||||
def django_shell(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv django-shell')
|
||||
|
||||
|
||||
@task
|
||||
def build(ctx,
|
||||
def png_to_data_url(ctx: Context, png_path):
|
||||
with open(png_path, 'rb') as png_f:
|
||||
data = png_f.read()
|
||||
|
||||
encoded_data = base64.b64encode(data)
|
||||
print(f'data:image/png;base64,{encoded_data.decode("utf-8")}')
|
||||
|
||||
|
||||
@task
|
||||
def build(ctx: Context,
|
||||
service,
|
||||
context=None,
|
||||
builder=None,
|
||||
|
@ -175,7 +185,7 @@ def build(ctx,
|
|||
|
||||
|
||||
@task
|
||||
def publish(ctx,
|
||||
def publish(ctx: Context,
|
||||
service,
|
||||
context=None,
|
||||
target='deployment',
|
||||
|
@ -199,12 +209,12 @@ def publish(ctx,
|
|||
|
||||
|
||||
@task
|
||||
def ci(ctx, service):
|
||||
def ci(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv ci')
|
||||
|
||||
|
||||
@task
|
||||
def setup(ctx, service=None):
|
||||
def setup(ctx: Context, service=None):
|
||||
services_to_setup = []
|
||||
|
||||
services_to_setup = [*ALL_SERVICES]
|
||||
|
@ -216,7 +226,7 @@ def setup(ctx, service=None):
|
|||
|
||||
|
||||
@task
|
||||
def install(ctx, service=None):
|
||||
def install(ctx: Context, service=None):
|
||||
services_to_setup = []
|
||||
|
||||
services_to_setup = [*ALL_SERVICES]
|
||||
|
@ -228,7 +238,7 @@ def install(ctx, service=None):
|
|||
|
||||
|
||||
@task
|
||||
def lock(ctx, service):
|
||||
def lock(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'poetry lock --no-update')
|
||||
|
||||
|
||||
|
@ -238,20 +248,20 @@ def start_cloud(ctx):
|
|||
|
||||
|
||||
@task
|
||||
def start_web(ctx, service):
|
||||
def start_web(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv start-web')
|
||||
|
||||
|
||||
@task
|
||||
def start_celery_worker(ctx, service):
|
||||
def start_celery_worker(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv start-worker')
|
||||
|
||||
|
||||
@task
|
||||
def start_celery_beat(ctx, service):
|
||||
def start_celery_beat(ctx: Context, service):
|
||||
_run_in_service(ctx, service, 'inv start-beat')
|
||||
|
||||
|
||||
@task
|
||||
def start_app(ctx, service, app):
|
||||
def start_app(ctx: Context, service, app):
|
||||
_run_in_service(ctx, service, f'inv start-{app}')
|
||||
|
|
Loading…
Reference in New Issue
Block a user