33import { Component , OnInit , inject } from '@angular/core' ;
44import { PrimeNG } from 'primeng/config' ;
55import { DataService } from './data.service' ;
6+ import { DatasetService } from './dataset.service' ;
67import { ActivatedRoute , Router , RouterOutlet } from '@angular/router' ;
78import { PluginService } from './plugin.service' ;
89
@@ -15,6 +16,7 @@ import { PluginService } from './plugin.service';
1516export class AppComponent implements OnInit {
1617 private primengConfig = inject ( PrimeNG ) ;
1718 dataService = inject ( DataService ) ;
19+ private datasetService = inject ( DatasetService ) ;
1820 private router = inject ( Router ) ;
1921 private route = inject ( ActivatedRoute ) ;
2022 private pluginService = inject ( PluginService ) ;
@@ -54,10 +56,12 @@ export class AppComponent implements OnInit {
5456 /**
5557 * Checks if the query params indicate a download flow that should skip login redirect.
5658 * Download flows allow guest access with Globus OAuth only.
59+ * Also handles navigation to /download when URL contains /download but route doesn't match.
5760 */
5861 private isDownloadFlow ( params : Record < string , string | undefined > ) : boolean {
5962 // For testing: allow overriding window.location.href
6063 const locationHref =
64+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6165 ( this as any ) . _testWindowLocationHref ?? window . location . href ;
6266
6367 // eslint-disable-next-line no-console
@@ -67,13 +71,26 @@ export class AppComponent implements OnInit {
6771 windowLocation : locationHref ,
6872 } ) ;
6973
70- // Check if we're on the download page (via router or window.location)
74+ // Check if we're on the download page via router
75+ if ( this . router . url . includes ( '/download' ) ) {
76+ // eslint-disable-next-line no-console
77+ console . debug (
78+ '[AppComponent] isDownloadFlow: true (router matches /download)' ,
79+ ) ;
80+ return true ;
81+ }
82+
83+ // Check if URL contains /download but Angular routing failed (e.g., /connect/download)
84+ // In this case, parse the callback and navigate to /download with the correct params
7185 if (
72- this . router . url . includes ( '/download' ) ||
73- locationHref . includes ( '/download' )
86+ locationHref . includes ( '/download' ) &&
87+ ! this . router . url . includes ( '/download' )
7488 ) {
7589 // eslint-disable-next-line no-console
76- console . debug ( '[AppComponent] isDownloadFlow: true (download page)' ) ;
90+ console . debug (
91+ '[AppComponent] URL contains /download but route does not match, redirecting to /download' ,
92+ ) ;
93+ this . redirectToDownload ( locationHref ) ;
7794 return true ;
7895 }
7996
@@ -139,6 +156,90 @@ export class AppComponent implements OnInit {
139156 return false ;
140157 }
141158
159+ /**
160+ * Parses the Globus callback from the URL and redirects to /download with the correct params.
161+ * The callback is a base64-encoded URL like:
162+ * https://example.com/api/v1/datasets/{datasetDbId}/globusDownloadParameters?locale=en&downloadId={uuid}
163+ */
164+ private redirectToDownload ( locationHref : string ) : void {
165+ try {
166+ const url = new URL ( locationHref ) ;
167+ const callback = url . searchParams . get ( 'callback' ) ;
168+
169+ if ( ! callback ) {
170+ // No callback, just navigate to /download
171+ this . router . navigate ( [ '/download' ] ) ;
172+ return ;
173+ }
174+
175+ const callbackUrl = atob ( callback ) ;
176+ const parts = callbackUrl . split ( '/' ) ;
177+ if ( parts . length <= 6 ) {
178+ // eslint-disable-next-line no-console
179+ console . warn (
180+ '[AppComponent] Invalid callback URL format:' ,
181+ callbackUrl ,
182+ ) ;
183+ this . router . navigate ( [ '/download' ] ) ;
184+ return ;
185+ }
186+
187+ // Extract datasetDbId from URL (position 6 in the path)
188+ const datasetDbId = parts [ 6 ] ;
189+
190+ // Extract downloadId from query params
191+ let downloadId : string | undefined ;
192+ const queryString = callbackUrl . split ( '?' ) [ 1 ] ;
193+ if ( queryString ) {
194+ const globusParams = queryString . split ( '&' ) ;
195+ for ( const p of globusParams ) {
196+ if ( p . startsWith ( 'downloadId=' ) ) {
197+ downloadId = p . substring ( 'downloadId=' . length ) ;
198+ break ;
199+ }
200+ }
201+ }
202+
203+ // Get the persistent ID (DOI) from the dataset database ID
204+ const dvToken = localStorage . getItem ( 'dataverseToken' ) ;
205+ this . datasetService
206+ . getDatasetVersion ( datasetDbId , dvToken ?? undefined )
207+ . subscribe ( {
208+ next : ( x ) => {
209+ // eslint-disable-next-line no-console
210+ console . debug ( '[AppComponent] Redirecting to /download with:' , {
211+ downloadId,
212+ datasetPid : x . persistentId ,
213+ } ) ;
214+ this . router . navigate ( [ '/download' ] , {
215+ queryParams : {
216+ downloadId : downloadId ,
217+ datasetPid : x . persistentId ,
218+ apiToken : dvToken ,
219+ } ,
220+ } ) ;
221+ } ,
222+ error : ( err ) => {
223+ // eslint-disable-next-line no-console
224+ console . error ( '[AppComponent] Failed to get dataset version:' , err ) ;
225+ // Navigate anyway with what we have
226+ this . router . navigate ( [ '/download' ] , {
227+ queryParams : {
228+ downloadId : downloadId ,
229+ } ,
230+ } ) ;
231+ } ,
232+ } ) ;
233+ } catch ( e ) {
234+ // eslint-disable-next-line no-console
235+ console . error (
236+ '[AppComponent] Failed to parse URL for download redirect:' ,
237+ e ,
238+ ) ;
239+ this . router . navigate ( [ '/download' ] ) ;
240+ }
241+ }
242+
142243 private static readonly REDIRECT_STORAGE_KEY = 'loginRedirectAttempt' ;
143244 private static readonly MAX_REDIRECTS = 2 ;
144245 private static readonly REDIRECT_WINDOW_MS = 30000 ; // 30 seconds
0 commit comments