Skip to content

Commit

Permalink
Merge pull request #10955 from owncloud/feat/token-renewal-worker
Browse files Browse the repository at this point in the history
feat: move access token timer to web worker
  • Loading branch information
JammingBen authored May 28, 2024
2 parents 9dba349 + d1e1141 commit 7c3ab56
Show file tree
Hide file tree
Showing 17 changed files with 359 additions and 212 deletions.
2 changes: 1 addition & 1 deletion dev/docker/ocis/csp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ directives:
font-src:
- '''self'''
frame-ancestors:
- '''none'''
- '''self'''
frame-src:
- '''self'''
- 'https://embed.diagrams.net/'
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@types/node-fetch": "2.6.10",
"@vitejs/plugin-vue": "5.0.3",
"@vitest/coverage-v8": "1.5.0",
"@vitest/web-worker": "1.6.0",
"@vue/test-utils": "2.4.5",
"autoprefixer": "10.4.16",
"browserslist-to-esbuild": "^1.2.0",
Expand Down Expand Up @@ -105,7 +106,7 @@
"vite": "5.2.8",
"vite-plugin-environment": "^1.1.3",
"vite-plugin-node-polyfills": "0.21.0",
"vitest": "1.5.0",
"vitest": "1.6.0",
"vitest-mock-extended": "1.3.1",
"vue-tsc": "1.8.27",
"vue3-gettext": "2.4.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/web-pkg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"zod": "^3.22.4",
"@toast-ui/editor": "^3.2.2",
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
"@vitest/web-worker": "1.6.0",
"prismjs": "^1.29.0",
"vite-plugin-dts": "3.6.0"
},
Expand All @@ -68,6 +69,7 @@
"lodash-es": "^4.17.21",
"luxon": "3.2.1",
"mark.js": "^8.11.1",
"oidc-client-ts": "^2.4.0",
"p-queue": "^6.6.2",
"password-sheriff": "^1.1.1",
"pinia": "2.1.7",
Expand All @@ -77,6 +79,6 @@
"vue-concurrency": "5.0.1",
"vue-router": "4.2.5",
"vue3-gettext": "2.4.0",
"vitest": "1.4.0"
"vitest": "1.6.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useService } from '../service'

export interface AuthServiceInterface {
handleAuthError(route: any): any
signinSilent(): Promise<unknown>
}

