11'use client' ;
22
3+ import { makeAutoObservable } from "mobx"
4+
35import {
46 Federation ,
57 Namespace ,
@@ -13,8 +15,12 @@ import {
1315import {
1416 fetchNamespace ,
1517 getObjectToken ,
16- parsePelicanObjectUrl ,
17- fetchFederationConfiguration
18+ parseObjectUrl ,
19+ fetchFederation ,
20+ get ,
21+ list ,
22+ put ,
23+ UnauthenticatedError
1824} from "./pelican" ;
1925import {
2026 generateCodeChallengeFromVerifier ,
@@ -29,6 +35,7 @@ import {
2935 downloadResponse ,
3036 ProxiedValue
3137} from "./util"
38+ import startAuthorizationCodeFlow from "./security/startAuthorizationCodeFlow" ;
3239
3340export class Client {
3441
@@ -46,6 +53,9 @@ export class Client {
4653
4754 // If there is a code in the URL, exchange it for a token
4855 this . exchangeCodeForToken ( )
56+
57+ // For React
58+ makeAutoObservable ( this )
4959 }
5060
5161 /**
@@ -54,77 +64,53 @@ export class Client {
5464 */
5565 async get ( objectUrl : string ) : Promise < void > {
5666
57- const { federationHostname, objectPath } = parsePelicanObjectUrl ( objectUrl )
67+ const { federationHostname} = parseObjectUrl ( objectUrl )
5868 const federation = await this . getFederation ( federationHostname )
5969 const namespace = await this . getNamespace ( objectUrl , federation )
60- const token = await getObjectToken ( namespace )
6170
62- const objectHttpUrl = new URL ( `${ federation . configuration . director_endpoint } ${ objectPath } ` )
63- const response = await fetch ( objectHttpUrl , {
64- headers : {
65- "Authorization" : `Bearer ${ token ?. value } `
66- }
67- } )
68-
69- if ( response . status === 200 ) {
71+ try {
72+ const response = await get ( objectUrl , federation , namespace )
7073 downloadResponse ( response )
71-
72- // If we get a 403, queue this request call it after getting a token
73- } else if ( response . status === 403 && ! token ) {
74- await this . queueRequestAndStartFlow ( objectUrl , federation )
75- } else {
76- throw new Error ( `Could not get object: ${ response . status } ${ response . statusText } ` )
74+ } catch ( error ) {
75+ if ( error instanceof UnauthenticatedError ) {
76+ await this . queueRequestAndStartFlow ( objectUrl , federation )
77+ } else {
78+ throw error
79+ }
7780 }
7881 }
7982
8083 async list ( collectionUrl : string ) : Promise < ObjectList [ ] | undefined > {
81- const { federationHostname, objectPath} = parsePelicanObjectUrl ( collectionUrl )
84+
85+ const { federationHostname} = parseObjectUrl ( collectionUrl )
8286 const federation = await this . getFederation ( federationHostname )
8387 const namespace = await this . getNamespace ( collectionUrl , federation )
84- const token = await getObjectToken ( namespace )
8588
86- const objectHttpUrl = new URL ( `${ federation . configuration . director_endpoint } ${ objectPath } ` )
87- const response = await fetch ( objectHttpUrl , {
88- method : "PROPFIND" ,
89- headers : {
90- "Authorization" : `Bearer ${ token ?. value } ` ,
91- "Depth" : "4"
89+ try {
90+ return list ( collectionUrl , federation , namespace )
91+ } catch ( error ) {
92+ if ( error instanceof UnauthenticatedError ) {
93+ await this . queueRequestAndStartFlow ( collectionUrl , federation )
94+ } else {
95+ throw error
9296 }
93- } )
94-
95- if ( response . status === 207 ) {
96- return parseWebDavXmlToJson ( await response . text ( ) )
97-
98- // If we get a 403, queue this request call it after getting a token
99- } else if ( response . status === 403 ) {
100- await this . queueRequestAndStartFlow ( collectionUrl , federation )
101- } else {
102- throw new Error ( `Could not list directory: ${ response . status } ${ response . statusText } ` )
10397 }
10498 }
10599
106100 async put ( objectUrl : string , file : File ) : Promise < void > {
107101
108- const { federationHostname, objectPath } = parsePelicanObjectUrl ( objectUrl )
102+ const { federationHostname, objectPath } = parseObjectUrl ( objectUrl )
109103 const federation = await this . getFederation ( federationHostname )
110104 const namespace = await this . getNamespace ( objectUrl , federation )
111- const token = await getObjectToken ( namespace )
112105
113- const objectHttpUrl = new URL ( `${ federation . configuration . director_endpoint } ${ objectPath } ` )
114- const response = await fetch ( objectHttpUrl , {
115- method : "PUT" ,
116- headers : {
117- "Authorization" : `Bearer ${ token ?. value } `
118- } ,
119- body : file
120- } )
121-
122- if ( response . status === 200 || response . status === 201 ) {
123- return
124- } else if ( response . status === 403 ) {
125- await this . queueRequestAndStartFlow ( objectUrl , federation )
126- } else {
127- throw new Error ( `Could not upload object: ${ response . status } ${ response . statusText } ` )
106+ try {
107+ await put ( objectUrl , file , federation , namespace )
108+ } catch ( error ) {
109+ if ( error instanceof UnauthenticatedError ) {
110+ await this . queueRequestAndStartFlow ( objectUrl , federation )
111+ } else {
112+ throw error
113+ }
128114 }
129115 }
130116
@@ -133,7 +119,7 @@ export class Client {
133119 * @param objectUrl
134120 */
135121 async permissions ( objectUrl : string ) : Promise < TokenPermission [ ] > {
136- const { federationHostname } = parsePelicanObjectUrl ( objectUrl )
122+ const { federationHostname } = parseObjectUrl ( objectUrl )
137123 const federation = await this . getFederation ( federationHostname )
138124 const namespace = await this . getNamespace ( objectUrl , federation )
139125 const token = await getObjectToken ( namespace )
@@ -150,7 +136,7 @@ export class Client {
150136 * Parse an object URL for its federation and namespace
151137 */
152138 async parseObjectUrl ( objectUrl : string ) : Promise < { federation : string , namespace : string } > {
153- const { federationHostname } = parsePelicanObjectUrl ( objectUrl )
139+ const { federationHostname } = parseObjectUrl ( objectUrl )
154140 const federation = await this . getFederation ( federationHostname )
155141 const namespace = await this . getNamespace ( objectUrl , federation )
156142 return { federation : federation . hostname , namespace : namespace . prefix }
@@ -161,15 +147,15 @@ export class Client {
161147 */
162148 async exchangeCodeForToken ( ) {
163149 const authCode = getAuthorizationCode ( ) ;
164- if ( authCode === null ) return ;
150+ if ( authCode . code === null ) return ;
165151
166152 try {
167153 // Get the namespace and federation from the state parameter
168154 const { federation : federationHostname , namespace : namespacePrefix } = parseOauthState ( new URL ( window . location . href ) )
169155 const namespace = this . federations . value [ federationHostname ] ?. namespaces [ namespacePrefix ]
170156
171157 // Check if we have an auth code to exchange for a token
172- const token = await getToken ( namespace . oidcConfiguration , this . codeVerifier . value , namespace . clientId , namespace . clientSecret , authCode )
158+ const token = await getToken ( namespace . oidcConfiguration , this . codeVerifier . value , namespace . clientId , namespace . clientSecret , authCode . code )
173159
174160 // Save the token to the namespace
175161 const federationsClone = cloneDeep ( this . federations . value )
@@ -189,7 +175,7 @@ export class Client {
189175 if ( ! this . federations . value ?. [ federationHostname ] ) {
190176 this . federations . value = {
191177 ...this . federations . value ,
192- [ federationHostname ] : await fetchFederationConfiguration ( federationHostname )
178+ [ federationHostname ] : await fetchFederation ( federationHostname )
193179 }
194180 }
195181 return this . federations . value [ federationHostname ]
@@ -202,7 +188,7 @@ export class Client {
202188 */
203189 async getNamespace ( objectUrl : string , federation : Federation ) : Promise < Namespace > {
204190
205- const { objectPrefix, objectPath} = parsePelicanObjectUrl ( objectUrl )
191+ const { objectPrefix, objectPath} = parseObjectUrl ( objectUrl )
206192
207193 // Check if we have already cached the namespace for this prefix
208194 if ( this . prefixToNamespace ?. [ objectPrefix ] ) {
@@ -232,7 +218,7 @@ export class Client {
232218 // Fetch and save the namespace information
233219 const namespace = await this . getNamespace ( objectUrl , federation )
234220
235- const { objectPath} = parsePelicanObjectUrl ( objectUrl )
221+ const { objectPath} = parseObjectUrl ( objectUrl )
236222
237223 // Queue the file request
238224 this . requestQueue . value . push ( {
@@ -245,38 +231,7 @@ export class Client {
245231 } )
246232
247233 // Start the authorization code flow
248- await this . startAuthorizationCodeFlow ( objectPath , namespace , federation )
249- }
250-
251- /**
252- * Start the OIDC authorization code flow to get a token for a namespace
253- * @param objectPath
254- * @param namespace
255- * @param federation
256- */
257- async startAuthorizationCodeFlow ( objectPath : string , namespace : Namespace , federation : Federation ) : Promise < void > {
258-
259- // Determine the token scopes
260- const scope = objectPath
261- . replace ( 'pelican://' , '' )
262- . replace ( federation . hostname , '' )
263- . replace ( namespace . prefix , '' )
264- . trim ( )
265-
266- // Build the Oauth URL
267- const codeChallenge = await generateCodeChallengeFromVerifier ( this . codeVerifier . value )
268- const authorizationUrl = new URL ( namespace . oidcConfiguration . authorization_endpoint )
269- authorizationUrl . searchParams . append ( "client_id" , namespace . clientId )
270- authorizationUrl . searchParams . append ( "response_type" , "code" )
271- authorizationUrl . searchParams . append ( "scope" , `storage.read:${ scope } storage.create:${ scope } ` )
272- authorizationUrl . searchParams . append ( "redirect_uri" , "http://localhost:3000" )
273- authorizationUrl . searchParams . append ( "code_challenge" , codeChallenge )
274- authorizationUrl . searchParams . append ( "code_challenge_method" , "S256" )
275- authorizationUrl . searchParams . append ( "state" , `namespace:${ namespace . prefix } ;federation:${ federation . hostname } ` )
276- authorizationUrl . searchParams . append ( "action" , "" )
277-
278- // Begin the authorization code flow to get a token
279- window . location . href = authorizationUrl . toString ( )
234+ await startAuthorizationCodeFlow ( this . codeVerifier . value , namespace , federation )
280235 }
281236
282237 /**
0 commit comments