Skip to content

Commit 62748a8

Browse files
committed
update AccessRepository.ts so it omits credentials if using Bearer Token Authorization
1 parent e137f48 commit 62748a8

2 files changed

Lines changed: 170 additions & 32 deletions

File tree

Lines changed: 101 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
import { ApiConfig, DataverseApiAuthMechanism } from '../../../core/infra/repositories/ApiConfig'
2+
import { WriteError } from '../../../core/domain/repositories/WriteError'
3+
import { ApiConstants } from '../../../core/infra/repositories/ApiConstants'
14
import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'
5+
import {
6+
buildRequestConfig,
7+
buildRequestUrl
8+
} from '../../../core/infra/repositories/apiConfigBuilders'
29
import { GuestbookResponseDTO } from '../../domain/dtos/GuestbookResponseDTO'
310
import { IAccessRepository } from '../../domain/repositories/IAccessRepository'
411

@@ -13,14 +20,7 @@ export class AccessRepository extends ApiRepository implements IAccessRepository
1320
const endpoint = this.buildApiEndpoint(`${this.accessResourceName}/datafile`, undefined, fileId)
1421
const queryParams = format ? { signed: true, format } : { signed: true }
1522

16-
return this.doPost(endpoint, guestbookResponse, queryParams)
17-
.then((response) => {
18-
const signedUrl = response.data.data.signedUrl
19-
return signedUrl
20-
})
21-
.catch((error) => {
22-
throw error
23-
})
23+
return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams)
2424
}
2525

2626
public async submitGuestbookForDatafilesDownload(
@@ -30,21 +30,14 @@ export class AccessRepository extends ApiRepository implements IAccessRepository
3030
): Promise<string> {
3131
const queryParams = format ? { signed: true, format } : { signed: true }
3232

33-
return this.doPost(
33+
return await this.submitGuestbookDownload(
3434
this.buildApiEndpoint(
3535
this.accessResourceName,
3636
`datafiles/${Array.isArray(fileIds) ? fileIds.join(',') : fileIds}`
3737
),
3838
guestbookResponse,
3939
queryParams
4040
)
41-
.then((response) => {
42-
const signedUrl = response.data.data.signedUrl
43-
return signedUrl
44-
})
45-
.catch((error) => {
46-
throw error
47-
})
4841
}
4942

5043
public async submitGuestbookForDatasetDownload(
@@ -59,14 +52,7 @@ export class AccessRepository extends ApiRepository implements IAccessRepository
5952
)
6053
const queryParams = format ? { signed: true, format } : { signed: true }
6154

62-
return this.doPost(endpoint, guestbookResponse, queryParams)
63-
.then((response) => {
64-
const signedUrl = response.data.data.signedUrl
65-
return signedUrl
66-
})
67-
.catch((error) => {
68-
throw error
69-
})
55+
return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams)
7056
}
7157

7258
public async submitGuestbookForDatasetVersionDownload(
@@ -82,13 +68,96 @@ export class AccessRepository extends ApiRepository implements IAccessRepository
8268
)
8369
const queryParams = format ? { signed: true, format } : { signed: true }
8470

