@codingfactory/socialkit-vue 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composables/useAuth.d.ts +27 -0
- package/dist/composables/useAuth.d.ts.map +1 -0
- package/dist/composables/useAuth.js +137 -0
- package/dist/composables/useAuth.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/terminology/index.d.ts +11 -0
- package/dist/plugins/terminology/index.d.ts.map +1 -0
- package/dist/plugins/terminology/index.js +91 -0
- package/dist/plugins/terminology/index.js.map +1 -0
- package/dist/plugins/terminology/terms.d.ts +15 -0
- package/dist/plugins/terminology/terms.d.ts.map +1 -0
- package/dist/plugins/terminology/terms.js +72 -0
- package/dist/plugins/terminology/terms.js.map +1 -0
- package/dist/plugins/terminology/types.d.ts +32 -0
- package/dist/plugins/terminology/types.d.ts.map +1 -0
- package/dist/plugins/terminology/types.js +2 -0
- package/dist/plugins/terminology/types.js.map +1 -0
- package/dist/services/api.d.ts +50 -0
- package/dist/services/api.d.ts.map +1 -0
- package/dist/services/api.js +305 -0
- package/dist/services/api.js.map +1 -0
- package/dist/services/auth.d.ts +127 -0
- package/dist/services/auth.d.ts.map +1 -0
- package/dist/services/auth.js +562 -0
- package/dist/services/auth.js.map +1 -0
- package/dist/stores/auth.d.ts +174 -0
- package/dist/stores/auth.d.ts.map +1 -0
- package/dist/stores/auth.js +262 -0
- package/dist/stores/auth.js.map +1 -0
- package/dist/types/api.d.ts +52 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +7 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/user.d.ts +42 -0
- package/dist/types/user.d.ts.map +1 -0
- package/dist/types/user.js +45 -0
- package/dist/types/user.js.map +1 -0
- package/dist/utils/tokenStorage.d.ts +41 -0
- package/dist/utils/tokenStorage.d.ts.map +1 -0
- package/dist/utils/tokenStorage.js +300 -0
- package/dist/utils/tokenStorage.js.map +1 -0
- package/package.json +40 -0
- package/src/composables/useAuth.ts +164 -0
- package/src/index.ts +118 -0
- package/src/plugins/terminology/index.ts +114 -0
- package/src/plugins/terminology/terms.ts +104 -0
- package/src/plugins/terminology/types.ts +28 -0
- package/src/services/api.ts +472 -0
- package/src/services/auth.ts +874 -0
- package/src/stores/auth.ts +400 -0
- package/src/types/api.ts +56 -0
- package/src/types/user.ts +94 -0
- package/src/utils/tokenStorage.ts +394 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token storage utility for SocialKit-powered frontends.
|
|
3
|
+
*
|
|
4
|
+
* Tokens are cached in memory and persisted to a configurable browser storage
|
|
5
|
+
* backend. Use `createTokenStorage()` to get a configured instance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import axios from 'axios'
|
|
9
|
+
|
|
10
|
+
/** Configuration for creating a token storage instance. */
|
|
11
|
+
export interface TokenStorageConfig {
|
|
12
|
+
/** Storage key for the token (default: 'auth_token'). */
|
|
13
|
+
storageKey?: string
|
|
14
|
+
/** API base URL used by the refresh client (default: '/api'). */
|
|
15
|
+
apiBaseUrl?: string
|
|
16
|
+
/** Endpoints to call when refreshing a token (default: ['/v1/auth/refresh-token']). */
|
|
17
|
+
refreshEndpoints?: string[]
|
|
18
|
+
/** Endpoints where the Authorization header should NOT be sent. */
|
|
19
|
+
anonymousEndpoints?: string[]
|
|
20
|
+
/** When true, persist in localStorage and share across tabs; otherwise sessionStorage. */
|
|
21
|
+
shareSessions?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Public token storage interface. */
|
|
25
|
+
export interface TokenStorage {
|
|
26
|
+
getToken(): string | null
|
|
27
|
+
setToken(token: string): void
|
|
28
|
+
removeToken(): void
|
|
29
|
+
hasToken(): boolean
|
|
30
|
+
shouldSkipAuth(url: string | undefined): boolean
|
|
31
|
+
isRefreshTokenEndpoint(url: string | undefined): boolean
|
|
32
|
+
tryRefreshToken(): Promise<string | null>
|
|
33
|
+
wasTokenRotatedSince(requestAuthHeaderOrHeaders: unknown): boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_STORAGE_KEY = 'auth_token'
|
|
37
|
+
const DEFAULT_REFRESH_ENDPOINTS = ['/v1/auth/refresh-token']
|
|
38
|
+
const DEFAULT_ANONYMOUS_ENDPOINTS = [
|
|
39
|
+
'/sanctum/csrf-cookie',
|
|
40
|
+
'/v1/auth/login',
|
|
41
|
+
'/v1/auth/register',
|
|
42
|
+
'/v1/auth/forgot-password',
|
|
43
|
+
'/v1/auth/reset-password'
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
interface StoragePair {
|
|
47
|
+
primary: Storage | null
|
|
48
|
+
secondary: Storage | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeStoredToken(stored: string | null): string | undefined {
|
|
52
|
+
if (typeof stored === 'string' && stored.length > 0) {
|
|
53
|
+
return stored
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getBrowserStorage(kind: 'local' | 'session'): Storage | null {
|
|
60
|
+
if (typeof window === 'undefined') {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
return kind === 'local' ? window.localStorage : window.sessionStorage
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveStoragePair(shareSessions: boolean): StoragePair {
|
|
72
|
+
if (shareSessions) {
|
|
73
|
+
return {
|
|
74
|
+
primary: getBrowserStorage('local'),
|
|
75
|
+
secondary: getBrowserStorage('session')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
primary: getBrowserStorage('session'),
|
|
81
|
+
secondary: getBrowserStorage('local')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readStorageToken(storage: Storage | null, storageKey: string): string | undefined {
|
|
86
|
+
if (storage === null) {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const stored = storage.getItem(storageKey)
|
|
92
|
+
return normalizeStoredToken(stored)
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeStorageToken(storage: Storage | null, storageKey: string, token: string): void {
|
|
99
|
+
if (storage === null) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
storage.setItem(storageKey, token)
|
|
105
|
+
} catch {
|
|
106
|
+
// Storage full or unavailable — token still works for this session via memory.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function removeStorageToken(storage: Storage | null, storageKey: string): void {
|
|
111
|
+
if (storage === null) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
storage.removeItem(storageKey)
|
|
117
|
+
} catch {
|
|
118
|
+
// Storage unavailable — ignore.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
123
|
+
return typeof value === 'object' && value !== null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractAuthorizationHeaderValue(requestAuthHeaderOrHeaders: unknown): string | null {
|
|
127
|
+
if (typeof requestAuthHeaderOrHeaders === 'string') {
|
|
128
|
+
return requestAuthHeaderOrHeaders
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!isObjectRecord(requestAuthHeaderOrHeaders)) {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const maybeGet = requestAuthHeaderOrHeaders.get
|
|
136
|
+
if (typeof maybeGet === 'function') {
|
|
137
|
+
const uppercaseValue = maybeGet.call(requestAuthHeaderOrHeaders, 'Authorization')
|
|
138
|
+
if (typeof uppercaseValue === 'string' && uppercaseValue.length > 0) {
|
|
139
|
+
return uppercaseValue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const lowercaseValue = maybeGet.call(requestAuthHeaderOrHeaders, 'authorization')
|
|
143
|
+
if (typeof lowercaseValue === 'string' && lowercaseValue.length > 0) {
|
|
144
|
+
return lowercaseValue
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const directUppercase = requestAuthHeaderOrHeaders.Authorization
|
|
149
|
+
if (typeof directUppercase === 'string' && directUppercase.length > 0) {
|
|
150
|
+
return directUppercase
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const directLowercase = requestAuthHeaderOrHeaders.authorization
|
|
154
|
+
if (typeof directLowercase === 'string' && directLowercase.length > 0) {
|
|
155
|
+
return directLowercase
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const [key, value] of Object.entries(requestAuthHeaderOrHeaders)) {
|
|
159
|
+
if (key.toLowerCase() !== 'authorization') {
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
164
|
+
return value
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract a token string from various API response shapes.
|
|
173
|
+
*
|
|
174
|
+
* Handles:
|
|
175
|
+
* - `{ token: string }`
|
|
176
|
+
* - `{ data: { token: string } }`
|
|
177
|
+
*/
|
|
178
|
+
export function extractTokenFromResponse(payload: unknown): string | undefined {
|
|
179
|
+
if (!payload || typeof payload !== 'object') {
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if ('token' in payload) {
|
|
184
|
+
const token = (payload as { token?: unknown }).token
|
|
185
|
+
if (typeof token === 'string' && token.length > 0) {
|
|
186
|
+
return token
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if ('data' in payload) {
|
|
191
|
+
return extractTokenFromResponse((payload as { data?: unknown }).data)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return undefined
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Create a configured token storage instance. */
|
|
198
|
+
export function createTokenStorage(config: TokenStorageConfig = {}): TokenStorage {
|
|
199
|
+
const storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY
|
|
200
|
+
const refreshEndpoints = config.refreshEndpoints ?? DEFAULT_REFRESH_ENDPOINTS
|
|
201
|
+
const anonymousEndpoints = config.anonymousEndpoints ?? DEFAULT_ANONYMOUS_ENDPOINTS
|
|
202
|
+
const apiBaseUrl = config.apiBaseUrl ?? '/api'
|
|
203
|
+
const shareSessions = config.shareSessions ?? false
|
|
204
|
+
const storagePair = resolveStoragePair(shareSessions)
|
|
205
|
+
|
|
206
|
+
let inMemoryToken: string | undefined
|
|
207
|
+
let refreshPromise: Promise<string | null> | null = null
|
|
208
|
+
|
|
209
|
+
function readPersistedToken(): string | undefined {
|
|
210
|
+
const primaryToken = readStorageToken(storagePair.primary, storageKey)
|
|
211
|
+
const secondaryToken = readStorageToken(storagePair.secondary, storageKey)
|
|
212
|
+
|
|
213
|
+
if (secondaryToken !== undefined) {
|
|
214
|
+
if (primaryToken === undefined) {
|
|
215
|
+
writeStorageToken(storagePair.primary, storageKey, secondaryToken)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
removeStorageToken(storagePair.secondary, storageKey)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return primaryToken ?? secondaryToken
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function persistToken(token: string): void {
|
|
225
|
+
writeStorageToken(storagePair.primary, storageKey, token)
|
|
226
|
+
removeStorageToken(storagePair.secondary, storageKey)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function clearPersistedTokens(): void {
|
|
230
|
+
removeStorageToken(getBrowserStorage('local'), storageKey)
|
|
231
|
+
removeStorageToken(getBrowserStorage('session'), storageKey)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function syncInMemoryTokenFromStorage(): void {
|
|
235
|
+
inMemoryToken = readPersistedToken()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
inMemoryToken = readPersistedToken()
|
|
239
|
+
|
|
240
|
+
if (typeof window !== 'undefined') {
|
|
241
|
+
window.addEventListener('storage', (event: StorageEvent) => {
|
|
242
|
+
const localStorageRef = getBrowserStorage('local')
|
|
243
|
+
const sessionStorageRef = getBrowserStorage('session')
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
event.storageArea &&
|
|
247
|
+
event.storageArea !== localStorageRef &&
|
|
248
|
+
event.storageArea !== sessionStorageRef
|
|
249
|
+
) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (event.key === storageKey) {
|
|
254
|
+
inMemoryToken = normalizeStoredToken(event.newValue)
|
|
255
|
+
|
|
256
|
+
if (inMemoryToken === undefined) {
|
|
257
|
+
clearPersistedTokens()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (event.key !== null) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
syncInMemoryTokenFromStorage()
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const refreshClient = axios.create({
|
|
272
|
+
baseURL: apiBaseUrl,
|
|
273
|
+
timeout: 30000,
|
|
274
|
+
headers: {
|
|
275
|
+
'Content-Type': 'application/json',
|
|
276
|
+
Accept: 'application/json'
|
|
277
|
+
},
|
|
278
|
+
withCredentials: false
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
function getToken(): string | null {
|
|
282
|
+
if (inMemoryToken !== undefined) {
|
|
283
|
+
return inMemoryToken
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const persistedToken = readPersistedToken()
|
|
287
|
+
if (persistedToken !== undefined) {
|
|
288
|
+
inMemoryToken = persistedToken
|
|
289
|
+
return persistedToken
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function setToken(token: string): void {
|
|
296
|
+
inMemoryToken = token
|
|
297
|
+
persistToken(token)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function removeToken(): void {
|
|
301
|
+
inMemoryToken = undefined
|
|
302
|
+
clearPersistedTokens()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function hasToken(): boolean {
|
|
306
|
+
return getToken() !== null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function shouldSkipAuth(url: string | undefined): boolean {
|
|
310
|
+
if (!url) {
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return anonymousEndpoints.some((endpoint) => url.includes(endpoint))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isRefreshTokenEndpoint(url: string | undefined): boolean {
|
|
318
|
+
if (!url) {
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return refreshEndpoints.some((endpoint) => url.includes(endpoint))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function requestTokenRefresh(): Promise<string | null> {
|
|
326
|
+
const existingToken = getToken()
|
|
327
|
+
if (!existingToken) {
|
|
328
|
+
return null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const endpoint of refreshEndpoints) {
|
|
332
|
+
try {
|
|
333
|
+
const response = await refreshClient.post(endpoint, {}, {
|
|
334
|
+
headers: {
|
|
335
|
+
Authorization: `Bearer ${existingToken}`
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const refreshedToken = extractTokenFromResponse(response.data)
|
|
340
|
+
if (refreshedToken) {
|
|
341
|
+
setToken(refreshedToken)
|
|
342
|
+
return refreshedToken
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
// Try the next known refresh endpoint.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const latestPersistedToken = readPersistedToken()
|
|
350
|
+
if (latestPersistedToken && latestPersistedToken !== existingToken) {
|
|
351
|
+
inMemoryToken = latestPersistedToken
|
|
352
|
+
return latestPersistedToken
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function tryRefreshToken(): Promise<string | null> {
|
|
359
|
+
if (refreshPromise) {
|
|
360
|
+
return refreshPromise
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
refreshPromise = requestTokenRefresh()
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
return await refreshPromise
|
|
367
|
+
} finally {
|
|
368
|
+
refreshPromise = null
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function wasTokenRotatedSince(requestAuthHeaderOrHeaders: unknown): boolean {
|
|
373
|
+
const requestAuthHeader = extractAuthorizationHeaderValue(requestAuthHeaderOrHeaders)
|
|
374
|
+
if (requestAuthHeader === null || !requestAuthHeader.startsWith('Bearer ')) {
|
|
375
|
+
return false
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const requestToken = requestAuthHeader.slice(7)
|
|
379
|
+
const currentToken = getToken()
|
|
380
|
+
|
|
381
|
+
return requestToken.length > 0 && currentToken !== null && currentToken !== requestToken
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
getToken,
|
|
386
|
+
setToken,
|
|
387
|
+
removeToken,
|
|
388
|
+
hasToken,
|
|
389
|
+
shouldSkipAuth,
|
|
390
|
+
isRefreshTokenEndpoint,
|
|
391
|
+
tryRefreshToken,
|
|
392
|
+
wasTokenRotatedSince
|
|
393
|
+
}
|
|
394
|
+
}
|