From 0de6eff1048e378bb7034590b6750116e40443b9 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 10 Feb 2023 10:19:34 +0100 Subject: [PATCH 1/4] seems to need es2018 target to compile something about AsyncIterator --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 81139f54..7df12ea9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "rootDir": "src", "strict": true, "strictNullChecks": false, - "target": "es2017", + "target": "es2018", "types": [] }, "include": ["src/*"] From a6978773461e752028e11efe619e72d3b42367ad Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 10 Feb 2023 10:42:12 +0100 Subject: [PATCH 2/4] add checkbox for including the token in the URL show different warnings under JupyterHub and not --- src/index.ts | 152 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 117 insertions(+), 35 deletions(-) diff --git a/src/index.ts b/src/index.ts index 505880c9..3500bd78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin + JupyterFrontEndPlugin, } from '@jupyterlab/application'; import { Clipboard, Dialog, ICommandPalette, - showDialog + showDialog, } from '@jupyterlab/apputils'; import { IRetroShell } from '@retrolab/application'; @@ -36,7 +36,7 @@ const plugin: JupyterFrontEndPlugin = { id: 'jupyterlab-link-share:plugin', autoStart: true, optional: [ICommandPalette, IMainMenu, ITranslator, IRetroShell], - activate: ( + activate: async ( app: JupyterFrontEnd, palette: ICommandPalette | null, menu: IMainMenu | null, @@ -50,8 +50,8 @@ const plugin: JupyterFrontEndPlugin = { label: trans.__('Share Jupyter Server Link'), execute: async () => { let results: { token: string }[]; - const isRunningUnderJupyterhub = PageConfig.getOption('hubUser') !== ''; - if (isRunningUnderJupyterhub) { + const isRunningUnderJupyterHub = PageConfig.getOption('hubUser') !== ''; + if (isRunningUnderJupyterHub) { // We are running on a JupyterHub, so let's just use the token set in PageConfig. // Any extra servers running on the server will still need to use this token anyway, // as all traffic (including any to jupyter-server-proxy) needs this token. @@ -60,30 +60,44 @@ const plugin: JupyterFrontEndPlugin = { results = await requestAPI('servers'); } - const links = results.map(server => { + const links = results.map((server) => { + let url: URL; if (retroShell) { // On retrolab, take current URL and set ?token to it - const url = new URL(location.href); - url.searchParams.set('token', server.token); - return url.toString(); + url = new URL(location.href); } else { // On JupyterLab, let PageConfig.getUrl do its magic. // Handles workspaces, single document mode, etc - return URLExt.normalize( - `${PageConfig.getUrl({ - workspace: PageConfig.defaultWorkspace - })}?token=${server.token}` + url = new URL( + URLExt.normalize( + `${PageConfig.getUrl({ + workspace: PageConfig.defaultWorkspace, + })}` + ) ); } + let tokenURL = new URL(url.toString()); + if (server.token) { + // add token to URL + tokenURL.searchParams.set('token', server.token); + } + return { + noToken: url.toString(), + withToken: tokenURL.toString(), + }; }); + const dialogBody = document.createElement('div'); const entries = document.createElement('div'); - links.map(link => { + dialogBody.appendChild(entries); + links.map((link) => { const p = document.createElement('p'); const text: HTMLInputElement = document.createElement('input'); + text.dataset.noToken = link.noToken; + text.dataset.withToken = link.withToken; text.readOnly = true; - text.value = link; - text.addEventListener('click', e => { + text.value = link.noToken; + text.addEventListener('click', (e) => { (e.target as HTMLInputElement).select(); }); text.style.width = '100%'; @@ -93,55 +107,123 @@ const plugin: JupyterFrontEndPlugin = { // Warn users of the security implications of using this link // FIXME: There *must* be a better way to create HTML - const warning = document.createElement('div'); + const tokenWarning = document.createElement('div'); const warningHeader = document.createElement('h3'); warningHeader.innerText = trans.__('Security warning!'); - warning.appendChild(warningHeader); + tokenWarning.appendChild(warningHeader); + + const tokenMessages: Array = []; - const messages = [ + tokenMessages.push( 'Anyone with this link has full access to your notebook server, including all your files!', 'Please be careful who you share it with.' - ]; - if (isRunningUnderJupyterhub) { - messages.push( + ); + if (isRunningUnderJupyterHub) { + tokenMessages.push( + // You can restart the server to revoke the token in a JupyterHub + 'They will be able to access this server AS YOU.' + ); + tokenMessages.push( // You can restart the server to revoke the token in a JupyterHub 'To revoke access, go to File -> Hub Control Panel, and restart your server' ); } else { - messages.push( + tokenMessages.push( // Elsewhere, you *must* shut down your server - no way to revoke it 'Currently, there is no way to revoke access other than shutting down your server' ); } - messages.map(m => { - warning.appendChild(document.createTextNode(trans.__(m))); - warning.appendChild(document.createElement('br')); + + const noTokenMessage = document.createElement('div'); + const noTokenMessages: Array = []; + if (isRunningUnderJupyterHub) { + noTokenMessages.push( + 'Only users with `access:servers` permissions for this server will be able to use this link.' + ); + } else { + noTokenMessages.push( + 'Only authenticated users will be able to use this link.' + ); + } + + tokenMessages.map((m) => { + tokenWarning.appendChild(document.createTextNode(trans.__(m))); + tokenWarning.appendChild(document.createElement('br')); }); + noTokenMessages.map((m) => { + noTokenMessage.appendChild(document.createTextNode(trans.__(m))); + noTokenMessage.appendChild(document.createElement('br')); + }); + const messages = { + noToken: noTokenMessage, + withToken: tokenWarning, + }; + + const message = document.createElement('div'); + message.appendChild(messages.noToken); + + // whether there's any token to be used in URLs + // if none, no point in adding a checkbox + const hasToken = + results.filter( + (server) => server.token !== undefined && server.token !== '' + ).length > 0; + + let includeTokenCheckbox: HTMLInputElement | undefined = undefined; + if (hasToken) { + // add checkbox to include token _if_ there's a token to include + const includeTokenCheckbox = document.createElement('input'); + includeTokenCheckbox.type = 'checkbox'; + const tokenLabel = document.createElement('label'); + tokenLabel.appendChild(includeTokenCheckbox) + tokenLabel.appendChild(document.createTextNode(trans.__('Include token in URL'))); + dialogBody.appendChild(tokenLabel); + + // when checkbox changes, toggle URL and message + includeTokenCheckbox.addEventListener('change', (e) => { + const isChecked: boolean = (e.target as HTMLInputElement).checked; + const key = isChecked ? 'withToken' : 'noToken'; + + // add or remove the token to the URL inputs + const inputElements = entries.getElementsByTagName('input'); + [...inputElements].map((input) => { + input.value = input.dataset[key] as string; + }); + + // swap out the warning message + message.removeChild(message.children[0]); + message.appendChild(messages[key]); + }); + } - entries.appendChild(warning); + dialogBody.appendChild(message); const result = await showDialog({ title: trans.__('Share Jupyter Server Link'), - body: new Widget({ node: entries }), + body: new Widget({ node: dialogBody }), buttons: [ Dialog.cancelButton({ label: trans.__('Cancel') }), Dialog.okButton({ label: trans.__('Copy Link'), - caption: trans.__('Copy the link to the Jupyter Server') - }) - ] + caption: trans.__('Copy the link to the Jupyter Server'), + }), + ], }); if (result.button.accept) { - Clipboard.copyToSystem(links[0]); + const key = + includeTokenCheckbox && includeTokenCheckbox.checked + ? 'withToken' + : 'noToken'; + Clipboard.copyToSystem(links[0][key]); } - } + }, }); if (palette) { palette.addItem({ command: CommandIDs.share, - category: trans.__('Server') + category: trans.__('Server'), }); } @@ -154,7 +236,7 @@ const plugin: JupyterFrontEndPlugin = { // Add the command to the menu shareMenu.addItem({ command: CommandIDs.share }); } - } + }, }; const plugins: JupyterFrontEndPlugin[] = [plugin]; From 7c39b3ff409f7d225a8047294e48a947b70c1bce Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 15 Mar 2023 12:04:02 +0100 Subject: [PATCH 3/4] run lint --- src/index.ts | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3500bd78..ea3df168 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ import { JupyterFrontEnd, - JupyterFrontEndPlugin, + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { Clipboard, Dialog, ICommandPalette, - showDialog, + showDialog } from '@jupyterlab/apputils'; import { IRetroShell } from '@retrolab/application'; @@ -60,7 +60,7 @@ const plugin: JupyterFrontEndPlugin = { results = await requestAPI('servers'); } - const links = results.map((server) => { + const links = results.map(server => { let url: URL; if (retroShell) { // On retrolab, take current URL and set ?token to it @@ -71,33 +71,33 @@ const plugin: JupyterFrontEndPlugin = { url = new URL( URLExt.normalize( `${PageConfig.getUrl({ - workspace: PageConfig.defaultWorkspace, + workspace: PageConfig.defaultWorkspace })}` ) ); } - let tokenURL = new URL(url.toString()); + const tokenURL = new URL(url.toString()); if (server.token) { // add token to URL tokenURL.searchParams.set('token', server.token); } return { noToken: url.toString(), - withToken: tokenURL.toString(), + withToken: tokenURL.toString() }; }); const dialogBody = document.createElement('div'); const entries = document.createElement('div'); dialogBody.appendChild(entries); - links.map((link) => { + links.map(link => { const p = document.createElement('p'); const text: HTMLInputElement = document.createElement('input'); text.dataset.noToken = link.noToken; text.dataset.withToken = link.withToken; text.readOnly = true; text.value = link.noToken; - text.addEventListener('click', (e) => { + text.addEventListener('click', e => { (e.target as HTMLInputElement).select(); }); text.style.width = '100%'; @@ -147,17 +147,17 @@ const plugin: JupyterFrontEndPlugin = { ); } - tokenMessages.map((m) => { + tokenMessages.map(m => { tokenWarning.appendChild(document.createTextNode(trans.__(m))); tokenWarning.appendChild(document.createElement('br')); }); - noTokenMessages.map((m) => { + noTokenMessages.map(m => { noTokenMessage.appendChild(document.createTextNode(trans.__(m))); noTokenMessage.appendChild(document.createElement('br')); }); const messages = { noToken: noTokenMessage, - withToken: tokenWarning, + withToken: tokenWarning }; const message = document.createElement('div'); @@ -167,27 +167,29 @@ const plugin: JupyterFrontEndPlugin = { // if none, no point in adding a checkbox const hasToken = results.filter( - (server) => server.token !== undefined && server.token !== '' + server => server.token !== undefined && server.token !== '' ).length > 0; - let includeTokenCheckbox: HTMLInputElement | undefined = undefined; + const includeTokenCheckbox: HTMLInputElement | undefined = undefined; if (hasToken) { // add checkbox to include token _if_ there's a token to include const includeTokenCheckbox = document.createElement('input'); includeTokenCheckbox.type = 'checkbox'; const tokenLabel = document.createElement('label'); - tokenLabel.appendChild(includeTokenCheckbox) - tokenLabel.appendChild(document.createTextNode(trans.__('Include token in URL'))); + tokenLabel.appendChild(includeTokenCheckbox); + tokenLabel.appendChild( + document.createTextNode(trans.__('Include token in URL')) + ); dialogBody.appendChild(tokenLabel); // when checkbox changes, toggle URL and message - includeTokenCheckbox.addEventListener('change', (e) => { + includeTokenCheckbox.addEventListener('change', e => { const isChecked: boolean = (e.target as HTMLInputElement).checked; const key = isChecked ? 'withToken' : 'noToken'; // add or remove the token to the URL inputs const inputElements = entries.getElementsByTagName('input'); - [...inputElements].map((input) => { + [...inputElements].map(input => { input.value = input.dataset[key] as string; }); @@ -206,9 +208,9 @@ const plugin: JupyterFrontEndPlugin = { Dialog.cancelButton({ label: trans.__('Cancel') }), Dialog.okButton({ label: trans.__('Copy Link'), - caption: trans.__('Copy the link to the Jupyter Server'), - }), - ], + caption: trans.__('Copy the link to the Jupyter Server') + }) + ] }); if (result.button.accept) { const key = @@ -217,13 +219,13 @@ const plugin: JupyterFrontEndPlugin = { : 'noToken'; Clipboard.copyToSystem(links[0][key]); } - }, + } }); if (palette) { palette.addItem({ command: CommandIDs.share, - category: trans.__('Server'), + category: trans.__('Server') }); } @@ -236,7 +238,7 @@ const plugin: JupyterFrontEndPlugin = { // Add the command to the menu shareMenu.addItem({ command: CommandIDs.share }); } - }, + } }; const plugins: JupyterFrontEndPlugin[] = [plugin]; From e9d632ab62d2360cda410ba4ad6e9e55e0b31649 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 17 Mar 2023 10:41:04 +0100 Subject: [PATCH 4/4] fix inclusion of token in copy --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index ea3df168..c1059ac6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,10 +170,10 @@ const plugin: JupyterFrontEndPlugin = { server => server.token !== undefined && server.token !== '' ).length > 0; - const includeTokenCheckbox: HTMLInputElement | undefined = undefined; + let includeTokenCheckbox: HTMLInputElement | undefined = undefined; if (hasToken) { // add checkbox to include token _if_ there's a token to include - const includeTokenCheckbox = document.createElement('input'); + includeTokenCheckbox = document.createElement('input'); includeTokenCheckbox.type = 'checkbox'; const tokenLabel = document.createElement('label'); tokenLabel.appendChild(includeTokenCheckbox);