Skip to content

Commit e25688c

Browse files
committed
feat: Add preview URL support for Globus downloads
Enable users with preview URLs to download draft datasets via Globus. Flow: 1. First arrival: check if user is logged in to Dataverse - If logged in: redirect to Globus OAuth immediately - If not logged in: show popup BEFORE OAuth redirect 2. Popup options: - 'Continue as guest': strips session_required_single_domain, allows any Globus account - 'Continue with preview URL': same as guest + uses preview token for API auth - 'Log in with KU Leuven': keeps session_required_single_domain restriction 3. After OAuth return: load files using the chosen authentication mode Technical changes: - Add isGuestMode, isPreviewUrlMode, previewUrlToken to LoginState - Add checkUserAndShowPopupOrRedirect() for first arrival flow - Add continueAsLoggedInUser() for KU Leuven login path - Strip session_required_single_domain based on isGuestMode flag - Preserve all state across OAuth redirect
1 parent ddf1f86 commit e25688c

3 files changed

Lines changed: 152 additions & 47 deletions

File tree

src/app/download/download.component.html

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,54 @@
11
<!-- Author: Eryk Kulikowski @ KU Leuven (2024). Apache 2.0 License -->
22

3-
<!-- Guest/Login choice popup -->
3+
<!-- Guest/Login choice popup - shown BEFORE OAuth redirect -->
44
<p-dialog
55
header="Access Options"
66
[modal]="true"
77
[(visible)]="showGuestLoginPopup"
88
[closable]="false"
99
>
1010
<p class="m-0">
11-
You are not logged in. You can continue as a guest to download publicly
12-
accessible files, or log in to access restricted files.
11+
You are not logged in to Dataverse. Choose how you want to access Globus:
1312
</p>
13+
14+
<!-- Preview URL input section - visible when downloadId is present -->
15+
@if (downloadId) {
16+
<div class="preview-url-section mt-3">
17+
<p class="m-0">
18+
<strong>Have a preview URL?</strong> Paste it below to access draft files:
19+
</p>
20+
<div class="p-inputgroup mt-2">
21+
<input
22+
type="text"
23+
pInputText
24+
[(ngModel)]="previewUrlInput"
25+
placeholder="Paste preview URL or token here"
26+
class="w-full"
27+
/>
28+
</div>
29+
</div>
30+
}
31+
1432
<ng-template pTemplate="footer">
15-
<p-button
16-
(click)="continueAsGuest()"
17-
styleClass="p-button-sm p-button-raised p-button-secondary"
18-
>Continue as guest</p-button
19-
>
20-
<p-button
21-
(click)="redirectToLogin()"
22-
styleClass="p-button-sm p-button-raised p-button-primary"
23-
[disabled]="!pluginService.getLoginRedirectUrl()"
24-
>Log in</p-button
25-
>
33+
<div>
34+
<p-button
35+
(click)="continueAsGuest()"
36+
styleClass="p-button-sm p-button-raised p-button-secondary"
37+
>Continue as guest</p-button
38+
>
39+
@if (downloadId && previewUrlInput) {
40+
<p-button
41+
(click)="continueWithPreviewUrl()"
42+
styleClass="p-button-sm p-button-raised p-button-warning"
43+
>Continue with preview URL</p-button
44+
>
45+
}
46+
<p-button
47+
(click)="continueAsLoggedInUser()"
48+
styleClass="p-button-sm p-button-raised p-button-primary"
49+
>Log in with KU Leuven</p-button
50+
>
51+
</div>
2652
</ng-template>
2753
</p-dialog>
2854

src/app/download/download.component.ts

Lines changed: 108 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { FormsModule } from '@angular/forms';
2727
import { PrimeTemplate, SelectItem, TreeNode } from 'primeng/api';
2828
import { Button, ButtonDirective } from 'primeng/button';
2929
import { Dialog } from 'primeng/dialog';
30+
import { InputTextModule } from 'primeng/inputtext';
3031
import { ProgressSpinnerModule } from 'primeng/progressspinner';
3132
import { Select } from 'primeng/select';
3233
import { Tree } from 'primeng/tree';
@@ -60,6 +61,7 @@ import { SubscriptionManager } from '../shared/types';
6061
ButtonDirective,
6162
Dialog,
6263
FormsModule,
64+
InputTextModule,
6365
Select,
6466
TreeTableModule,
6567
PrimeTemplate,
@@ -114,6 +116,9 @@ export class DownloadComponent
114116
done = false;
115117
datasetUrl = '';
116118
showGuestLoginPopup = false;
119+
previewUrlInput = '';
120+
isPreviewUrlMode = false;
121+
isGuestMode = false;
117122

