Skip to content

Commit d2853a9

Browse files
Drop use of passwords from everywhere, and refactor sharing-related code (#64)
Co-authored-by: Peyton Murray <[email protected]>
1 parent 210569c commit d2853a9

File tree

6 files changed

+140
-246
lines changed

6 files changed

+140
-246
lines changed

src/index.ts

Lines changed: 90 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,14 @@ import { INotebookContent } from '@jupyterlab/nbformat';
88
import { customSidebar } from './sidebar';
99
import { SharingService } from './sharing-service';
1010

11-
import {
12-
IShareDialogData,
13-
ShareDialog,
14-
createSuccessDialog,
15-
createErrorDialog
16-
} from './ui-components/share-dialog';
11+
import { createSuccessDialog, createErrorDialog } from './ui-components/share-dialog';
1712

1813
import { exportNotebookAsPDF } from './pdf';
1914
import { files } from './pages/files';
2015
import { Commands } from './commands';
2116
import { competitions } from './pages/competitions';
2217
import { notebookPlugin } from './pages/notebook';
23-
24-
function generatePassword(): string {
25-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
26-
const array = new Uint8Array(16);
27-
crypto.getRandomValues(array);
28-
let password = '';
29-
for (let i = 0; i < array.length; i++) {
30-
password += chars.charAt(array[i] % chars.length);
31-
}
32-
return password;
33-
}
18+
import { generateDefaultNotebookName } from './notebook-name';
3419

3520
/**
3621
* Get the current notebook panel
@@ -50,6 +35,84 @@ function getCurrentNotebook(
5035
return widget;
5136
}
5237

38+
const manuallySharing = new WeakSet<NotebookPanel>();
39+
40+
/**
41+
* Show a dialog with a shareable link for the notebook.
42+
* @param sharingService - The sharing service instance to use for generating the shareable link.
43+
* @param notebookContent - The content of the notebook to share, from which we extract the ID.
44+
*/
45+
async function showShareDialog(sharingService: SharingService, notebookContent: INotebookContent) {
46+
const id = (notebookContent.metadata.readableId || notebookContent.metadata.sharedId) as string;
47+
const shareableLink = sharingService.makeRetrieveURL(id).toString();
48+
49+
const dialogResult = await showDialog({
50+
title: '',
51+
body: ReactWidget.create(createSuccessDialog(shareableLink)),
52+
buttons: [Dialog.okButton({ label: 'Copy Link!' }), Dialog.cancelButton({ label: 'Close' })]
53+
});
54+
55+
if (dialogResult.button.label === 'Copy Link!') {
56+
try {
57+
await navigator.clipboard.writeText(shareableLink);
58+
} catch (err) {
59+
console.error('Failed to copy link:', err);
60+
}
61+
}
62+
}
63+
64+
/**
65+
* Notebook share/save handler. This function handles both sharing a new notebook and
66+
* updating an existing shared notebook.
67+
* @param notebookPanel - The notebook panel to handle sharing for.
68+
* @param sharingService - The sharing service instance to use for sharing operations.
69+
* @param manual - Whether this is a manual share operation triggered by the user, i.e., it is
70+
* true when the user clicks "Share Notebook" from the menu.
71+
*/
72+
async function handleNotebookSharing(
73+
notebookPanel: NotebookPanel,
74+
sharingService: SharingService,
75+
manual: boolean
76+
) {
77+
const notebookContent = notebookPanel.context.model.toJSON() as INotebookContent;
78+
79+
const sharedId = notebookContent.metadata?.sharedId as string | undefined;
80+
const defaultName = generateDefaultNotebookName();
81+
82+
try {
83+
if (sharedId) {
84+
console.log('Updating notebook:', sharedId);
85+
await sharingService.update(sharedId, notebookContent);
86+
87+
console.log('Notebook automatically synced to CKHub');
88+
} else {
89+
const shareResponse = await sharingService.share(notebookContent);
90+
91+
notebookContent.metadata = {
92+
...notebookContent.metadata,
93+
sharedId: shareResponse.notebook.id,
94+
readableId: shareResponse.notebook.readable_id,
95+
sharedName: defaultName,
96+
lastShared: new Date().toISOString()
97+
};
98+
99+
notebookPanel.context.model.fromJSON(notebookContent);
100+
await notebookPanel.context.save();
101+
}
102+
103+
if (manual) {
104+
await showShareDialog(sharingService, notebookContent);
105+
}
106+
} catch (error) {
107+
console.warn('Failed to sync notebook to CKHub:', error);
108+
await showDialog({
109+
title: manual ? 'Error Sharing Notebook' : 'Sync Failed',
110+
body: ReactWidget.create(createErrorDialog(error)),
111+
buttons: [Dialog.okButton()]
112+
});
113+
}
114+
}
115+
53116
/**
54117
* JUPYTEREVERYWHERE EXTENSION
55118
*/
@@ -70,108 +133,20 @@ const plugin: JupyterFrontEndPlugin<void> = {
70133
const apiUrl =
71134
PageConfig.getOption('sharing_service_api_url') || 'http://localhost:8080/api/v1';
72135

73-
const notebookPasswords = new Map();
74136
const sharingService = new SharingService(apiUrl);
75137

76-
async function handleNotebookSave(
77-
notebookPanel: NotebookPanel,
78-
isManualShare: boolean = false
79-
) {
80-
const notebookContent = notebookPanel.context.model.toJSON() as INotebookContent;
81-
82-
// Check if notebook has already been shared
83-
const isAlreadyShared =
84-
notebookContent.metadata &&
85-
typeof notebookContent.metadata === 'object' &&
86-
'sharedId' in notebookContent.metadata;
87-
88-
if (isAlreadyShared && !isManualShare) {
89-
try {
90-
const sharedId = notebookContent.metadata.sharedId as string;
91-
console.log('Updating notebook:', sharedId);
92-
93-
await sharingService.update(sharedId, notebookContent);
94-
95-
console.log('Notebook automatically synced to CKHub');
96-
} catch (error) {
97-
console.warn('Failed to sync notebook to CKHub:', error);
98-
await showDialog({
99-
title: 'Sync Failed',
100-
body: ReactWidget.create(createErrorDialog(error)),
101-
buttons: [Dialog.okButton()]
102-
});
103-
}
104-
return;
105-
}
106-
107-
if (!isAlreadyShared && !isManualShare) {
108-
// First save/share; displays a shareable link and shows a password in a dialog
109-
const password = generatePassword();
110-
const defaultName = `Notebook_${new Date().getFullYear()}-${(new Date().getMonth() + 1).toString().padStart(2, '0')}-${new Date().getDate().toString().padStart(2, '0')}`;
111-
112-
try {
113-
const shareResponse = await sharingService.share(notebookContent, password);
114-
115-
if (shareResponse && shareResponse.notebook) {
116-
notebookContent.metadata = {
117-
...notebookContent.metadata,
118-
sharedId: shareResponse.notebook.id,
119-
readableId: shareResponse.notebook.readable_id,
120-
sharedName: defaultName,
121-
isPasswordProtected: true,
122-
lastShared: new Date().toISOString()
123-
};
124-
125-
notebookPasswords.set(shareResponse.notebook.id, password);
126-
127-
notebookPanel.context.model.fromJSON(notebookContent);
128-
await notebookPanel.context.save();
129-
}
130-
} catch (error) {
131-
console.error('Failed to share notebook:', error);
132-
await showDialog({
133-
title: 'Error Sharing Notebook',
134-
body: ReactWidget.create(createErrorDialog(error)),
135-
buttons: [Dialog.okButton()]
136-
});
137-
}
138-
}
139-
140-
if (isManualShare) {
141-
// Manual share button pressed - show link and password
142-
const readableId = notebookContent.metadata.readableId as string;
143-
const sharedId = notebookContent.metadata.sharedId as string;
144-
const shareableLink = sharingService.makeRetrieveURL(readableId || sharedId).toString();
145-
146-
const dialogResult = await showDialog({
147-
title: '',
148-
body: ReactWidget.create(
149-
createSuccessDialog(shareableLink, notebookPasswords.get(sharedId))
150-
),
151-
buttons: [
152-
Dialog.okButton({ label: 'Copy Link!' }),
153-
Dialog.cancelButton({ label: 'Close' })
154-
]
155-
});
156-
157-
if (dialogResult.button.label === 'Copy Link!') {
158-
try {
159-
await navigator.clipboard.writeText(shareableLink);
160-
} catch (err) {
161-
console.error('Failed to copy link:', err);
162-
}
163-
}
164-
}
165-
}
166-
167138
/**
168139
* Hook into notebook saves using the saveState signal to handle CKHub sharing
169140
*/
170141
tracker.widgetAdded.connect((sender, widget) => {
171142
widget.context.saveState.connect(async (sender, saveState) => {
172143
// Only trigger when save is completed (not dirty and not saving)
173144
if (saveState === 'completed') {
174-
await handleNotebookSave(widget, false);
145+
if (manuallySharing.has(widget)) {
146+
// Skip auto-sync if it's a manual share.
147+
return;
148+
}
149+
await handleNotebookSharing(widget, sharingService, false);
175150
}
176151
});
177152
});
@@ -224,78 +199,14 @@ const plugin: JupyterFrontEndPlugin<void> = {
224199
return;
225200
}
226201

202+
// Mark this notebook as being shared manually (i.e., the user has
203+
// clicked the "Share Notebook" command).
204+
manuallySharing.add(notebookPanel);
205+
227206
// Save the notebook before we share it.
228207
await notebookPanel.context.save();
229208

230-
const notebookContent = notebookPanel.context.model.toJSON() as INotebookContent;
231-
232-
// Check if notebook has already been shared
233-
const notebookId = notebookContent?.metadata?.sharedId as string;
234-
235-
const isNewShare = !notebookId;
236-
237-
if (isNewShare) {
238-
// First time sharing - show dialog to get notebook name
239-
const result = await showDialog({
240-
title: 'Share Notebook',
241-
body: new ShareDialog(),
242-
buttons: [Dialog.cancelButton(), Dialog.okButton()]
243-
});
244-
245-
if (result.button.accept) {
246-
const { notebookName } = result.value as IShareDialogData;
247-
const password = generatePassword();
248-
249-
try {
250-
const shareResponse = await sharingService.share(notebookContent, password);
251-
252-
let shareableLink = '';
253-
if (shareResponse && shareResponse.notebook) {
254-
notebookContent.metadata = {
255-
...notebookContent.metadata,
256-
sharedId: shareResponse.notebook.id,
257-
readable_id: shareResponse.notebook.readable_id,
258-
sharedName: notebookName,
259-
isPasswordProtected: true
260-
};
261-
262-
notebookPanel.context.model.fromJSON(notebookContent);
263-
await notebookPanel.context.save();
264-
265-
const id = shareResponse.notebook.readable_id || shareResponse.notebook.id;
266-
shareableLink = sharingService.makeRetrieveURL(id).toString();
267-
notebookPasswords.set(shareResponse.notebook.id, password);
268-
}
269-
270-
if (shareableLink) {
271-
const dialogResult = await showDialog({
272-
title: '',
273-
body: ReactWidget.create(createSuccessDialog(shareableLink, password)),
274-
buttons: [
275-
Dialog.okButton({ label: 'Done' }),
276-
Dialog.cancelButton({ label: 'Copy Link' })
277-
]
278-
});
279-
280-
if (dialogResult.button.label === 'Copy Link') {
281-
try {
282-
await navigator.clipboard.writeText(shareableLink);
283-
} catch (err) {
284-
console.error('Failed to copy link:', err);
285-
}
286-
}
287-
}
288-
} catch (error) {
289-
await showDialog({
290-
title: 'Error',
291-
body: ReactWidget.create(createErrorDialog(error)),
292-
buttons: [Dialog.okButton()]
293-
});
294-
}
295-
}
296-
} else {
297-
await handleNotebookSave(notebookPanel, true);
298-
}
209+
await handleNotebookSharing(notebookPanel, sharingService, true);
299210
} catch (error) {
300211
console.error('Error in share command:', error);
301212
}

src/notebook-name.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Generates a default notebook name based on the current date and time.
3+
*
4+
* @returns A string representing the default notebook name, with
5+
* the format: "Notebook_YYYY-MM-DD_HH-MM-SS"
6+
*/
7+
export function generateDefaultNotebookName(): string {
8+
const now = new Date();
9+
10+
const pad = (n: number) => n.toString().padStart(2, '0');
11+
12+
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
13+
const time = `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
14+
15+
return `Notebook_${date}_${time}`;
16+
}

src/sharing-service.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export interface IShareResponse {
2020
notebook: {
2121
id: UUID;
2222
readable_id: string;
23-
password?: string; // Optional password from API
2423
};
2524
}
2625

@@ -251,19 +250,15 @@ export class SharingService {
251250
* Stores a notebook on the server and returns a share response with the notebook ID and metadata.
252251
*
253252
* @param notebook - The notebook content to be shared.
254-
* @param password - An optional password to allow editing the shared notebook later.
255253
* @returns A promise that resolves to the share response.
256254
* @throws {Error} If the notebook content is invalid or if the sharing request fails.
257255
*/
258-
async share(notebook: INotebookContent, password?: string): Promise<IShareResponse> {
256+
async share(notebook: INotebookContent): Promise<IShareResponse> {
259257
if (!validateNotebookContent(notebook)) {
260258
throw new Error('Invalid notebook content');
261259
}
262260

263261
const requestData: Record<string, any> = { notebook };
264-
if (password) {
265-
requestData.password = password;
266-
}
267262

268263
const endpoint = new URL('notebooks', this.api_url);
269264

@@ -291,7 +286,6 @@ export class SharingService {
291286
* Updates an existing shared notebook
292287
* @param id - Notebook ID
293288
* @param notebook - Updated notebook content
294-
* @param password - Password if notebook is protected
295289
* @returns API response with updated notebook details
296290
*/
297291
async update(id: string, notebook: INotebookContent): Promise<IShareResponse> {

0 commit comments

Comments
 (0)