Skip to content

Commit 93b0fdf

Browse files
committed
feat: add setRequestToken and fetchRequestToken methods
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 33b43e3 commit 93b0fdf

8 files changed

Lines changed: 1610 additions & 897 deletions

File tree

lib/eventbus.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { NextcloudUser } from './user.ts'
77

88
declare module '@nextcloud/event-bus' {
99
export interface NextcloudEvents {
10+
'csrf-token-update': { token: string }
1011
// mapping of 'event name' => 'event type'
1112
'user:info:changed': NextcloudUser
1213
}

lib/globals.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
*/
55

66
declare global {
7+
// eslint-disable-next-line camelcase
8+
var _nc_auth_requesttoken: string | undefined
9+
710
interface Window {
811
_oc_isadmin?: boolean
912
}

lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ export type { NextcloudUser } from './user.ts'
88

99
export { getCSPNonce } from './csp-nonce.ts'
1010
export { getGuestNickname, getGuestUser, setGuestNickname } from './guest.ts'
11-
export { getRequestToken, onRequestTokenUpdate } from './requesttoken.ts'
11+
export { fetchRequestToken, getRequestToken, onRequestTokenUpdate, setRequestToken } from './requesttoken.ts'
1212
export { getCurrentUser } from './user.ts'

lib/requesttoken.ts

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,99 @@
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*/
55

6-
import { subscribe } from '@nextcloud/event-bus'
6+
import { emit, subscribe } from '@nextcloud/event-bus'
7+
import { generateUrl } from '@nextcloud/router'
78

89
export interface CsrfTokenObserver {
910
(token: string): void
1011
}
1112

12-
let token: string | null | undefined
13-
const observers: CsrfTokenObserver[] = []
14-
1513
/**
1614
* Get current request token
1715
*
1816
* @return Current request token or null if not set
1917
*/
2018
export function getRequestToken(): string | null {
21-
if (token === undefined) {
22-
// Only on first load, try to get token from document
23-
token = document.head.dataset.requesttoken ?? null
19+
if (globalThis.document) {
20+
return document.head.dataset.requesttoken ?? null
21+
}
22+
// for service workers or other contexts without DOM, we keep the token in memory
23+
return globalThis._nc_auth_requesttoken ?? null
24+
}
25+
26+
/**
27+
* Set a new CSRF token (e.g. because of session refresh).
28+
* This also emits an event bus event for the updated token.
29+
*
30+
* @param token - The new token
31+
* @fires Error - If the passed token is not a potential valid token
32+
*/
33+
export function setRequestToken(token: string): void {
34+
if (!token || typeof token !== 'string') {
35+
throw new Error('Invalid CSRF token given', { cause: { token } })
36+
}
37+
38+
if (globalThis.document) {
39+
document.head.dataset.requesttoken = token
40+
} else {
41+
globalThis._nc_auth_requesttoken = token
2442
}
43+
emit('csrf-token-update', { token })
44+
}
45+
46+
/**
47+
* Fetch the request token from the API.
48+
* This does also set it on the current context, see `setRequestToken`.
49+
*
50+
* @fires Error - If the request failed
51+
*/
52+
export async function fetchRequestToken(): Promise<string> {
53+
const url = generateUrl('/csrftoken')
54+
55+
const response = await fetch(url)
56+
if (!response.ok) {
57+
throw new Error('Could not fetch CSRF token from API', { cause: response })
58+
}
59+
60+
const { token } = await response.json()
61+
setRequestToken(token)
2562
return token
2663
}
2764

65+
const _observers: CsrfTokenObserver[] = []
2866
/**
2967
* Add an observer which is called when the CSRF token changes
3068
*
3169
* @param observer The observer
3270
*/
3371
export function onRequestTokenUpdate(observer: CsrfTokenObserver): void {
34-
observers.push(observer)
72+
_subscribeToTokenUpdates()
73+
_observers.push(observer)
3574
}
3675

37-
// Listen to server event and keep token in sync
38-
subscribe('csrf-token-update', (e: unknown) => {
39-
token = (e as { token: string }).token
40-
41-
observers.forEach((observer) => {
42-
try {
43-
observer(token!)
44-
} catch (error) {
45-
// we cannot use the logger as the logger uses this library = circular dependency
46-
// eslint-disable-next-line no-console
47-
console.error('Error updating CSRF token observer', error)
76+
let _initialized = false
77+
/**
78+
* Subscribe to token update events from server.
79+
*
80+
* This is legacy and not needed once all supported server versions use `setRequestToken` of this library.
81+
*/
82+
function _subscribeToTokenUpdates(): void {
83+
if (_initialized) {
84+
return
85+
}
86+
87+
_initialized = true
88+
// Listen to server event and keep token in sync
89+
subscribe('csrf-token-update', (event) => {
90+
setRequestToken(event.token)
91+
for (const observer of _observers) {
92+
try {
93+
observer(event.token)
94+
} catch (error) {
95+
// we cannot use the logger as the logger uses this library = circular dependency
96+
// eslint-disable-next-line no-console
97+
console.error('Error updating CSRF token observer', error)
98+
}
4899
}
49100
})
50-
})
101+
}

0 commit comments

Comments
 (0)