Skip to content

Commit

Permalink
refactor: use request interceptors in client service
Browse files Browse the repository at this point in the history
Removes the old way of re-initializing clients in the client service when they needed to. The client service now uses request interceptors for the dynamic headers such as `Authorization`.

This a) is more developer friendly since the client service can now be destructured and b) removes the overhead of re-initializing the clients over and over again.
  • Loading branch information
Jannik Stehle committed Sep 25, 2024
1 parent 97c79af commit d1db596
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 136 deletions.
18 changes: 16 additions & 2 deletions packages/web-pkg/src/http/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelTokenSource } from 'axios'
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
CancelTokenSource,
InternalAxiosRequestConfig
} from 'axios'
import merge from 'lodash-es/merge'
import { z } from 'zod'

Expand All @@ -9,9 +15,17 @@ export class HttpClient {
private readonly instance: AxiosInstance
private readonly cancelToken: CancelTokenSource

constructor(config?: AxiosRequestConfig) {
constructor(
config?: AxiosRequestConfig,
interceptor?: (
value: InternalAxiosRequestConfig<any>
) => InternalAxiosRequestConfig<any> | Promise<InternalAxiosRequestConfig<any>>
) {
this.cancelToken = axios.CancelToken.source()
this.instance = axios.create(config)
if (interceptor) {
this.instance.interceptors.request.use(interceptor)
}
}

public cancel(msg?: string): void {
Expand Down
218 changes: 95 additions & 123 deletions packages/web-pkg/src/services/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,16 @@
import { HttpClient as _HttpClient } from '../../http'
import { client } from '@ownclouders/web-client'
import { HttpClient } from '../../http'
import { graph, ocs, webdav } from '@ownclouders/web-client'
import { Graph } from '@ownclouders/web-client/graph'
import { OCS } from '@ownclouders/web-client/ocs'
import { Auth, AuthParameters } from './auth'
import axios, { AxiosInstance } from 'axios'
import { AuthParameters } from './auth'
import axios from 'axios'
import { v4 as uuidV4 } from 'uuid'
import { WebDAV } from '@ownclouders/web-client/webdav'
import { Language } from 'vue3-gettext'
import { FetchEventSourceInit } from '@microsoft/fetch-event-source'
import { sse } from '@ownclouders/web-client/sse'
import { AuthStore, ConfigStore } from '../../composables'

interface ClientContext {
language: string
token: string
publicLinkToken?: string
publicLinkPassword?: string
}

interface HttpClient extends ClientContext {
client: _HttpClient
}

interface OcClient extends ClientContext {
graph: Graph
ocs: OCS
webdav: WebDAV
}

const createFetchOptions = (authParams: AuthParameters, language: string): FetchEventSourceInit => {
return {
headers: {
Expand All @@ -39,24 +22,6 @@ const createFetchOptions = (authParams: AuthParameters, language: string): Fetch
}
}

const createAxiosInstance = (
authParams: AuthParameters,
language: string,
initiatorId: string
): AxiosInstance => {
const auth = new Auth(authParams)
const axiosClient = axios.create({
headers: { ...auth.getHeaders(), 'Accept-Language': language, 'Initiator-ID': initiatorId }
})
axiosClient.interceptors.request.use((config) => {
config.headers['X-Request-ID'] = uuidV4()
config.headers['X-Requested-With'] = 'XMLHttpRequest'
config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
return config
})
return axiosClient
}

export interface ClientServiceOptions {
configStore: ConfigStore
language: Language
Expand All @@ -68,46 +33,55 @@ export class ClientService {
private language: Language
private authStore: AuthStore

private initiatorUuid: string

private httpAuthenticatedClient: HttpClient
private httpUnAuthenticatedClient: HttpClient

private ocUserContextClient: OcClient
private ocPublicLinkContextClient: OcClient
private ocWebdavContextClient: OcClient
private graphClient: Graph
private ocsClient: OCS
private webDavClient: WebDAV

public initiatorId = uuidV4()

private staticHeaders: Record<string, string> = {
'Initiator-ID': this.initiatorId,
'X-Requested-With': 'XMLHttpRequest'
}

constructor(options: ClientServiceOptions) {
this.configStore = options.configStore
this.language = options.language
this.authStore = options.authStore

this.initiatorUuid = uuidV4()
}
this.initGraphClient()
this.initOcsClient()
this.initWebDavClient()

public get initiatorId(): string {
return this.initiatorUuid
this.httpAuthenticatedClient = new HttpClient(
{ baseURL: this.configStore.serverUrl, headers: this.staticHeaders },
(config) => {
Object.assign(config.headers, this.getDynamicHeaders())
return config
}
)
this.httpUnAuthenticatedClient = new HttpClient(
{ baseURL: this.configStore.serverUrl, headers: this.staticHeaders },
(config) => {
Object.assign(config.headers, this.getDynamicHeaders({ useAuth: false }))
return config
}
)
}

public get httpAuthenticated(): _HttpClient {
if (this.clientNeedsInit(this.httpAuthenticatedClient)) {
this.httpAuthenticatedClient = this.getHttpClient(true)
}
return this.httpAuthenticatedClient.client
public get httpAuthenticated() {
return this.httpAuthenticatedClient
}

public get httpUnAuthenticated(): _HttpClient {
if (this.clientNeedsInit(this.httpUnAuthenticatedClient, false)) {
this.httpUnAuthenticatedClient = this.getHttpClient()
}
return this.httpUnAuthenticatedClient.client
public get httpUnAuthenticated() {
return this.httpUnAuthenticatedClient
}

public get graphAuthenticated(): Graph {
if (this.clientNeedsInit(this.ocUserContextClient)) {
this.ocUserContextClient = this.getOcClient({ accessToken: this.authStore.accessToken })
}
return this.ocUserContextClient.graph
public get graphAuthenticated() {
return this.graphClient
}

public get sseAuthenticated(): EventSource {
Expand All @@ -117,78 +91,76 @@ export class ClientService {
)
}

public get ocsUserContext(): OCS {
if (this.clientNeedsInit(this.ocUserContextClient)) {
this.ocUserContextClient = this.getOcClient({ accessToken: this.authStore.accessToken })
}
return this.ocUserContextClient.ocs
public get ocs() {
return this.ocsClient
}

public ocsPublicLinkContext(password?: string): OCS {
if (this.clientNeedsInit(this.ocPublicLinkContextClient)) {
this.ocPublicLinkContextClient = this.getOcClient({
publicLinkToken: this.authStore.accessToken,
publicLinkPassword: password
})
}
return this.ocPublicLinkContextClient.ocs
/** @deprecated use `ocs()` instead */
public get ocsUserContext() {
return this.ocs
}

private getHttpClient(authenticated = false): HttpClient {
return {
...(!!authenticated && { token: this.authStore.accessToken }),
language: this.currentLanguage,
client: new _HttpClient({
baseURL: this.configStore.serverUrl,
headers: {
'Accept-Language': this.currentLanguage,
...(!!authenticated && { Authorization: 'Bearer ' + this.authStore.accessToken }),
'X-Requested-With': 'XMLHttpRequest',
'X-Request-ID': uuidV4(),
'Initiator-ID': this.initiatorId
}
})
}
/** @deprecated use `ocs()` instead */
public ocsPublicLinkContext(password?: string) {
return this.ocs
}

private getOcClient(authParams: AuthParameters): OcClient {
const { graph, ocs, webdav } = client({
axiosClient: createAxiosInstance(authParams, this.currentLanguage, this.initiatorId),
baseURI: this.configStore.serverUrl
})
public get webdav() {
return this.webDavClient
}

return {
token: this.authStore.accessToken,
language: this.currentLanguage,
graph,
ocs,
webdav
}
get currentLanguage() {
return this.language.current
}

private clientNeedsInit(client: ClientContext, hasToken = true) {
return (
!client ||
(hasToken && client.token !== this.authStore.accessToken) ||
client.publicLinkPassword !== this.authStore.publicLinkPassword ||
client.publicLinkToken !== this.authStore.publicLinkToken ||
client.language !== this.currentLanguage
)
private initGraphClient() {
const axiosClient = axios.create({ headers: this.staticHeaders })
axiosClient.interceptors.request.use((config) => {
Object.assign(config.headers, this.getDynamicHeaders())
return config
})
this.graphClient = graph(this.configStore.serverUrl, axiosClient)
}

public get webdav(): WebDAV {
const hasToken = !!this.authStore.accessToken
if (this.clientNeedsInit(this.ocWebdavContextClient, hasToken)) {
this.ocWebdavContextClient = this.getOcClient({
accessToken: this.authStore.accessToken,
publicLinkPassword: this.authStore.publicLinkPassword,
publicLinkToken: this.authStore.publicLinkToken
})
}
return this.ocWebdavContextClient.webdav
private initOcsClient() {
const axiosClient = axios.create({ headers: this.staticHeaders })
axiosClient.interceptors.request.use((config) => {
Object.assign(config.headers, this.getDynamicHeaders())
return config
})
this.ocsClient = ocs(this.configStore.serverUrl, axiosClient)
}

get currentLanguage(): string {
return this.language.current
private initWebDavClient() {
this.webDavClient = webdav(this.configStore.serverUrl, () => {
const headers = { ...this.staticHeaders, ...this.getDynamicHeaders() }

if (this.authStore.publicLinkToken) {
headers['public-token'] = this.authStore.publicLinkToken
}

if (this.authStore.publicLinkPassword) {
headers['Authorization'] =
'Basic ' +
Buffer.from(['public', this.authStore.publicLinkPassword].join(':')).toString('base64')
}

return headers
})
}

/**
* Dynamic headers that should be provided via callback or interceptor because they may
* change during the lifetime of the application (e.g. token renewal).
*/
private getDynamicHeaders({ useAuth = true }: { useAuth?: boolean } = {}): Record<
string,
string
> {
return {
'Accept-Language': this.currentLanguage,
'X-Request-ID': uuidV4(),
...(useAuth && { Authorization: 'Bearer ' + this.authStore.accessToken })
}
}
}
20 changes: 9 additions & 11 deletions packages/web-runtime/src/services/auth/publicLinkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,17 @@ export class PublicLinkManager {
return
}

let password
if (this.isPasswordRequired(token)) {
password = this.getPassword(token)
}

try {
await this.fetchCapabilities({
password
})
await this.fetchCapabilities()
} catch (e) {
console.error(e)
}

let password: string
if (this.isPasswordRequired(token)) {
password = this.getPassword(token)
}

this.authStore.setPublicLinkContext({
publicLinkToken: token,
publicLinkPassword: password,
Expand All @@ -116,12 +114,12 @@ export class PublicLinkManager {
this.authStore.clearPublicLinkContext()
}

private async fetchCapabilities({ password = '' }): Promise<void> {
private async fetchCapabilities(): Promise<void> {
if (this.capabilityStore.isInitialized) {
return
}
const client = this.clientService.ocsPublicLinkContext(password)
const response = await client.getCapabilities()
const { ocs } = this.clientService
const response = await ocs.getCapabilities()
this.capabilityStore.setCapabilities(response)
}
}

0 comments on commit d1db596

Please sign in to comment.