@matdata/yasgui 5.6.0 → 5.7.0
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/build/ts/src/ConfigExportImport.js +1 -0
- package/build/ts/src/ConfigExportImport.js.map +1 -1
- package/build/ts/src/OAuth2Utils.d.ts +18 -0
- package/build/ts/src/OAuth2Utils.js +214 -0
- package/build/ts/src/OAuth2Utils.js.map +1 -0
- package/build/ts/src/Tab.d.ts +1 -0
- package/build/ts/src/Tab.js +62 -3
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +1 -0
- package/build/ts/src/TabSettingsModal.js +197 -17
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/index.d.ts +12 -0
- package/build/ts/src/index.js +3 -0
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/version.d.ts +1 -1
- package/build/ts/src/version.js +1 -1
- package/build/yasgui.min.css +1 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +185 -157
- package/build/yasgui.min.js.map +4 -4
- package/package.json +3 -2
- package/src/ConfigExportImport.ts +1 -0
- package/src/OAuth2Utils.ts +315 -0
- package/src/Tab.ts +85 -2
- package/src/TabSettingsModal.scss +70 -3
- package/src/TabSettingsModal.ts +234 -13
- package/src/index.ts +15 -0
- package/src/tab.scss +1 -0
- package/src/themes.scss +1 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matdata/yasgui",
|
|
3
3
|
"description": "Yet Another SPARQL GUI",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.7.0",
|
|
5
5
|
"main": "build/yasgui.min.js",
|
|
6
6
|
"types": "build/ts/src/index.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"jsuri": "^1.3.1",
|
|
34
34
|
"lodash-es": "^4.17.15",
|
|
35
35
|
"sortablejs": "^1.10.2",
|
|
36
|
-
"@matdata/yasgui-graph-plugin": "^1.
|
|
36
|
+
"@matdata/yasgui-graph-plugin": "^1.3.0",
|
|
37
|
+
"@matdata/yasgui-table-plugin": "^1.0.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/autosuggest-highlight": "^3.1.0",
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Utility Functions
|
|
3
|
+
* Handles OAuth 2.0 Authorization Code flow with PKCE
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a random string for PKCE code verifier
|
|
8
|
+
*/
|
|
9
|
+
function generateRandomString(length: number): string {
|
|
10
|
+
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
11
|
+
const values = crypto.getRandomValues(new Uint8Array(length));
|
|
12
|
+
return Array.from(values)
|
|
13
|
+
.map((v) => possible[v % possible.length])
|
|
14
|
+
.join("");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate SHA-256 hash and base64url encode it
|
|
19
|
+
*/
|
|
20
|
+
async function sha256(plain: string): Promise<string> {
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
const data = encoder.encode(plain);
|
|
23
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
24
|
+
return base64urlEncode(hash);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Base64url encode (without padding)
|
|
29
|
+
*/
|
|
30
|
+
function base64urlEncode(buffer: ArrayBuffer): string {
|
|
31
|
+
const bytes = new Uint8Array(buffer);
|
|
32
|
+
let binary = "";
|
|
33
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
34
|
+
binary += String.fromCharCode(bytes[i]);
|
|
35
|
+
}
|
|
36
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OAuth2Config {
|
|
40
|
+
clientId: string;
|
|
41
|
+
authorizationEndpoint: string;
|
|
42
|
+
tokenEndpoint: string;
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
scope?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OAuth2TokenResponse {
|
|
48
|
+
access_token: string;
|
|
49
|
+
id_token?: string; // ID token for authentication (Azure AD, OIDC)
|
|
50
|
+
refresh_token?: string;
|
|
51
|
+
expires_in?: number; // seconds
|
|
52
|
+
token_type: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a token has expired
|
|
57
|
+
*/
|
|
58
|
+
export function isTokenExpired(tokenExpiry?: number): boolean {
|
|
59
|
+
if (!tokenExpiry) return true;
|
|
60
|
+
// Add 60 second buffer to refresh before actual expiration
|
|
61
|
+
return Date.now() >= tokenExpiry - 60000;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Start OAuth 2.0 Authorization Code flow with PKCE
|
|
66
|
+
* Opens a popup window for user authentication
|
|
67
|
+
*/
|
|
68
|
+
export async function startOAuth2Flow(config: OAuth2Config): Promise<OAuth2TokenResponse> {
|
|
69
|
+
// Generate PKCE code verifier and challenge
|
|
70
|
+
const codeVerifier = generateRandomString(128);
|
|
71
|
+
const codeChallenge = await sha256(codeVerifier);
|
|
72
|
+
|
|
73
|
+
// Generate state for CSRF protection
|
|
74
|
+
const state = generateRandomString(32);
|
|
75
|
+
|
|
76
|
+
// Generate unique flow ID to prevent collisions across multiple OAuth flows
|
|
77
|
+
const flowId = generateRandomString(16);
|
|
78
|
+
|
|
79
|
+
// Store code verifier and state in sessionStorage for later retrieval with unique keys
|
|
80
|
+
sessionStorage.setItem(`oauth2_code_verifier_${flowId}`, codeVerifier);
|
|
81
|
+
sessionStorage.setItem(`oauth2_state_${flowId}`, state);
|
|
82
|
+
|
|
83
|
+
// Build authorization URL
|
|
84
|
+
const authUrl = new URL(config.authorizationEndpoint);
|
|
85
|
+
authUrl.searchParams.set("client_id", config.clientId);
|
|
86
|
+
authUrl.searchParams.set("response_type", "code");
|
|
87
|
+
authUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
88
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
89
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
90
|
+
// Include flowId in state to link the callback to this specific flow
|
|
91
|
+
authUrl.searchParams.set("state", `${state}:${flowId}`);
|
|
92
|
+
if (config.scope) {
|
|
93
|
+
authUrl.searchParams.set("scope", config.scope);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Open popup window
|
|
97
|
+
const width = 600;
|
|
98
|
+
const height = 700;
|
|
99
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
100
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
101
|
+
const popup = window.open(
|
|
102
|
+
authUrl.toString(),
|
|
103
|
+
"OAuth2 Authorization",
|
|
104
|
+
`width=${width},height=${height},left=${left},top=${top},popup=yes,scrollbars=yes`,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!popup) {
|
|
108
|
+
throw new Error("Failed to open OAuth 2.0 authorization popup. Please allow popups for this site.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Wait for the OAuth callback
|
|
112
|
+
return new Promise<OAuth2TokenResponse>((resolve, reject) => {
|
|
113
|
+
// Flag to prevent race condition between polling and messageHandler
|
|
114
|
+
let flowCompleted = false;
|
|
115
|
+
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
clearInterval(checkInterval);
|
|
118
|
+
window.removeEventListener("message", messageHandler);
|
|
119
|
+
// Clean up sessionStorage
|
|
120
|
+
sessionStorage.removeItem(`oauth2_code_verifier_${flowId}`);
|
|
121
|
+
sessionStorage.removeItem(`oauth2_state_${flowId}`);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const checkInterval = setInterval(() => {
|
|
125
|
+
try {
|
|
126
|
+
// Check if popup is closed
|
|
127
|
+
if (popup.closed) {
|
|
128
|
+
if (!flowCompleted) {
|
|
129
|
+
flowCompleted = true;
|
|
130
|
+
cleanup();
|
|
131
|
+
reject(new Error("OAuth 2.0 authorization was cancelled"));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Try to read popup URL (will throw if cross-origin)
|
|
137
|
+
try {
|
|
138
|
+
const popupUrl = popup.location.href;
|
|
139
|
+
if (popupUrl.startsWith(config.redirectUri) && !flowCompleted) {
|
|
140
|
+
flowCompleted = true;
|
|
141
|
+
cleanup();
|
|
142
|
+
popup.close();
|
|
143
|
+
|
|
144
|
+
// Parse the authorization code from URL
|
|
145
|
+
const url = new URL(popupUrl);
|
|
146
|
+
const code = url.searchParams.get("code");
|
|
147
|
+
const returnedState = url.searchParams.get("state");
|
|
148
|
+
const error = url.searchParams.get("error");
|
|
149
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
150
|
+
|
|
151
|
+
if (error) {
|
|
152
|
+
reject(new Error(`OAuth 2.0 error: ${error}${errorDescription ? " - " + errorDescription : ""}`));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!code) {
|
|
157
|
+
reject(new Error("No authorization code received"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate state includes our flowId
|
|
162
|
+
const expectedState = `${state}:${flowId}`;
|
|
163
|
+
if (returnedState !== expectedState) {
|
|
164
|
+
reject(new Error("State mismatch - possible CSRF attack"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Exchange code for tokens
|
|
169
|
+
exchangeCodeForToken(config, code, codeVerifier).then(resolve).catch(reject);
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// Cross-origin error is expected when on OAuth provider's domain
|
|
173
|
+
// Keep waiting
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
// Ignore errors while checking popup
|
|
177
|
+
}
|
|
178
|
+
}, 500);
|
|
179
|
+
|
|
180
|
+
// Also listen for postMessage from redirect page (alternative method)
|
|
181
|
+
const messageHandler = (event: MessageEvent) => {
|
|
182
|
+
// Validate origin if needed (should match redirect URI origin)
|
|
183
|
+
if (event.data && event.data.type === "oauth2_callback" && !flowCompleted) {
|
|
184
|
+
flowCompleted = true;
|
|
185
|
+
cleanup();
|
|
186
|
+
if (popup && !popup.closed) {
|
|
187
|
+
popup.close();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { code, state: returnedState, error, error_description } = event.data;
|
|
191
|
+
|
|
192
|
+
if (error) {
|
|
193
|
+
reject(new Error(`OAuth 2.0 error: ${error}${error_description ? " - " + error_description : ""}`));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!code) {
|
|
198
|
+
reject(new Error("No authorization code received"));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Validate state includes our flowId
|
|
203
|
+
const expectedState = `${state}:${flowId}`;
|
|
204
|
+
if (returnedState !== expectedState) {
|
|
205
|
+
reject(new Error("State mismatch - possible CSRF attack"));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Exchange code for tokens
|
|
210
|
+
exchangeCodeForToken(config, code, codeVerifier).then(resolve).catch(reject);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
window.addEventListener("message", messageHandler);
|
|
215
|
+
|
|
216
|
+
// Cleanup timeout after 5 minutes
|
|
217
|
+
setTimeout(
|
|
218
|
+
() => {
|
|
219
|
+
if (!flowCompleted) {
|
|
220
|
+
flowCompleted = true;
|
|
221
|
+
cleanup();
|
|
222
|
+
if (popup && !popup.closed) {
|
|
223
|
+
popup.close();
|
|
224
|
+
}
|
|
225
|
+
reject(new Error("OAuth 2.0 authorization timeout"));
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
5 * 60 * 1000,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Exchange authorization code for access token
|
|
235
|
+
*/
|
|
236
|
+
async function exchangeCodeForToken(
|
|
237
|
+
config: OAuth2Config,
|
|
238
|
+
code: string,
|
|
239
|
+
codeVerifier: string,
|
|
240
|
+
): Promise<OAuth2TokenResponse> {
|
|
241
|
+
const tokenUrl = config.tokenEndpoint;
|
|
242
|
+
|
|
243
|
+
const body = new URLSearchParams();
|
|
244
|
+
body.set("grant_type", "authorization_code");
|
|
245
|
+
body.set("code", code);
|
|
246
|
+
body.set("redirect_uri", config.redirectUri);
|
|
247
|
+
body.set("client_id", config.clientId);
|
|
248
|
+
body.set("code_verifier", codeVerifier);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(tokenUrl, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: {
|
|
254
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
255
|
+
},
|
|
256
|
+
body: body.toString(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
const errorText = await response.text();
|
|
261
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const tokenResponse: OAuth2TokenResponse = await response.json();
|
|
265
|
+
|
|
266
|
+
// Note: sessionStorage cleanup is handled by the caller
|
|
267
|
+
return tokenResponse;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// Note: sessionStorage cleanup is handled by the caller
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Refresh an OAuth 2.0 access token using a refresh token
|
|
276
|
+
*/
|
|
277
|
+
export async function refreshOAuth2Token(
|
|
278
|
+
config: Omit<OAuth2Config, "authorizationEndpoint" | "redirectUri">,
|
|
279
|
+
refreshToken: string,
|
|
280
|
+
): Promise<OAuth2TokenResponse> {
|
|
281
|
+
const tokenUrl = config.tokenEndpoint;
|
|
282
|
+
|
|
283
|
+
const body = new URLSearchParams();
|
|
284
|
+
body.set("grant_type", "refresh_token");
|
|
285
|
+
body.set("refresh_token", refreshToken);
|
|
286
|
+
body.set("client_id", config.clientId);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const response = await fetch(tokenUrl, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: {
|
|
292
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
293
|
+
},
|
|
294
|
+
body: body.toString(),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
const errorText = await response.text();
|
|
299
|
+
throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const tokenResponse: OAuth2TokenResponse = await response.json();
|
|
303
|
+
return tokenResponse;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Calculate token expiry timestamp from expires_in value
|
|
311
|
+
*/
|
|
312
|
+
export function calculateTokenExpiry(expiresIn?: number): number | undefined {
|
|
313
|
+
if (!expiresIn) return undefined;
|
|
314
|
+
return Date.now() + expiresIn * 1000;
|
|
315
|
+
}
|
package/src/Tab.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as shareLink from "./linkUtils";
|
|
|
9
9
|
import EndpointSelect from "./endpointSelect";
|
|
10
10
|
import "./tab.scss";
|
|
11
11
|
import { getRandomId, default as Yasgui, YasguiRequestConfig } from "./";
|
|
12
|
+
import * as OAuth2Utils from "./OAuth2Utils";
|
|
12
13
|
|
|
13
14
|
// Layout orientation toggle icons
|
|
14
15
|
const HORIZONTAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24" class="svgImg">
|
|
@@ -477,9 +478,15 @@ export class Tab extends EventEmitter {
|
|
|
477
478
|
public getName() {
|
|
478
479
|
return this.persistentJson.name;
|
|
479
480
|
}
|
|
480
|
-
|
|
481
|
-
public query(): Promise<any> {
|
|
481
|
+
public async query(): Promise<any> {
|
|
482
482
|
if (!this.yasqe) return Promise.reject(new Error("No yasqe editor initialized"));
|
|
483
|
+
|
|
484
|
+
// Check and refresh OAuth 2.0 token if needed
|
|
485
|
+
const tokenValid = await this.ensureOAuth2TokenValid();
|
|
486
|
+
if (!tokenValid) {
|
|
487
|
+
return Promise.reject(new Error("OAuth 2.0 authentication failed"));
|
|
488
|
+
}
|
|
489
|
+
|
|
483
490
|
return this.yasqe.query();
|
|
484
491
|
}
|
|
485
492
|
|
|
@@ -528,11 +535,85 @@ export class Tab extends EventEmitter {
|
|
|
528
535
|
apiKey: auth.apiKey,
|
|
529
536
|
},
|
|
530
537
|
};
|
|
538
|
+
} else if (auth.type === "oauth2") {
|
|
539
|
+
// For OAuth 2.0, return the current access token and ID token
|
|
540
|
+
// Token refresh is handled separately before query execution
|
|
541
|
+
if (auth.accessToken) {
|
|
542
|
+
return {
|
|
543
|
+
type: "oauth2" as const,
|
|
544
|
+
config: {
|
|
545
|
+
accessToken: auth.accessToken,
|
|
546
|
+
idToken: auth.idToken,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
531
550
|
}
|
|
532
551
|
|
|
533
552
|
return undefined;
|
|
534
553
|
}
|
|
535
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Check and refresh OAuth 2.0 token if needed
|
|
557
|
+
* Should be called before query execution
|
|
558
|
+
*/
|
|
559
|
+
private async ensureOAuth2TokenValid(): Promise<boolean> {
|
|
560
|
+
const endpoint = this.getEndpoint();
|
|
561
|
+
if (!endpoint) return true;
|
|
562
|
+
|
|
563
|
+
const endpointConfig = this.yasgui.persistentConfig.getEndpointConfig(endpoint);
|
|
564
|
+
if (!endpointConfig || !endpointConfig.authentication) return true;
|
|
565
|
+
|
|
566
|
+
const auth = endpointConfig.authentication;
|
|
567
|
+
if (auth.type !== "oauth2") return true;
|
|
568
|
+
|
|
569
|
+
// Check if token is expired
|
|
570
|
+
if (OAuth2Utils.isTokenExpired(auth.tokenExpiry)) {
|
|
571
|
+
// Try to refresh the token if we have a refresh token
|
|
572
|
+
if (auth.refreshToken) {
|
|
573
|
+
try {
|
|
574
|
+
const tokenResponse = await OAuth2Utils.refreshOAuth2Token(
|
|
575
|
+
{
|
|
576
|
+
clientId: auth.clientId,
|
|
577
|
+
tokenEndpoint: auth.tokenEndpoint,
|
|
578
|
+
},
|
|
579
|
+
auth.refreshToken,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const tokenExpiry = OAuth2Utils.calculateTokenExpiry(tokenResponse.expires_in);
|
|
583
|
+
|
|
584
|
+
// Update stored authentication with new tokens
|
|
585
|
+
this.yasgui.persistentConfig.addOrUpdateEndpoint(endpoint, {
|
|
586
|
+
authentication: {
|
|
587
|
+
...auth,
|
|
588
|
+
accessToken: tokenResponse.access_token,
|
|
589
|
+
idToken: tokenResponse.id_token,
|
|
590
|
+
refreshToken: tokenResponse.refresh_token || auth.refreshToken,
|
|
591
|
+
tokenExpiry,
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return true;
|
|
596
|
+
} catch (error) {
|
|
597
|
+
console.error("Failed to refresh OAuth 2.0 token:", error);
|
|
598
|
+
// Token refresh failed, user needs to re-authenticate
|
|
599
|
+
alert(
|
|
600
|
+
"Your OAuth 2.0 session has expired and could not be refreshed. Please re-authenticate by clicking the Settings button (gear icon) and selecting the SPARQL Endpoints tab.",
|
|
601
|
+
);
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
// No refresh token available, user needs to re-authenticate
|
|
606
|
+
alert(
|
|
607
|
+
"Your OAuth 2.0 session has expired. Please re-authenticate by clicking the Settings button (gear icon) and selecting the SPARQL Endpoints tab.",
|
|
608
|
+
);
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Token is still valid
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
536
617
|
/**
|
|
537
618
|
* The Yasgui configuration object may contain a custom request config
|
|
538
619
|
* This request config object can contain getter functions, or plain json
|
|
@@ -606,6 +687,8 @@ export class Tab extends EventEmitter {
|
|
|
606
687
|
processedReqConfig.bearerAuth = endpointAuth.config;
|
|
607
688
|
} else if (endpointAuth.type === "apiKey" && typeof processedReqConfig.apiKeyAuth === "undefined") {
|
|
608
689
|
processedReqConfig.apiKeyAuth = endpointAuth.config;
|
|
690
|
+
} else if (endpointAuth.type === "oauth2" && typeof processedReqConfig.oauth2Auth === "undefined") {
|
|
691
|
+
processedReqConfig.oauth2Auth = endpointAuth.config;
|
|
609
692
|
}
|
|
610
693
|
}
|
|
611
694
|
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
background: var(--yasgui-bg-primary, white);
|
|
64
64
|
border-radius: 8px;
|
|
65
65
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
66
|
-
max-width:
|
|
66
|
+
max-width: 1000px;
|
|
67
67
|
width: 90%;
|
|
68
68
|
max-height: 90vh;
|
|
69
69
|
display: flex;
|
|
@@ -160,6 +160,13 @@
|
|
|
160
160
|
|
|
161
161
|
.settingsSection {
|
|
162
162
|
margin-bottom: 20px;
|
|
163
|
+
|
|
164
|
+
h3 {
|
|
165
|
+
margin: 0 0 12px 0;
|
|
166
|
+
font-size: 16px;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
color: var(--yasgui-text-primary, #000);
|
|
169
|
+
}
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
.settingsLabel {
|
|
@@ -297,7 +304,8 @@
|
|
|
297
304
|
}
|
|
298
305
|
|
|
299
306
|
.primaryButton,
|
|
300
|
-
.secondaryButton
|
|
307
|
+
.secondaryButton,
|
|
308
|
+
.dangerButton {
|
|
301
309
|
padding: 8px 20px;
|
|
302
310
|
border: none;
|
|
303
311
|
border-radius: 4px;
|
|
@@ -320,7 +328,21 @@
|
|
|
320
328
|
color: var(--yasgui-text-primary, #333);
|
|
321
329
|
|
|
322
330
|
&:hover {
|
|
323
|
-
background: var(--yasgui-
|
|
331
|
+
background: var(--yasgui-bg-quaternary, #d0d0d0);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.dangerButton {
|
|
336
|
+
background: var(--yasgui-danger-bg, #dc3545);
|
|
337
|
+
color: white;
|
|
338
|
+
margin-top: 10px;
|
|
339
|
+
|
|
340
|
+
&:hover {
|
|
341
|
+
background: var(--yasgui-danger-hover, #c82333);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
&:active {
|
|
345
|
+
background: var(--yasgui-danger-active, #bd2130);
|
|
324
346
|
}
|
|
325
347
|
}
|
|
326
348
|
|
|
@@ -849,6 +871,24 @@
|
|
|
849
871
|
}
|
|
850
872
|
}
|
|
851
873
|
|
|
874
|
+
.authHelpLink {
|
|
875
|
+
padding: 10px 12px;
|
|
876
|
+
background: var(--yasgui-bg-secondary, #f5f5f5);
|
|
877
|
+
border: 1px solid var(--yasgui-border-color, #e0e0e0);
|
|
878
|
+
border-radius: 4px;
|
|
879
|
+
font-size: 13px;
|
|
880
|
+
margin-bottom: 12px;
|
|
881
|
+
|
|
882
|
+
a {
|
|
883
|
+
color: var(--yasgui-accent-color, #337ab7);
|
|
884
|
+
text-decoration: none;
|
|
885
|
+
|
|
886
|
+
&:hover {
|
|
887
|
+
text-decoration: underline;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
852
892
|
.authSecurityNotice {
|
|
853
893
|
padding: 12px;
|
|
854
894
|
background: var(--yasgui-warning-bg, #fff3cd);
|
|
@@ -925,6 +965,33 @@
|
|
|
925
965
|
}
|
|
926
966
|
}
|
|
927
967
|
|
|
968
|
+
// OAuth 2.0 specific styling
|
|
969
|
+
.authInputHelp {
|
|
970
|
+
font-size: 12px;
|
|
971
|
+
color: var(--yasgui-text-secondary, #666);
|
|
972
|
+
margin-top: 4px;
|
|
973
|
+
line-height: 1.4;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.oauth2TokenStatus {
|
|
977
|
+
padding: 8px 12px;
|
|
978
|
+
border-radius: 4px;
|
|
979
|
+
font-size: 14px;
|
|
980
|
+
font-weight: 500;
|
|
981
|
+
|
|
982
|
+
&.authenticated {
|
|
983
|
+
background: var(--yasgui-success-bg, #d4edda);
|
|
984
|
+
color: var(--yasgui-success-text, #155724);
|
|
985
|
+
border: 1px solid var(--yasgui-success-border, #c3e6cb);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
&.expired {
|
|
989
|
+
background: var(--yasgui-warning-bg, #fff3cd);
|
|
990
|
+
color: var(--yasgui-warning-text, #856404);
|
|
991
|
+
border: 1px solid var(--yasgui-warning-border, #ffeaa7);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
928
995
|
// Add endpoint form styling
|
|
929
996
|
.addEndpointForm {
|
|
930
997
|
margin-top: 20px;
|