export const useAuthService = (): AuthServiceInterface => {
Expand Down
18 changes: 13 additions & 5 deletions packages/web-pkg/src/composables/piniaStores/webWorkers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ import { UseWebWorkerReturn, useWebWorker } from '@vueuse/core'

export interface WebWorker<T = any> extends UseWebWorkerReturn<T> {
id: string
needsTokenRenewal: boolean
}

export type WorkerTopic = 'startProcess' | 'tokenUpdate'

export const useWebWorkersStore = defineStore('webWorkers', () => {
const workers = ref([]) as Ref<WebWorker[]>

const createWorker = <T = any>(workerUrl: string): WebWorker<T> => {
const createWorker = <T = any>(
workerUrl: string,
{ needsTokenRenewal = false }: { needsTokenRenewal?: boolean } = {}
): WebWorker<T> => {
const workerId = uuidV4()
const result = useWebWorker(workerUrl, { type: 'module', name: workerId })
const worker = { id: workerId, ...result }
const worker = { id: workerId, needsTokenRenewal, ...result }
unref(workers).push(worker)
return worker
}
Expand All @@ -37,9 +41,13 @@ export const useWebWorkersStore = defineStore('webWorkers', () => {
}

const updateAccessTokens = (accessToken: string) => {
unref(workers).forEach(({ post }) => {
post(JSON.stringify({ topic: 'tokenUpdate', data: { accessToken: `Bearer ${accessToken}` } }))
})
unref(workers)
.filter(({ needsTokenRenewal }) => needsTokenRenewal)
.forEach(({ post }) => {
post(
JSON.stringify({ topic: 'tokenUpdate', data: { accessToken: `Bearer ${accessToken}` } })
)
})
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export const useDeleteWorker = () => {
{ space, resources }: { space: SpaceResource; resources: Resource[] },
callback: (result: WorkerReturnData) => void
) => {
const worker = createWorker<WorkerReturnData>(DeleteWorker as unknown as string)
const worker = createWorker<WorkerReturnData>(DeleteWorker as unknown as string, {
needsTokenRenewal: true
})

let resolveLoading: (value: unknown) => void

Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/webWorkers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './deleteWorker'
export * from './tokenTimerWorker'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useTokenTimerWorker'
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ref, unref } from 'vue'
import { ErrorTimeout } from 'oidc-client-ts'
import { AuthServiceInterface } from '../../authContext'
import { WebWorker, useWebWorkersStore } from '../../piniaStores/webWorkers'
import TokenWorker from './worker?worker'

export type TokenTimerWorkerTopic = 'set' | 'reset'

export const useTokenTimerWorker = ({ authService }: { authService: AuthServiceInterface }) => {
const { createWorker } = useWebWorkersStore()

const worker = ref<WebWorker>()

const startWorker = () => {
worker.value = createWorker(TokenWorker as unknown as string)

unref(unref(worker).worker).onmessage = () => {
authService.signinSilent().catch((error) => {
if (error instanceof ErrorTimeout) {
console.warn('token renewal timed out, retrying in 5 seconds...')
unref(worker).post(JSON.stringify({ topic: 'set', expiry: 5, expiryThreshold: 0 }))
return
}

console.error('token renewal error:', error)
})
}
}

const setTokenTimer = ({
expiry,
expiryThreshold
}: {
expiry: number
expiryThreshold: number
}) => {
if (!unref(worker)) {
console.error('token timer worker is not running')
return
}

unref(worker).post(JSON.stringify({ topic: 'set', expiry, expiryThreshold }))
}

const resetTokenTimer = () => {
if (!unref(worker)) {
console.error('token timer worker is not running')
return
}

unref(worker).post(JSON.stringify({ topic: 'reset' }))
}

return { startWorker, setTokenTimer, resetTokenTimer }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { TokenTimerWorkerTopic } from './useTokenTimerWorker'

type Message = {
topic: TokenTimerWorkerTopic
expiry: number
expiryThreshold: number
}

let timerId: ReturnType<typeof setTimeout>

const resetTimer = () => {
clearTimeout(timerId)
timerId = undefined
}

self.onmessage = (e: MessageEvent) => {
const { topic, expiry, expiryThreshold } = JSON.parse(e.data) as Message

if (topic === 'reset') {
resetTimer()
return
}

let timerInSeconds = expiry - expiryThreshold
if (timerInSeconds <= 0) {
// timer can't be smaller or equal 0
timerInSeconds = 1
}

resetTimer()

timerId = setTimeout(() => {
postMessage(true)
}, timerInSeconds * 1000)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { unref } from 'vue'
import { getComposableWrapper } from 'web-test-helpers'
import { mock } from 'vitest-mock-extended'
import {
AuthServiceInterface,
WebWorker,
WebWorkersStore,
useTokenTimerWorker,
useWebWorkersStore
} from '../../../../../src/composables'

describe('useTokenTimerWorker', () => {
describe('method "startWorker"', () => {
it('creates a worker instance', () => {
getWrapper({
setup: ({ startWorker }, { webWorkersStore }) => {
startWorker()
expect(vi.mocked(webWorkersStore.createWorker)).toHaveBeenCalled()
}
})
})
})

describe('method "setTokenTimer"', () => {
it('posts an event to the worker when started', () => {
getWrapper({
setup: ({ startWorker, setTokenTimer }, { workerMock }) => {
startWorker()
const expiryOptions = { expiry: 10, expiryThreshold: 1 }
setTokenTimer(expiryOptions)

expect(unref(workerMock).post).toHaveBeenCalledWith(
JSON.stringify({ topic: 'set', ...expiryOptions })
)
}
})
})
it('does not post an event to the worker when not started', () => {
const consoleSpy = vi.fn()
vi.spyOn(console, 'error').mockImplementation(consoleSpy)

getWrapper({
setup: ({ setTokenTimer }) => {
const expiryOptions = { expiry: 10, expiryThreshold: 1 }
setTokenTimer(expiryOptions)

expect(consoleSpy).toHaveBeenCalled()
}
})
})
})

describe('method "resetTokenTimer"', () => {
it('posts an event to the worker when started', () => {
getWrapper({
setup: ({ startWorker, resetTokenTimer }, { workerMock }) => {
startWorker()
resetTokenTimer()

expect(unref(workerMock).post).toHaveBeenCalledWith(JSON.stringify({ topic: 'reset' }))
}
})
})
it('does not post an event to the worker when not started', () => {
const consoleSpy = vi.fn()
vi.spyOn(console, 'error').mockImplementation(consoleSpy)

getWrapper({
setup: ({ resetTokenTimer }) => {
resetTokenTimer()

expect(consoleSpy).toHaveBeenCalled()
}
})
})
})
})

function getWrapper({
setup
}: {
setup: (
instance: ReturnType<typeof useTokenTimerWorker>,
{ webWorkersStore }: { webWorkersStore: WebWorkersStore; workerMock: WebWorker }
) => void
}) {
return {
wrapper: getComposableWrapper(() => {
const instance = useTokenTimerWorker({ authService: mock<AuthServiceInterface>() })

const webWorkersStore = useWebWorkersStore()

const workerMock = mock<WebWorker>()
vi.mocked(webWorkersStore.createWorker).mockReturnValue(workerMock)

setup(instance, { webWorkersStore, workerMock })
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { unref } from 'vue'
import { useWebWorker } from '@vueuse/core'
import TokenWorker from '../../../../../src/composables/webWorkers/tokenTimerWorker/worker?worker'

describe('token timer worker', () => {
let worker: ReturnType<typeof useWebWorker>

beforeEach(() => {
worker = useWebWorker(TokenWorker as unknown as string, { type: 'module' })
})

afterEach(() => {
worker.terminate()
})

it('should not post a message with "reset" topic', async () => {
const messageSpy = vi.fn()
unref(worker.worker).onmessage = messageSpy
worker.post(JSON.stringify({ topic: 'reset' }))

await new Promise((resolve) => setTimeout(resolve, 1100))

expect(messageSpy).not.toHaveBeenCalled()
})

it('should post a message with "set" topic', async () => {
const messageSpy = vi.fn()
unref(worker.worker).onmessage = messageSpy
worker.post(JSON.stringify({ topic: 'set', expiry: 1, expiryThreshold: 1 }))

await new Promise((resolve) => setTimeout(resolve, 1100))

expect(messageSpy).toHaveBeenCalled()
})
})
Loading

0 comments on commit 7c3ab56

Please sign in to comment.