@datalayer/core 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -13
- package/lib/client/auth/AuthenticationManager.d.ts +95 -0
- package/lib/client/auth/AuthenticationManager.js +214 -0
- package/lib/client/auth/index.d.ts +8 -0
- package/lib/client/auth/index.js +17 -0
- package/lib/client/auth/storage.d.ts +154 -0
- package/lib/client/auth/storage.js +447 -0
- package/lib/client/auth/strategies.d.ts +54 -0
- package/lib/client/auth/strategies.js +238 -0
- package/lib/client/auth/types.d.ts +151 -0
- package/lib/{examples/index.js → client/auth/types.js} +4 -2
- package/lib/client/base.d.ts +3 -0
- package/lib/client/base.js +9 -0
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +2 -0
- package/lib/components/auth/Login.d.ts +40 -0
- package/lib/components/auth/Login.js +173 -0
- package/lib/components/auth/Login.stories.d.ts +54 -0
- package/lib/components/auth/Login.stories.js +104 -0
- package/lib/components/auth/LoginToken.d.ts +16 -0
- package/lib/components/auth/LoginToken.js +63 -0
- package/lib/components/auth/index.d.ts +5 -0
- package/lib/components/auth/index.js +16 -0
- package/lib/components/avatars/BoringAvatar.d.ts +6 -15
- package/lib/components/avatars/BoringAvatar.js +30 -34
- package/lib/components/avatars/BoringAvatar.stories.d.ts +7 -16
- package/lib/components/avatars/UserProfileAvatar.d.ts +1 -6
- package/lib/components/avatars/UserProfileAvatar.js +3 -8
- package/lib/components/buttons/DownloadCSVButton.d.ts +2 -7
- package/lib/components/buttons/DownloadCSVButton.js +1 -5
- package/lib/components/buttons/DownloadJsonButton.d.ts +3 -10
- package/lib/components/buttons/DownloadJsonButton.js +1 -7
- package/lib/components/buttons/UploadButton.d.ts +1 -4
- package/lib/components/buttons/UploadButton.js +3 -7
- package/lib/components/chat/ChatComponent.js +4 -0
- package/lib/components/chat/display/ReasoningPart.js +4 -0
- package/lib/components/chat/display/ToolPart.js +4 -0
- package/lib/components/chat/display/index.js +4 -0
- package/lib/components/chat/handler.js +4 -0
- package/lib/components/chat/index.js +4 -0
- package/lib/components/display/CenteredSpinner.d.ts +1 -4
- package/lib/components/display/CenteredSpinner.js +1 -5
- package/lib/components/display/HorizontalCenter.d.ts +1 -4
- package/lib/components/display/HorizontalCenter.js +1 -5
- package/lib/components/flashes/FlashClosable.d.ts +1 -4
- package/lib/components/flashes/FlashClosable.js +1 -5
- package/lib/components/flashes/FlashDisclaimer.js +1 -1
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/notebooks/JupyterNotebook.d.ts +1 -6
- package/lib/components/notebooks/JupyterNotebook.js +1 -5
- package/lib/components/snapshots/RuntimeSnapshotMenu.d.ts +1 -4
- package/lib/components/snapshots/RuntimeSnapshotMenu.js +1 -5
- package/lib/config/Configuration.js +1 -1
- package/lib/examples/CellExample.js +11 -47
- package/lib/examples/lexical-theme.css +436 -0
- package/lib/examples/notebooks/Matplotlib.ipynb.json +1 -1
- package/lib/examples/notebooks/NotebookExample1.ipynb.json +1 -1
- package/lib/hooks/useAIJupyterChat.js +4 -0
- package/lib/hooks/useBackdrop.d.ts +4 -4
- package/lib/hooks/useBackdrop.js +5 -9
- package/lib/hooks/useCache.d.ts +5 -1
- package/lib/hooks/useCache.js +27 -14
- package/lib/hooks/useMobile.js +4 -0
- package/lib/hooks/useScreenshot.d.ts +3 -5
- package/lib/hooks/useScreenshot.js +1 -8
- package/lib/models/Outbound.d.ts +2 -0
- package/lib/models/Outbound.js +3 -1
- package/lib/state/substates/CoreState.js +1 -1
- package/lib/state/substates/IAMState.js +15 -6
- package/lib/tools/adapters/agui/AgUIToolAdapter.d.ts +75 -0
- package/lib/tools/adapters/agui/AgUIToolAdapter.js +244 -0
- package/lib/tools/adapters/agui/index.d.ts +10 -0
- package/lib/tools/adapters/agui/index.js +19 -0
- package/lib/tools/adapters/agui/lexicalHooks.d.ts +27 -0
- package/lib/tools/adapters/agui/lexicalHooks.js +64 -0
- package/lib/tools/adapters/agui/notebookHooks.d.ts +27 -0
- package/lib/tools/adapters/agui/notebookHooks.js +61 -0
- package/lib/tools/index.d.ts +6 -0
- package/lib/tools/index.js +18 -0
- package/lib/types.js +2 -3
- package/lib/utils/cli/index.d.ts +4 -0
- package/lib/utils/cli/index.js +13 -0
- package/lib/utils/cli/query.d.ts +6 -0
- package/lib/utils/cli/query.js +26 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/package.json +50 -7
- package/lib/examples/ChatExample.d.ts +0 -8
- package/lib/examples/ChatExample.js +0 -51
- package/lib/examples/DatalayerNotebookExample.d.ts +0 -16
- package/lib/examples/DatalayerNotebookExample.js +0 -75
- package/lib/examples/NativeNavigationExample.d.ts +0 -8
- package/lib/examples/NativeNavigationExample.js +0 -97
- package/lib/examples/NotebookMutationsKernel.d.ts +0 -2
- package/lib/examples/NotebookMutationsKernel.js +0 -115
- package/lib/examples/NotebookMutationsServiceManager.d.ts +0 -2
- package/lib/examples/NotebookMutationsServiceManager.js +0 -107
- package/lib/examples/ReactRouterExample.d.ts +0 -6
- package/lib/examples/ReactRouterExample.js +0 -175
- package/lib/examples/example-selector.d.ts +0 -22
- package/lib/examples/example-selector.js +0 -46
- package/lib/examples/index.d.ts +0 -2
- package/lib/examples/main.d.ts +0 -1
- package/lib/examples/main.js +0 -153
- package/lib/examples/notebooks/OutputIPyWidgetsExample.d.ts +0 -145
- package/lib/examples/notebooks/OutputIPyWidgetsExample.js +0 -153
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/*
|
|
6
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
7
|
+
*
|
|
8
|
+
* Datalayer License
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Authentication strategy implementations
|
|
12
|
+
*/
|
|
13
|
+
import * as authentication from '../../api/iam/authentication';
|
|
14
|
+
import * as profile from '../../api/iam/profile';
|
|
15
|
+
import { UserDTO } from '../../models/UserDTO';
|
|
16
|
+
/**
|
|
17
|
+
* Base authentication strategy with common functionality
|
|
18
|
+
*/
|
|
19
|
+
class BaseAuthStrategy {
|
|
20
|
+
iamRunUrl;
|
|
21
|
+
storage;
|
|
22
|
+
constructor(iamRunUrl, storage) {
|
|
23
|
+
this.iamRunUrl = iamRunUrl;
|
|
24
|
+
this.storage = storage;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate a token by calling whoami
|
|
28
|
+
*/
|
|
29
|
+
async validateToken(token) {
|
|
30
|
+
const response = await profile.whoami(token, this.iamRunUrl);
|
|
31
|
+
if (!response || !response.profile) {
|
|
32
|
+
throw new Error('Invalid response from profile API');
|
|
33
|
+
}
|
|
34
|
+
const userData = {
|
|
35
|
+
id: response.profile.id,
|
|
36
|
+
uid: response.profile.uid,
|
|
37
|
+
handle_s: response.profile.handle_s || response.profile.handle,
|
|
38
|
+
email_s: response.profile.email_s || response.profile.email,
|
|
39
|
+
first_name_t: response.profile.first_name_t || response.profile.first_name || '',
|
|
40
|
+
last_name_t: response.profile.last_name_t || response.profile.last_name || '',
|
|
41
|
+
avatar_url_s: response.profile.avatar_url_s || response.profile.avatar_url,
|
|
42
|
+
};
|
|
43
|
+
return new UserDTO(userData, undefined);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Token-based authentication strategy
|
|
48
|
+
* Authenticates using an existing token
|
|
49
|
+
*/
|
|
50
|
+
export class TokenAuthStrategy extends BaseAuthStrategy {
|
|
51
|
+
canHandle(options) {
|
|
52
|
+
return !!options.token;
|
|
53
|
+
}
|
|
54
|
+
async authenticate(options) {
|
|
55
|
+
if (!options.token) {
|
|
56
|
+
throw new Error('Token is required for token authentication');
|
|
57
|
+
}
|
|
58
|
+
// Validate the token
|
|
59
|
+
const user = await this.validateToken(options.token);
|
|
60
|
+
// Store the token if requested
|
|
61
|
+
if (!options.noStore && this.storage) {
|
|
62
|
+
if (this.storage.setToken) {
|
|
63
|
+
await this.storage.setToken(options.token);
|
|
64
|
+
}
|
|
65
|
+
if (this.storage.setUser) {
|
|
66
|
+
await this.storage.setUser(user);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { user, token: options.token };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Credentials-based authentication strategy
|
|
74
|
+
* Authenticates using handle and password
|
|
75
|
+
*/
|
|
76
|
+
export class CredentialsAuthStrategy extends BaseAuthStrategy {
|
|
77
|
+
canHandle(options) {
|
|
78
|
+
return !!options.handle && !!options.password;
|
|
79
|
+
}
|
|
80
|
+
async authenticate(options) {
|
|
81
|
+
if (!options.handle || !options.password) {
|
|
82
|
+
throw new Error('Handle and password are required for credentials authentication');
|
|
83
|
+
}
|
|
84
|
+
// Call the login API
|
|
85
|
+
const response = await authentication.login({ handle: options.handle, password: options.password }, this.iamRunUrl);
|
|
86
|
+
if (!response || !response.success || !response.token) {
|
|
87
|
+
throw new Error(response?.message || 'Login failed');
|
|
88
|
+
}
|
|
89
|
+
const token = response.token;
|
|
90
|
+
// Get user profile
|
|
91
|
+
const user = await this.validateToken(token);
|
|
92
|
+
// Store the token if requested
|
|
93
|
+
if (!options.noStore && this.storage) {
|
|
94
|
+
this.storage.setToken?.(token);
|
|
95
|
+
this.storage.setUser?.(user);
|
|
96
|
+
}
|
|
97
|
+
return { user, token };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Storage-based authentication strategy
|
|
102
|
+
* Authenticates using a token from storage
|
|
103
|
+
*/
|
|
104
|
+
export class StorageAuthStrategy extends BaseAuthStrategy {
|
|
105
|
+
canHandle(options) {
|
|
106
|
+
// Can handle if:
|
|
107
|
+
// 1. Storage is available
|
|
108
|
+
// 2. No other auth method is provided (this is the fallback strategy)
|
|
109
|
+
if (!this.storage || !this.storage.isAvailable()) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// Return true if no explicit auth method provided (empty options or noStore only)
|
|
113
|
+
// This allows the authenticate() method to check storage async
|
|
114
|
+
const hasExplicitAuthMethod = !!options.token ||
|
|
115
|
+
!!options.handle ||
|
|
116
|
+
!!options.password ||
|
|
117
|
+
!!options.useBrowser;
|
|
118
|
+
return !hasExplicitAuthMethod;
|
|
119
|
+
}
|
|
120
|
+
async authenticate(options) {
|
|
121
|
+
if (!this.storage) {
|
|
122
|
+
throw new Error('Storage is required for storage-based authentication');
|
|
123
|
+
}
|
|
124
|
+
// Try async getTokenAsync first (for VS Code async keytar support)
|
|
125
|
+
let token = null;
|
|
126
|
+
if ('getTokenAsync' in this.storage &&
|
|
127
|
+
typeof this.storage.getTokenAsync === 'function') {
|
|
128
|
+
token = await this.storage.getTokenAsync();
|
|
129
|
+
}
|
|
130
|
+
else if (this.storage.getToken) {
|
|
131
|
+
token = this.storage.getToken();
|
|
132
|
+
}
|
|
133
|
+
if (!token) {
|
|
134
|
+
throw new Error('No token found in storage');
|
|
135
|
+
}
|
|
136
|
+
// Validate the token
|
|
137
|
+
const user = await this.validateToken(token);
|
|
138
|
+
return { user, token };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Browser OAuth strategy
|
|
143
|
+
* Authenticates using browser-based OAuth flow with GitHub or LinkedIn
|
|
144
|
+
*/
|
|
145
|
+
export class BrowserOAuthStrategy extends BaseAuthStrategy {
|
|
146
|
+
canHandle(options) {
|
|
147
|
+
return !!options.useBrowser;
|
|
148
|
+
}
|
|
149
|
+
async authenticate(options) {
|
|
150
|
+
// Import OAuth2 API
|
|
151
|
+
const { getOAuth2AuthzUrl } = await import('../../api/iam/oauth2');
|
|
152
|
+
// Default to GitHub provider
|
|
153
|
+
const provider = options.oauthProvider || 'github';
|
|
154
|
+
if (provider !== 'github' && provider !== 'linkedin') {
|
|
155
|
+
throw new Error(`Unsupported OAuth provider: ${provider}. Use 'github' or 'linkedin'.`);
|
|
156
|
+
}
|
|
157
|
+
// Generate callback URI
|
|
158
|
+
const callbackUri = options.callbackUri || `${window.location.origin}/auth/callback`;
|
|
159
|
+
try {
|
|
160
|
+
// Step 1: Get OAuth authorization URL
|
|
161
|
+
const authzResponse = await getOAuth2AuthzUrl(provider, callbackUri, this.iamRunUrl, options.nonce);
|
|
162
|
+
// Step 2: Open OAuth URL in popup or redirect
|
|
163
|
+
if (options.usePopup !== false) {
|
|
164
|
+
// Use popup window
|
|
165
|
+
const popup = window.open(authzResponse.loginURL, 'oauth-login', 'width=600,height=700,left=100,top=100');
|
|
166
|
+
if (!popup) {
|
|
167
|
+
throw new Error('Failed to open OAuth popup. Please allow popups for this site.');
|
|
168
|
+
}
|
|
169
|
+
// Wait for callback - Datalayer redirects to our callback with ?user=<json>&token=<token>
|
|
170
|
+
const result = await this.waitForOAuthCallback(popup, callbackUri);
|
|
171
|
+
// Extract token and user from callback result
|
|
172
|
+
const token = result.token;
|
|
173
|
+
const user = result.user;
|
|
174
|
+
if (!token) {
|
|
175
|
+
throw new Error('No token received from OAuth callback');
|
|
176
|
+
}
|
|
177
|
+
if (!user) {
|
|
178
|
+
throw new Error('No user data received from OAuth callback');
|
|
179
|
+
}
|
|
180
|
+
// Store token if requested
|
|
181
|
+
if (!options.noStore && this.storage) {
|
|
182
|
+
this.storage.setToken?.(token);
|
|
183
|
+
this.storage.setUser?.(user);
|
|
184
|
+
}
|
|
185
|
+
return { user, token };
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// Redirect to OAuth URL
|
|
189
|
+
window.location.href = authzResponse.loginURL;
|
|
190
|
+
// Return a pending promise (page will redirect)
|
|
191
|
+
return new Promise(() => {
|
|
192
|
+
// This promise never resolves because we redirect
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
throw new Error(`OAuth authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Wait for OAuth callback in popup window
|
|
202
|
+
* Expects: { user, token, error } from the Datalayer OAuth callback
|
|
203
|
+
*/
|
|
204
|
+
async waitForOAuthCallback(popup, callbackUri) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
const timeout = setTimeout(() => {
|
|
207
|
+
popup.close();
|
|
208
|
+
reject(new Error('OAuth authentication timed out'));
|
|
209
|
+
}, 5 * 60 * 1000); // 5 minute timeout
|
|
210
|
+
// Listen for message from popup
|
|
211
|
+
const handleMessage = (event) => {
|
|
212
|
+
// Verify origin
|
|
213
|
+
if (event.origin !== new URL(callbackUri).origin) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
window.removeEventListener('message', handleMessage);
|
|
218
|
+
popup.close();
|
|
219
|
+
if (event.data.error) {
|
|
220
|
+
reject(new Error(`OAuth error: ${event.data.error}`));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
resolve(event.data);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
window.addEventListener('message', handleMessage);
|
|
227
|
+
// Check if popup was closed manually
|
|
228
|
+
const checkClosed = setInterval(() => {
|
|
229
|
+
if (popup.closed) {
|
|
230
|
+
clearInterval(checkClosed);
|
|
231
|
+
clearTimeout(timeout);
|
|
232
|
+
window.removeEventListener('message', handleMessage);
|
|
233
|
+
reject(new Error('OAuth popup was closed'));
|
|
234
|
+
}
|
|
235
|
+
}, 1000);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication types and interfaces for the Datalayer SDK
|
|
3
|
+
*/
|
|
4
|
+
import { UserDTO } from '../../models/UserDTO';
|
|
5
|
+
/**
|
|
6
|
+
* Authentication credentials for login
|
|
7
|
+
*/
|
|
8
|
+
export interface AuthCredentials {
|
|
9
|
+
handle: string;
|
|
10
|
+
password: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Authentication options for login flow
|
|
14
|
+
*/
|
|
15
|
+
export interface AuthOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Existing authentication token
|
|
18
|
+
*/
|
|
19
|
+
token?: string;
|
|
20
|
+
/**
|
|
21
|
+
* User handle for credentials-based auth
|
|
22
|
+
*/
|
|
23
|
+
handle?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Password for credentials-based auth
|
|
26
|
+
*/
|
|
27
|
+
password?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Use browser-based OAuth flow
|
|
30
|
+
*/
|
|
31
|
+
useBrowser?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* OAuth provider (github or linkedin)
|
|
34
|
+
*/
|
|
35
|
+
oauthProvider?: 'github' | 'linkedin';
|
|
36
|
+
/**
|
|
37
|
+
* Callback URI for OAuth redirect
|
|
38
|
+
*/
|
|
39
|
+
callbackUri?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Nonce for OAuth state parameter
|
|
42
|
+
*/
|
|
43
|
+
nonce?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Use popup window for OAuth (default: true). If false, redirects the current page.
|
|
46
|
+
*/
|
|
47
|
+
usePopup?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Don't store the token after authentication
|
|
50
|
+
*/
|
|
51
|
+
noStore?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Authentication result containing user and token
|
|
55
|
+
*/
|
|
56
|
+
export interface AuthResult {
|
|
57
|
+
user: UserDTO;
|
|
58
|
+
token: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Token storage backend interface
|
|
62
|
+
*/
|
|
63
|
+
export interface TokenStorage {
|
|
64
|
+
/**
|
|
65
|
+
* Get token from storage
|
|
66
|
+
*/
|
|
67
|
+
get(key: string): string | null;
|
|
68
|
+
/**
|
|
69
|
+
* Set token in storage
|
|
70
|
+
*/
|
|
71
|
+
set(key: string, value: string): void;
|
|
72
|
+
/**
|
|
73
|
+
* Delete token from storage
|
|
74
|
+
*/
|
|
75
|
+
delete(key: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Check if storage is available
|
|
78
|
+
*/
|
|
79
|
+
isAvailable(): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Get stored authentication token (convenience method)
|
|
82
|
+
*/
|
|
83
|
+
getToken?(): string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Store authentication token (convenience method)
|
|
86
|
+
* May be async to support keyring storage
|
|
87
|
+
*/
|
|
88
|
+
setToken?(token: string): void | Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Delete authentication token (convenience method)
|
|
91
|
+
* May be async to support keyring storage
|
|
92
|
+
*/
|
|
93
|
+
deleteToken?(): void | Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Store user data (optional)
|
|
96
|
+
* May be async to support keyring storage
|
|
97
|
+
*/
|
|
98
|
+
setUser?(user: any): void | Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Clear all authentication data (optional)
|
|
101
|
+
* May be async to support keyring storage
|
|
102
|
+
*/
|
|
103
|
+
clear?(): void | Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Authentication strategy interface
|
|
107
|
+
*/
|
|
108
|
+
export interface AuthStrategy {
|
|
109
|
+
/**
|
|
110
|
+
* Authenticate using this strategy
|
|
111
|
+
*/
|
|
112
|
+
authenticate(options: AuthOptions): Promise<AuthResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Check if this strategy can handle the given options
|
|
115
|
+
*/
|
|
116
|
+
canHandle(options: AuthOptions): boolean;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Browser OAuth configuration
|
|
120
|
+
*/
|
|
121
|
+
export interface BrowserOAuthConfig {
|
|
122
|
+
/**
|
|
123
|
+
* OAuth provider name (e.g., 'github', 'linkedin')
|
|
124
|
+
*/
|
|
125
|
+
provider: string;
|
|
126
|
+
/**
|
|
127
|
+
* Callback URL for OAuth redirect
|
|
128
|
+
*/
|
|
129
|
+
callbackUrl: string;
|
|
130
|
+
/**
|
|
131
|
+
* IAM server URL
|
|
132
|
+
*/
|
|
133
|
+
iamUrl: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Token validation result
|
|
137
|
+
*/
|
|
138
|
+
export interface TokenValidationResult {
|
|
139
|
+
/**
|
|
140
|
+
* Whether the token is valid
|
|
141
|
+
*/
|
|
142
|
+
valid: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* User associated with the token (if valid)
|
|
145
|
+
*/
|
|
146
|
+
user?: UserDTO;
|
|
147
|
+
/**
|
|
148
|
+
* Error message (if invalid)
|
|
149
|
+
*/
|
|
150
|
+
error?: string;
|
|
151
|
+
}
|
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Authentication types and interfaces for the Datalayer SDK
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/lib/client/base.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EnvironmentDTO } from '../models/EnvironmentDTO';
|
|
2
|
+
import { AuthenticationManager } from './auth';
|
|
2
3
|
/** Handlers for SDK method lifecycle events. */
|
|
3
4
|
export interface SDKHandlers {
|
|
4
5
|
/** Called before any SDK method execution */
|
|
@@ -35,6 +36,8 @@ export declare class DatalayerClientBase {
|
|
|
35
36
|
readonly environments: EnvironmentDTO[];
|
|
36
37
|
/** Method lifecycle handlers */
|
|
37
38
|
readonly handlers?: SDKHandlers;
|
|
39
|
+
/** Authentication manager */
|
|
40
|
+
readonly auth: AuthenticationManager;
|
|
38
41
|
/**
|
|
39
42
|
* Create a DatalayerClient base instance.
|
|
40
43
|
* @param config - Client configuration options
|
package/lib/client/base.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module client/base
|
|
8
8
|
*/
|
|
9
9
|
import { DEFAULT_SERVICE_URLS } from '../api/constants';
|
|
10
|
+
import { AuthenticationManager } from './auth';
|
|
10
11
|
/** Base Client class providing core configuration and token management. */
|
|
11
12
|
export class DatalayerClientBase {
|
|
12
13
|
/** URL for IAM service */
|
|
@@ -21,6 +22,8 @@ export class DatalayerClientBase {
|
|
|
21
22
|
environments = [];
|
|
22
23
|
/** Method lifecycle handlers */
|
|
23
24
|
handlers;
|
|
25
|
+
/** Authentication manager */
|
|
26
|
+
auth;
|
|
24
27
|
/**
|
|
25
28
|
* Create a DatalayerClient base instance.
|
|
26
29
|
* @param config - Client configuration options
|
|
@@ -32,6 +35,12 @@ export class DatalayerClientBase {
|
|
|
32
35
|
this.spacerRunUrl = config.spacerRunUrl || DEFAULT_SERVICE_URLS.SPACER;
|
|
33
36
|
this.token = config.token;
|
|
34
37
|
this.handlers = config.handlers;
|
|
38
|
+
// Initialize authentication manager
|
|
39
|
+
this.auth = new AuthenticationManager(this.iamRunUrl);
|
|
40
|
+
// If a token was provided, store it in the auth manager
|
|
41
|
+
if (this.token) {
|
|
42
|
+
this.auth.storeToken(this.token);
|
|
43
|
+
}
|
|
35
44
|
}
|
|
36
45
|
/**
|
|
37
46
|
* Get the current configuration including service URLs and token.
|
package/lib/client/index.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export { HealthCheck } from '../models/HealthCheck';
|
|
|
68
68
|
export type { HealthCheckJSON } from '../models/HealthCheck';
|
|
69
69
|
export { ItemTypes } from './constants';
|
|
70
70
|
export type { ItemType } from './constants';
|
|
71
|
+
export * from './auth';
|
|
71
72
|
export interface DatalayerClient {
|
|
72
73
|
getToken(): string | undefined;
|
|
73
74
|
setToken(token: string): Promise<void>;
|
package/lib/client/index.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface ILoginProps {
|
|
2
|
+
/**
|
|
3
|
+
* Page heading
|
|
4
|
+
*/
|
|
5
|
+
heading?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Home page route
|
|
8
|
+
*/
|
|
9
|
+
homeRoute: string;
|
|
10
|
+
/**
|
|
11
|
+
* Login page route
|
|
12
|
+
*/
|
|
13
|
+
loginRoute?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Join page route
|
|
16
|
+
*/
|
|
17
|
+
joinRoute?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Join confirmation route
|
|
20
|
+
*/
|
|
21
|
+
joinConfirmRoute?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Password route
|
|
24
|
+
*/
|
|
25
|
+
passwordRoute?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Show Email Login form
|
|
28
|
+
*/
|
|
29
|
+
showEmailLogin?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Show GitHub Login button
|
|
32
|
+
*/
|
|
33
|
+
showGitHubLogin?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Show token Login buttons
|
|
36
|
+
*/
|
|
37
|
+
showTokenLogin?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare const Login: (props: ILoginProps) => JSX.Element;
|
|
40
|
+
export default Login;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/*
|
|
7
|
+
* Copyright (c) 2021-2024 Datalayer, Inc.
|
|
8
|
+
*
|
|
9
|
+
* Datalayer License
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
13
|
+
import { EyeIcon, EyeClosedIcon, MarkGithubIcon } from '@primer/octicons-react';
|
|
14
|
+
import { Button, FormControl, Heading, Link, PageLayout, TextInput, } from '@primer/react';
|
|
15
|
+
import { Box } from '@datalayer/primer-addons';
|
|
16
|
+
import { asUser, IAMProvidersSpecs } from '../../models';
|
|
17
|
+
import { useIAMStore } from '../../state';
|
|
18
|
+
import { CenteredSpinner } from '../display';
|
|
19
|
+
import { isInsideJupyterLab, validateLength } from '../../utils';
|
|
20
|
+
import { useNavigate, useCache, useToast, useIAM } from '../../hooks';
|
|
21
|
+
import { LoginToken } from './LoginToken';
|
|
22
|
+
export const Login = (props) => {
|
|
23
|
+
const { heading, homeRoute, loginRoute, showEmailLogin = true, showGitHubLogin = true, showTokenLogin = true, } = props;
|
|
24
|
+
const { useLogin, useOAuth2AuthorizationURL } = useCache({ loginRoute });
|
|
25
|
+
const loginMutation = useLogin();
|
|
26
|
+
const getOAuth2URLMutation = useOAuth2AuthorizationURL();
|
|
27
|
+
const { loginAndNavigate, setLogin } = useIAM();
|
|
28
|
+
const { externalToken, iamProvidersAuthorizationURL, addIAMProviderAuthorizationURL, iamRunUrl, logout, checkIAMToken, } = useIAMStore();
|
|
29
|
+
const { enqueueToast } = useToast();
|
|
30
|
+
const navigate = useNavigate();
|
|
31
|
+
const [loading, setLoading] = useState(false);
|
|
32
|
+
const [loadingWithToken, setLoadingWithToken] = useState(-1);
|
|
33
|
+
const [formValues, setFormValues] = useState({
|
|
34
|
+
handle: undefined,
|
|
35
|
+
password: undefined,
|
|
36
|
+
});
|
|
37
|
+
const [validationResult, setValidationResult] = useState({
|
|
38
|
+
handle: undefined,
|
|
39
|
+
password: undefined,
|
|
40
|
+
});
|
|
41
|
+
const [passwordVisibility, setPasswordVisibility] = useState(false);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const initIAMProvider = (iamProvider) => {
|
|
44
|
+
const callbackURI = isInsideJupyterLab()
|
|
45
|
+
? URLExt.join(PageConfig.getBaseUrl(), iamProvider.oauth2CallbackServerRoute)
|
|
46
|
+
: location.protocol +
|
|
47
|
+
'//' +
|
|
48
|
+
location.hostname +
|
|
49
|
+
':' +
|
|
50
|
+
location.port +
|
|
51
|
+
iamProvider.oauth2CallbackUIRoute;
|
|
52
|
+
const queryArgs = {
|
|
53
|
+
provider: iamProvider.name,
|
|
54
|
+
callback_uri: callbackURI,
|
|
55
|
+
};
|
|
56
|
+
/*
|
|
57
|
+
const xsrfTokenMatch = document.cookie.match('\\b_xsrf=([^;]*)\\b');
|
|
58
|
+
if (xsrfTokenMatch) {
|
|
59
|
+
queryArgs['xsrf'] = xsrfTokenMatch[1];
|
|
60
|
+
}
|
|
61
|
+
*/
|
|
62
|
+
getOAuth2URLMutation.mutate(queryArgs, {
|
|
63
|
+
onSuccess: authUrl => {
|
|
64
|
+
if (authUrl) {
|
|
65
|
+
addIAMProviderAuthorizationURL(iamProvider.name, authUrl);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error(`Failed to get the Login URL from Datalayer IAM for provider ${iamProvider.name}.`);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
if (!iamProvidersAuthorizationURL[IAMProvidersSpecs.GitHub.name]) {
|
|
74
|
+
initIAMProvider(IAMProvidersSpecs.GitHub);
|
|
75
|
+
}
|
|
76
|
+
if (!iamProvidersAuthorizationURL[IAMProvidersSpecs.LinkedIn.name]) {
|
|
77
|
+
initIAMProvider(IAMProvidersSpecs.LinkedIn);
|
|
78
|
+
}
|
|
79
|
+
}, [iamRunUrl, iamProvidersAuthorizationURL, addIAMProviderAuthorizationURL]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (externalToken) {
|
|
82
|
+
setLoadingWithToken(1);
|
|
83
|
+
loginAndNavigate(externalToken, logout, checkIAMToken, navigate, homeRoute)
|
|
84
|
+
.catch(error => {
|
|
85
|
+
console.debug('Failed to login with token from cookie..', error);
|
|
86
|
+
enqueueToast('Failed to check authentication.', { variant: 'error' });
|
|
87
|
+
})
|
|
88
|
+
.finally(() => {
|
|
89
|
+
setLoadingWithToken(-1);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}, [externalToken]);
|
|
93
|
+
const handleKeyDown = (event) => {
|
|
94
|
+
if (event.key === 'Enter') {
|
|
95
|
+
submit();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const handleHandleChange = (event) => {
|
|
99
|
+
setFormValues(prevFormValues => ({
|
|
100
|
+
...prevFormValues,
|
|
101
|
+
handle: event.target.value,
|
|
102
|
+
}));
|
|
103
|
+
};
|
|
104
|
+
const handlePasswordChange = (event) => {
|
|
105
|
+
setFormValues(prevFormValues => ({
|
|
106
|
+
...prevFormValues,
|
|
107
|
+
password: event.target.value,
|
|
108
|
+
}));
|
|
109
|
+
};
|
|
110
|
+
const submit = async () => {
|
|
111
|
+
if (loading ||
|
|
112
|
+
validationResult.handle !== '' ||
|
|
113
|
+
validationResult.password !== '') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setLoading(true);
|
|
117
|
+
loginMutation.mutate({
|
|
118
|
+
handle: formValues.handle,
|
|
119
|
+
password: formValues.password,
|
|
120
|
+
}, {
|
|
121
|
+
onSuccess: (resp) => {
|
|
122
|
+
if (resp.success) {
|
|
123
|
+
setValidationResult({ handle: '', password: '' });
|
|
124
|
+
const user = asUser(resp.user);
|
|
125
|
+
const token = resp.token;
|
|
126
|
+
setLogin(user, token);
|
|
127
|
+
navigate(homeRoute);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
enqueueToast('Failed to login. Check your username and password.', {
|
|
131
|
+
variant: 'warning',
|
|
132
|
+
});
|
|
133
|
+
setValidationResult({
|
|
134
|
+
handle: '',
|
|
135
|
+
password: 'Invalid credentials',
|
|
136
|
+
});
|
|
137
|
+
console.debug(`Failed to login: ${resp.message}`, resp.errors ?? '');
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
onSettled: () => {
|
|
141
|
+
setLoading(false);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
setValidationResult({
|
|
147
|
+
...validationResult,
|
|
148
|
+
handle: formValues.handle === undefined
|
|
149
|
+
? undefined
|
|
150
|
+
: validateLength(formValues.handle, 1)
|
|
151
|
+
? ''
|
|
152
|
+
: 'Your username may not be empty.',
|
|
153
|
+
password: formValues.password === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: validateLength(formValues.password, 1)
|
|
156
|
+
? ''
|
|
157
|
+
: 'Your password may not be empty.',
|
|
158
|
+
});
|
|
159
|
+
}, [formValues]);
|
|
160
|
+
return (_jsxs(PageLayout, { containerWidth: "medium", padding: "normal", style: { overflow: 'visible', minHeight: 'calc(100vh - 45px)' }, children: [_jsx(PageLayout.Header, { children: _jsx(Heading, { children: heading || 'Login to Datalayer' }) }), _jsx(PageLayout.Content, { children: loadingWithToken < 0 ? (_jsx(_Fragment, { children: _jsxs(Box, { display: "flex", children: [showEmailLogin && (_jsxs(Box, { sx: { label: { marginTop: 2 }, paddingRight: '10%' }, children: [_jsx(Box, { mt: 5, children: _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "Your username" }), _jsx(TextInput, { autoFocus: true, placeholder: "Your username", value: formValues.handle, onChange: handleHandleChange, onKeyDown: handleKeyDown }), validationResult.handle && (_jsx(FormControl.Validation, { variant: validationResult.handle ? 'error' : 'success', children: validationResult.handle }))] }) }), _jsx(Box, { children: _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "Your password" }), _jsx(TextInput, { placeholder: "Your password", type: passwordVisibility ? 'text' : 'password', value: formValues.password, onChange: handlePasswordChange, onKeyDown: handleKeyDown, trailingAction: _jsx(TextInput.Action, { onClick: () => {
|
|
161
|
+
setPasswordVisibility(!passwordVisibility);
|
|
162
|
+
}, icon: passwordVisibility ? EyeClosedIcon : EyeIcon, "aria-label": passwordVisibility
|
|
163
|
+
? 'Hide password'
|
|
164
|
+
: 'Show password', sx: { color: 'var(--fgColor-muted)' } }), sx: { overflow: 'visible' } }), validationResult.password && (_jsx(FormControl.Validation, { variant: validationResult.password ? 'error' : 'success', children: validationResult.password }))] }) }), _jsxs(Box, { mt: 5, children: [_jsx(Button, { variant: "primary", disabled: loading ||
|
|
165
|
+
validationResult.handle !== '' ||
|
|
166
|
+
validationResult.password !== '', onClick: submit, children: loading
|
|
167
|
+
? 'Login…'
|
|
168
|
+
: heading
|
|
169
|
+
? 'Login with Datalayer'
|
|
170
|
+
: 'Login' }), _jsx(Box, { pt: 6 }), _jsx(Link, { href: "https://datalayer.app/password", target: "_blank", children: "Forgot password?" })] })] })), _jsx(Box, { children: _jsxs(Box, { display: "flex", flexDirection: "column", sx: { margin: 'auto' }, children: [showGitHubLogin &&
|
|
171
|
+
iamProvidersAuthorizationURL[IAMProvidersSpecs.GitHub.name] && (_jsx(Button, { leadingVisual: MarkGithubIcon, href: iamProvidersAuthorizationURL[IAMProvidersSpecs.GitHub.name], as: "a", style: { margin: '10px 0' }, children: "Login with GitHub" })), showTokenLogin && (_jsx(LoginToken, { homeRoute: homeRoute, style: { margin: '10px 0' } }))] }) })] }) })) : loadingWithToken ? (_jsx(CenteredSpinner, { message: "Checking authentication\u2026" })) : (_jsx(_Fragment, {})) })] }));
|
|
172
|
+
};
|
|
173
|
+
export default Login;
|