-
Notifications
You must be signed in to change notification settings - Fork 214
/
Copy pathapi-token.server.ts
178 lines (151 loc) · 5.14 KB
/
api-token.server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import { Mutex, MutexInterface } from "async-mutex"
import { createApiService } from "~/data/api-service"
import { error, log } from "~/utils/console"
import type { AxiosError } from "axios"
import type { Context, Plugin } from "@nuxt/types"
/* Process level state */
export interface Process {
tokenData: {
accessToken: string
accessTokenExpiry: number
}
tokenFetching: Promise<void>
fetchingMutex: Mutex
}
export declare let process: NodeJS.Process & Process
/**
* Store the plugin's "state" on the `process` to prevent it being
* thrown out in dev mode when the plugin's module
* is mysteriously reloaded (cache-busted) for each request.
*/
process.tokenData = process.tokenData || {
accessToken: "", // '' denotes non-existent key
accessTokenExpiry: 0, // 0 denotes non-existent key
}
process.tokenFetching = process.tokenFetching || Promise.resolve()
/* Token refresh logic */
interface TokenResponse {
access_token: string
expires_in: number
}
/**
* Get the timestamp as the number of seconds from the UNIX epoch.
* @returns the UNIX timestamp with a resolution of one second
*/
const currTimestamp = (): number => Math.floor(Date.now() / 1e3)
export const expiryThreshold = 5 // seconds
/**
* Check whether an access token does not yet exist or if the existing
* access token is set to expire soon.
* @returns whether the stored access token is about to expire
*/
const isNewTokenNeeded = (): boolean => {
if (!process.tokenData.accessToken) {
return true
}
const aboutToExpire =
process.tokenData.accessTokenExpiry - expiryThreshold <= currTimestamp()
return aboutToExpire
}
/**
* Update `tokenData` with the new access token given the client ID and secret.
* @param clientId - the client ID of the application issued by the API
* @param clientSecret - the client secret of the application issued by the API
*/
const refreshApiAccessToken = async (
clientId: string,
clientSecret: string
) => {
const formData = new URLSearchParams()
formData.append("client_id", clientId)
formData.append("client_secret", clientSecret)
formData.append("grant_type", "client_credentials")
const apiService = createApiService()
try {
const res = await apiService.post<TokenResponse>(
"auth_tokens/token",
formData
)
process.tokenData.accessToken = res.data.access_token
process.tokenData.accessTokenExpiry = currTimestamp() + res.data.expires_in
} catch (e) {
/**
* If an error occurs, serve the current request (and any pending)
* anonymously and hope it works. By setting the expiry to 0 we queue
* up another token fetch attempt for the next request.
*/
error("Unable to retrieve API token, clearing existing token", e)
process.tokenData.accessToken = ""
process.tokenData.accessTokenExpiry = 0
;(e as AxiosError).message = `Unable to retrieve API token. ${
(e as AxiosError).message
}`
throw e
}
}
process.fetchingMutex = new Mutex()
/**
* Get an async function that always returns a valid, automatically-refreshed
* API access token.
*
* The `fetchingMutex` allows all requests on the same process to understand
* whether it's necessary for them to request a token refresh or if another
* request has already queued the work. If so, they can just await the process-global
* promise that will resolve when the api token data refresh request has resolved.
*
* @param context - the Nuxt context
*/
const getApiAccessToken = async (
context: Context
): Promise<string | undefined> => {
const { apiClientId, apiClientSecret } = context.$config
if (!(apiClientId || apiClientSecret)) {
return undefined
}
let release: MutexInterface.Releaser | undefined = undefined
// Only request a new token if one is needed _and_ there is
// not already another request making the request (represented
// by the locked mutex).
if (isNewTokenNeeded() && !process.fetchingMutex.isLocked()) {
log("acquiring mutex lock")
release = await process.fetchingMutex.acquire()
log("mutex lock acquired, preparing token refresh request")
process.tokenFetching = refreshApiAccessToken(apiClientId, apiClientSecret)
}
try {
log("awaiting the fetching of the api token to resolve")
await process.tokenFetching
log("done waiting for the token, moving on now...")
} finally {
/**
* Releasing must be in a `finally` block otherwise if the
* tokenFetching promise raises then the mutex will never
* release and subsequent requests will never retry the
* refresh.
*/
if (release) {
log("releasing mutex")
release()
log("mutex released")
}
}
return process.tokenData.accessToken
}
/* Plugin */
declare module "@nuxt/types" {
interface Context {
$openverseApiToken: string
}
}
const apiToken: Plugin = async (context, inject) => {
let openverseApiToken: string | undefined
try {
openverseApiToken = await getApiAccessToken(context)
} catch (e) {
// capture the exception but allow the request to continue with anonymous API requests
context.$sentry.captureException(e)
} finally {
inject("openverseApiToken", openverseApiToken || "")
}
}
export default apiToken