118123
// globus
119124
token?: string;
@@ -149,34 +154,11 @@ export class DownloadComponent
149154
// eslint-disable-next-line no-console
150155
console.debug('[DownloadComponent] Config loaded');
151156

152-
// Check if user is logged in and show popup if not
153-
// eslint-disable-next-line no-console
154-
console.debug('[DownloadComponent] Checking user info for popup...');
155-
try {
156-
const userInfo = await firstValueFrom(this.dataService.getUserInfo());
157-
// eslint-disable-next-line no-console
158-
console.debug('[DownloadComponent] getUserInfo response:', userInfo);
159-
if (!userInfo.loggedIn) {
160-
// eslint-disable-next-line no-console
161-
console.debug('[DownloadComponent] User not logged in, showing popup');
162-
this.showGuestLoginPopup = true;
163-
} else {
164-
// eslint-disable-next-line no-console
165-
console.debug('[DownloadComponent] User is logged in, no popup needed');
166-
}
167-
} catch (err) {
168-
// eslint-disable-next-line no-console
169-
console.error('[DownloadComponent] getUserInfo error:', err);
170-
// eslint-disable-next-line no-console
171-
console.debug('[DownloadComponent] Assuming not logged in, showing popup');
172-
this.showGuestLoginPopup = true;
173-
}
174-
175157
const dvToken = localStorage.getItem('dataverseToken');
176158
if (dvToken !== null) {
177159
this.dataverseToken = dvToken;
178160
}
179-
this.route.queryParams.subscribe((params) => {
161+
this.route.queryParams.subscribe(async (params) => {
180162
const apiToken = params['apiToken'];
181163
if (apiToken) {
182164
this.dataverseToken = apiToken;
@@ -189,17 +171,27 @@ export class DownloadComponent
189171
this.downloadId = params['downloadId'];
190172
const code = params['code'];
191173
if (code !== undefined) {
174+
// Returning from OAuth - parse state
192175
const loginState: LoginState = JSON.parse(params['state']);
193176
if (loginState.nonce) {
194177
const doi = loginState.datasetId?.value
195178
? loginState.datasetId?.value
196179
: '?';
197180
this.doiItems = [{ label: doi, value: doi }];
198181
this.datasetId = doi;
199-
// Only try to load files if we have a valid dataset ID
200-
if (doi && doi !== '?' && doi !== 'undefined') {
201-
this.onDatasetChange();
182+
183+
// Restore state from OAuth redirect
184+
if (loginState.downloadId) {
185+
this.downloadId = loginState.downloadId;
186+
}
187+
if (loginState.isGuestMode) {
188+
this.isGuestMode = true;
202189
}
190+
if (loginState.isPreviewUrlMode && loginState.previewUrlToken) {
191+
this.isPreviewUrlMode = true;
192+
this.dataverseToken = loginState.previewUrlToken;
193+
}
194+
203195
const tokenSubscription = this.oauth
204196
.getToken('globus', code, loginState.nonce)
205197
.subscribe((x) => {
@@ -211,9 +203,15 @@ export class DownloadComponent
211203
});
212204
}
213205
this.globusPlugin = this.pluginService.getGlobusPlugin();
206+
207+
// After OAuth return, load files (user already made their choice before redirect)
208+
if (this.datasetId && this.datasetId !== '?' && this.datasetId !== 'undefined') {
209+
this.onDatasetChange();
210+
}
214211
} else {
212+
// First arrival - check if user is logged in and show popup if not
215213
this.globusPlugin = this.pluginService.getGlobusPlugin();
216-
this.getRepoToken();
214+
await this.checkUserAndShowPopupOrRedirect();
217215
}
218216
});
219217
this.datasetSearchResultsSubscription =
@@ -277,9 +275,82 @@ export class DownloadComponent
277275
}
278276

279277
continueAsGuest(): void {
280-
// User chose to continue without Dataverse login
281-
// They will still need to authenticate with Globus OAuth
278+
// User chose to continue as guest - strip session_required_single_domain and redirect to OAuth
279+
this.isGuestMode = true;
280+
this.showGuestLoginPopup = false;
281+
this.getRepoToken();
282+
}
283+
284+
continueAsLoggedInUser(): void {
285+
// User chose to log in with KU Leuven account - keep session_required_single_domain
286+
this.isGuestMode = false;
282287
this.showGuestLoginPopup = false;
288+
this.getRepoToken();
289+
}
290+
291+
async checkUserAndShowPopupOrRedirect(): Promise<void> {
292+
// Check if user is logged in; show popup if not, otherwise redirect to OAuth
293+
// eslint-disable-next-line no-console
294+
console.debug('[DownloadComponent] Checking user info on first arrival...');
295+
try {
296+
const userInfo = await firstValueFrom(this.dataService.getUserInfo());
297+
// eslint-disable-next-line no-console
298+
console.debug('[DownloadComponent] getUserInfo response:', userInfo);
299+
if (!userInfo.loggedIn) {
300+
// eslint-disable-next-line no-console
301+
console.debug('[DownloadComponent] User not logged in, showing popup');
302+
this.showGuestLoginPopup = true;
303+
} else {
304+
// eslint-disable-next-line no-console
305+
console.debug('[DownloadComponent] User is logged in, redirecting to OAuth');
306+
this.getRepoToken();
307+
}
308+
} catch (err) {
309+
// eslint-disable-next-line no-console
310+
console.error('[DownloadComponent] getUserInfo error:', err);
311+
// eslint-disable-next-line no-console
312+
console.debug('[DownloadComponent] Assuming not logged in, showing popup');
313+
this.showGuestLoginPopup = true;
314+
}
315+
}
316+
317+
continueWithPreviewUrl(): void {
318+
// Extract token from pasted preview URL and redirect to OAuth
319+
const token = this.extractPreviewUrlToken(this.previewUrlInput);
320+
if (token) {
321+
this.dataverseToken = token;
322+
this.isPreviewUrlMode = true;
323+
this.isGuestMode = true; // Preview users also need guest Globus access
324+
this.showGuestLoginPopup = false;
325+
// eslint-disable-next-line no-console
326+
console.debug('[DownloadComponent] Preview URL token extracted, redirecting to OAuth');
327+
this.getRepoToken();
328+
} else {
329+
// eslint-disable-next-line no-console
330+
console.error('[DownloadComponent] Could not extract token from preview URL');
331+
}
332+
}
333+
334+
extractPreviewUrlToken(input: string): string | null {
335+
if (!input) return null;
336+
const trimmed = input.trim();
337+
// If it's just a UUID-like token, return it directly
338+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
339+
if (uuidPattern.test(trimmed)) {
340+
return trimmed;
341+
}
342+
// Try to extract token from URL like https://.../previewurl.xhtml?token=...
343+
try {
344+
const url = new URL(trimmed);
345+
const token = url.searchParams.get('token');
346+
if (token) return token;
347+
} catch {
348+
// Not a valid URL, ignore
349+
}
350+
// Try regex fallback for token= in any string
351+
const tokenMatch = trimmed.match(/token=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
352+
if (tokenMatch) return tokenMatch[1];
353+
return null;
283354
}
284355

285356
onUserChange() {
@@ -685,20 +756,24 @@ export class DownloadComponent
685756
if (tg.URL?.includes('://')) {
686757
url = tg.URL;
687758
}
688-
// For guest users, strip session_required_single_domain to allow any Globus identity
689-
if (this.showGuestLoginPopup) {
759+
// For guest/preview users, strip session_required_single_domain to allow any Globus identity
760+
if (this.isGuestMode) {
690761
url = url.replace(/[&?]session_required_single_domain=[^&]*/g, '');
691762
}
692763
if (tg.oauth_client_id !== undefined && tg.oauth_client_id !== '') {
693764
const nonce = this.newNonce(44);
694-
// Only include datasetId if one is actually selected
765+
// Include all state to preserve across OAuth redirect
695766
const loginState: LoginState = {
696767
datasetId:
697768
this.datasetId && this.datasetId !== '?'
698769
? { value: this.datasetId, label: this.datasetId }
699770
: undefined,
700771
nonce: nonce,
701772
download: true,
773+
downloadId: this.downloadId,
774+
isGuestMode: this.isGuestMode,
775+
isPreviewUrlMode: this.isPreviewUrlMode,
776+
previewUrlToken: this.isPreviewUrlMode ? this.dataverseToken : undefined,
702777
};
703778
let clId = '?client_id=';
704779
if (url.includes('?')) {

src/app/models/oauth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export interface LoginState {
1313
datasetId?: Item;
1414
collectionId?: Item;
1515
download?: boolean;
16+
downloadId?: string;
17+
isGuestMode?: boolean;
18+
isPreviewUrlMode?: boolean;
19+
previewUrlToken?: string;
1620
}
1721

1822
export interface Item {

0 commit comments

Comments
 (0)