85-
return this.doPost(endpoint, guestbookResponse, queryParams)
86-
.then((response) => {
87-
const signedUrl = response.data.data.signedUrl
88-
return signedUrl
89-
})
90-
.catch((error) => {
91-
throw error
92-
})
71+
return await this.submitGuestbookDownload(endpoint, guestbookResponse, queryParams)
72+
}
73+
74+
private async submitGuestbookDownload(
75+
apiEndpoint: string,
76+
guestbookResponse: GuestbookResponseDTO,
77+
queryParams: object
78+
): Promise<string> {
79+
const requestConfig = buildRequestConfig(
80+
true,
81+
queryParams,
82+
ApiConstants.CONTENT_TYPE_APPLICATION_JSON
83+
)
84+
const response = await fetch(
85+
this.buildUrlWithQueryParams(buildRequestUrl(apiEndpoint), queryParams),
86+
{
87+
method: 'POST',
88+
headers: this.buildFetchHeaders(requestConfig.headers),
89+
credentials: this.getFetchCredentials(requestConfig.withCredentials),
90+
body: JSON.stringify(guestbookResponse)
91+
}
92+
).catch((error) => {
93+
throw new WriteError(error instanceof Error ? error.message : String(error))
94+
})
95+
96+
const responseData = await this.parseResponseBody(response)
97+
98+
if (!response.ok) {
99+
throw new WriteError(this.buildFetchErrorMessage(response.status, responseData))
100+
}
101+
102+
return responseData.data.signedUrl as string
103+
}
104+
105+
private getFetchCredentials(withCredentials?: boolean): RequestCredentials | undefined {
106+
if (ApiConfig.dataverseApiAuthMechanism === DataverseApiAuthMechanism.BEARER_TOKEN) {
107+
return 'omit'
108+
}
109+
110+
if (withCredentials) {
111+
return 'include'
112+
}
113+
114+
return undefined
115+
}
116+
117+
private buildUrlWithQueryParams(requestUrl: string, queryParams: object): string {
118+
const url = new URL(requestUrl)
119+
120+
Object.entries(queryParams).forEach(([key, value]) => {
121+
if (value !== undefined && value !== null) {
122+
url.searchParams.append(key, String(value))
123+
}
124+
})
125+
126+
return url.toString()
127+
}
128+
129+
private buildFetchHeaders(headers?: Record<string, unknown>): Record<string, string> {
130+
const fetchHeaders: Record<string, string> = {}
131+
132+
if (!headers) {
133+
return fetchHeaders
134+
}
135+
136+
Object.entries(headers).forEach(([key, value]) => {
137+
if (value !== undefined) {
138+
fetchHeaders[key] = String(value)
139+
}
140+
})
141+
142+
return fetchHeaders
143+
}
144+
145+
private async parseResponseBody(response: Response): Promise<any> {
146+
const contentType = response.headers.get('content-type') ?? ''
147+
148+
if (contentType.includes('application/json')) {
149+
return await response.json()
150+
}
151+
152+
return await response.text()
153+
}
154+
155+
private buildFetchErrorMessage(status: number, responseData: any): string {
156+
const message =
157+
typeof responseData === 'string'
158+
? responseData
159+
: responseData?.message || responseData?.data?.message || 'unknown error'
160+
161+
return `[${status}] ${message}`
93162
}
94163
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import { AccessRepository } from '../../../src/access/infra/repositories/AccessRepository'
6+
import { GuestbookResponseDTO } from '../../../src/access/domain/dtos/GuestbookResponseDTO'
7+
import {
8+
ApiConfig,
9+
DataverseApiAuthMechanism
10+
} from '../../../src/core/infra/repositories/ApiConfig'
11+
import { TestConstants } from '../../testHelpers/TestConstants'
12+
13+
describe('AccessRepository', () => {
14+
const sut = new AccessRepository()
15+
const guestbookResponse: GuestbookResponseDTO = {
16+
guestbookResponse: {
17+
answers: [{ id: 1, value: 'question 1' }]
18+
}
19+
}
20+
21+
beforeEach(() => {
22+
window.localStorage.setItem(
23+
TestConstants.TEST_BEARER_TOKEN_LOCAL_STORAGE_KEY,
24+
JSON.stringify(TestConstants.TEST_DUMMY_BEARER_TOKEN)
25+
)
26+
})
27+
28+
afterEach(() => {
29+
window.localStorage.clear()
30+
})
31+
32+
test('uses fetch with credentials omit for bearer token auth', async () => {
33+
ApiConfig.init(
34+
TestConstants.TEST_API_URL,
35+
DataverseApiAuthMechanism.BEARER_TOKEN,
36+
undefined,
37+
TestConstants.TEST_BEARER_TOKEN_LOCAL_STORAGE_KEY
38+
)
39+
40+
const fetchMock = jest.fn().mockResolvedValue({
41+
ok: true,
42+
status: 200,
43+
headers: new Headers({ 'content-type': 'application/json' }),
44+
json: jest.fn().mockResolvedValue({
45+
data: {
46+
signedUrl: 'https://signed.dataset'
47+
}
48+
})
49+
} as unknown as Response)
50+
51+
global.fetch = fetchMock as typeof fetch
52+
53+
const actual = await sut.submitGuestbookForDatasetDownload(123, guestbookResponse, 'original')
54+
55+
expect(fetchMock).toHaveBeenCalledWith(
56+
`${TestConstants.TEST_API_URL}/access/dataset/123?signed=true&format=original`,
57+
{
58+
method: 'POST',
59+
headers: {
60+
'Content-Type': 'application/json',
61+
Authorization: `Bearer ${TestConstants.TEST_DUMMY_BEARER_TOKEN}`
62+
},
63+
credentials: 'omit',
64+
body: JSON.stringify(guestbookResponse)
65+
}
66+
)
67+
expect(actual).toBe('https://signed.dataset')
68+
})
69+
})

0 commit comments

Comments
 (0)