Skip to content

Commit c9c0397

Browse files
committed
Move functions out of the client and make pure for React
1 parent 16fdd66 commit c9c0397

21 files changed

+542
-186
lines changed

package-lock.json

Lines changed: 53 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"typescript": "^5.2.2"
4040
},
4141
"dependencies": {
42-
"lodash-es": "^4.17.21"
42+
"lodash-es": "^4.17.21",
43+
"mobx": "^6.15.0",
44+
"mobx-react-lite": "^4.1.1"
4345
}
4446
}

src/Client.ts

Lines changed: 47 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use client';
22

3+
import { makeAutoObservable } from "mobx"
4+
35
import {
46
Federation,
57
Namespace,
@@ -13,8 +15,12 @@ import {
1315
import {
1416
fetchNamespace,
1517
getObjectToken,
16-
parsePelicanObjectUrl,
17-
fetchFederationConfiguration
18+
parseObjectUrl,
19+
fetchFederation,
20+
get,
21+
list,
22+
put,
23+
UnauthenticatedError
1824
} from "./pelican";
1925
import {
2026
generateCodeChallengeFromVerifier,
@@ -29,6 +35,7 @@ import {
2935
downloadResponse,
3036
ProxiedValue
3137
} from "./util"
38+
import startAuthorizationCodeFlow from "./security/startAuthorizationCodeFlow";
3239

3340
export 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
/**

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ export * from "./Client";
33

44
export * from "./types"
55

6+
export * from "./pelican"
7+
68
export default Client;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class UnauthenticatedError extends Error {
2+
constructor(message: string | undefined) {
3+
super(message);
4+
this.name = this.constructor.name;
5+
}
6+
}
7+
8+
export default UnauthenticatedError;

src/pelican/UnauthorizedError.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class UnauthorizedError extends Error {
2+
constructor(message: string | undefined) {
3+
super(message);
4+
this.name = this.constructor.name;
5+
}
6+
}
7+
8+
export default UnauthorizedError;
Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
1-
import {FederationConfiguration} from "../types";
1+
import {Federation, FederationConfiguration} from "../types";
22

3-
const registerFederation = async (federationHostname: string) => {
4-
5-
// Load in the federation configuration and save to the session
3+
const fetchFederation = async (federationHostname: string): Promise<Federation> => {
64
const configurationEndpoint = `https://${federationHostname}/.well-known/pelican-configuration`
75
const res = await fetch(configurationEndpoint)
86
if(res.status !== 200){
97
throw new Error(`Metadata endpoint returned ${res.status}: ` + configurationEndpoint)
108
}
119
const federationConfiguration = await res.json() as FederationConfiguration
12-
const federation = {
10+
return {
1311
hostname: federationHostname,
1412
configuration: federationConfiguration,
1513
namespaces: {}
1614
}
17-
18-
return federation
1915
}
2016

21-
export default registerFederation;
17+
export default fetchFederation;

0 commit comments

Comments
 (0)