@ametie/vue-muza-use 0.4.9 → 0.4.10
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/README.md +320 -551
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,114 +40,10 @@ yarn add @ametie/vue-muza-use axios
|
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
## 🔐 Token Management
|
|
44
|
-
|
|
45
|
-
The library supports **automatic token refresh** with two security modes:
|
|
46
|
-
|
|
47
|
-
### Mode 1: localStorage (Default)
|
|
48
|
-
```typescript
|
|
49
|
-
const api = createApiClient({
|
|
50
|
-
baseURL: 'https://api.example.com',
|
|
51
|
-
authOptions: {
|
|
52
|
-
refreshUrl: '/auth/refresh',
|
|
53
|
-
refreshWithCredentials: false, // or omit (default)
|
|
54
|
-
onTokenRefreshFailed: () => router.push('/login')
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
```
|
|
58
|
-
- ✅ Both `accessToken` and `refreshToken` stored in localStorage
|
|
59
|
-
- ⚠️ Less secure (vulnerable to XSS), but easier to implement
|
|
60
|
-
|
|
61
|
-
### Mode 2: httpOnly Cookies (Recommended for Production)
|
|
62
|
-
```typescript
|
|
63
|
-
const api = createApiClient({
|
|
64
|
-
baseURL: 'https://api.example.com',
|
|
65
|
-
authOptions: {
|
|
66
|
-
refreshUrl: '/auth/refresh',
|
|
67
|
-
refreshWithCredentials: true, // 🔑 Key parameter!
|
|
68
|
-
onTokenRefreshFailed: () => router.push('/login')
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
```
|
|
72
|
-
- ✅ Only `accessToken` in localStorage
|
|
73
|
-
- 🔒 `refreshToken` sent via httpOnly cookie (XSS protection)
|
|
74
|
-
- 📡 Backend must set: `Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=None` (for cross-origin)
|
|
75
|
-
- 🎯 **Smart Auto-Detection**: If no `refreshToken` in localStorage, `withCredentials: true` is automatically enabled
|
|
76
|
-
|
|
77
|
-
**⚠️ Common Issues & Solutions:**
|
|
78
|
-
|
|
79
|
-
1. **Cookie not sent to backend?**
|
|
80
|
-
- Check: Browser DevTools → Application → Cookies
|
|
81
|
-
- Cookie must have: `HttpOnly`, `Secure` (for HTTPS), `SameSite=None` (for cross-origin)
|
|
82
|
-
- Cookie domain must match your request domain
|
|
83
|
-
|
|
84
|
-
2. **CORS errors?**
|
|
85
|
-
- Backend must set: `Access-Control-Allow-Credentials: true`
|
|
86
|
-
- Backend must set specific origin (NOT `*`): `Access-Control-Allow-Origin: https://your-frontend.com`
|
|
87
|
-
- Frontend baseURL must match backend domain
|
|
88
|
-
|
|
89
|
-
3. **401 on refresh?**
|
|
90
|
-
- Check Network tab → Refresh request → Headers
|
|
91
|
-
- Verify "Cookie" header includes your refresh token
|
|
92
|
-
- Backend logs: does it receive the cookie?
|
|
93
|
-
|
|
94
|
-
### Custom Refresh Payload
|
|
95
|
-
Send additional data with refresh requests:
|
|
96
|
-
|
|
97
|
-
**⚠️ IMPORTANT:** Use functions for dynamic data (like tokens from storage):
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
const api = createApiClient({
|
|
101
|
-
baseURL: 'https://api.example.com',
|
|
102
|
-
authOptions: {
|
|
103
|
-
refreshUrl: '/auth/refresh',
|
|
104
|
-
// ❌ WRONG - computed once at app start
|
|
105
|
-
// refreshPayload: { refreshToken: tokenManager.getRefreshToken() }
|
|
106
|
-
|
|
107
|
-
// ✅ CORRECT - computed on each refresh
|
|
108
|
-
refreshPayload: () => ({
|
|
109
|
-
refreshToken: tokenManager.getRefreshToken(),
|
|
110
|
-
timestamp: Date.now()
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Static payload (for constants only):
|
|
117
|
-
```typescript
|
|
118
|
-
refreshPayload: {
|
|
119
|
-
deviceId: 'mobile-v1',
|
|
120
|
-
platform: 'ios' // constants that don't change
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Dynamic function:
|
|
125
|
-
```typescript
|
|
126
|
-
refreshPayload: () => ({
|
|
127
|
-
timestamp: Date.now(),
|
|
128
|
-
sessionId: getSessionId() // reads current value
|
|
129
|
-
})
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Saving Tokens After Login
|
|
133
|
-
```typescript
|
|
134
|
-
import { tokenManager } from '@ametie/vue-muza-use'
|
|
135
|
-
|
|
136
|
-
const { execute } = useApiPost<AuthResponse>('/auth/login', {
|
|
137
|
-
authMode: 'public',
|
|
138
|
-
onSuccess(response) {
|
|
139
|
-
tokenManager.setTokens({
|
|
140
|
-
accessToken: response.data.accessToken,
|
|
141
|
-
refreshToken: response.data.refreshToken, // optional in cookie mode
|
|
142
|
-
expiresIn: response.data.expiresIn
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
```
|
|
147
|
-
---
|
|
148
|
-
|
|
149
43
|
## 🚀 Quick Start
|
|
150
44
|
|
|
45
|
+
Get started in 2 minutes with minimal configuration.
|
|
46
|
+
|
|
151
47
|
### 1. Setup Plugin (`main.ts`)
|
|
152
48
|
|
|
153
49
|
```typescript
|
|
@@ -157,30 +53,20 @@ import App from './App.vue'
|
|
|
157
53
|
|
|
158
54
|
const app = createApp(App)
|
|
159
55
|
|
|
160
|
-
// Create
|
|
56
|
+
// Create API client with minimal config
|
|
161
57
|
const api = createApiClient({
|
|
162
|
-
baseURL: 'https://api.example.com'
|
|
163
|
-
withAuth: true, // Automatic token injection
|
|
164
|
-
authOptions: {
|
|
165
|
-
refreshUrl: '/auth/refresh',
|
|
166
|
-
onTokenRefreshFailed: () => {
|
|
167
|
-
window.location.href = '/login'
|
|
168
|
-
}
|
|
169
|
-
}
|
|
58
|
+
baseURL: 'https://api.example.com'
|
|
170
59
|
})
|
|
171
60
|
|
|
172
61
|
// Install plugin
|
|
173
|
-
app.use(createApi({
|
|
174
|
-
axios: api,
|
|
175
|
-
onError: (error) => {
|
|
176
|
-
console.error('API Error:', error.message)
|
|
177
|
-
}
|
|
178
|
-
}))
|
|
62
|
+
app.use(createApi({ axios: api }))
|
|
179
63
|
|
|
180
64
|
app.mount('#app')
|
|
181
65
|
```
|
|
182
66
|
|
|
183
|
-
|
|
67
|
+
> 💡 **That's it!** No auth configuration needed to get started. Add it later when you need it.
|
|
68
|
+
|
|
69
|
+
### 2. Your First Request
|
|
184
70
|
|
|
185
71
|
```vue
|
|
186
72
|
<script setup lang="ts">
|
|
@@ -193,7 +79,7 @@ interface User {
|
|
|
193
79
|
}
|
|
194
80
|
|
|
195
81
|
const { data, loading, error } = useApi<User>('/users/1', {
|
|
196
|
-
immediate: true
|
|
82
|
+
immediate: true // Auto-fetch on mount
|
|
197
83
|
})
|
|
198
84
|
</script>
|
|
199
85
|
|
|
@@ -207,29 +93,23 @@ const { data, loading, error } = useApi<User>('/users/1', {
|
|
|
207
93
|
</template>
|
|
208
94
|
```
|
|
209
95
|
|
|
210
|
-
### 3.
|
|
96
|
+
### 3. Live Search with Debounce
|
|
211
97
|
|
|
212
|
-
This
|
|
98
|
+
This example shows the library's power: **automatic race condition handling** and **debouncing** built-in.
|
|
213
99
|
|
|
214
100
|
```vue
|
|
215
101
|
<script setup lang="ts">
|
|
216
102
|
import { ref } from 'vue'
|
|
217
103
|
import { useApi } from '@ametie/vue-muza-use'
|
|
218
104
|
|
|
219
|
-
interface Product {
|
|
220
|
-
id: number
|
|
221
|
-
name: string
|
|
222
|
-
price: number
|
|
223
|
-
}
|
|
224
|
-
|
|
225
105
|
const searchQuery = ref('')
|
|
226
106
|
|
|
227
|
-
// 💡
|
|
228
|
-
const { data, loading } = useApi
|
|
107
|
+
// 💡 Use a getter function for dynamic URLs
|
|
108
|
+
const { data, loading } = useApi(
|
|
229
109
|
() => `/products/search?q=${searchQuery.value}`,
|
|
230
110
|
{
|
|
231
|
-
watch: searchQuery,
|
|
232
|
-
debounce: 500
|
|
111
|
+
watch: searchQuery, // Auto-refetch when query changes
|
|
112
|
+
debounce: 500 // Wait 500ms after typing stops
|
|
233
113
|
}
|
|
234
114
|
)
|
|
235
115
|
</script>
|
|
@@ -247,274 +127,145 @@ const { data, loading } = useApi<Product[]>(
|
|
|
247
127
|
</template>
|
|
248
128
|
```
|
|
249
129
|
|
|
130
|
+
**🎯 What just happened?**
|
|
131
|
+
- No race conditions — previous searches auto-cancel
|
|
132
|
+
- Debounce — waits for user to stop typing
|
|
133
|
+
- TypeScript — full type safety
|
|
134
|
+
- Clean code — no manual cleanup needed
|
|
135
|
+
|
|
250
136
|
---
|
|
251
137
|
|
|
252
|
-
## 📖
|
|
138
|
+
## 📖 Basic Usage
|
|
253
139
|
|
|
254
140
|
### GET Requests
|
|
255
141
|
|
|
256
|
-
####
|
|
142
|
+
#### Manual Execution
|
|
257
143
|
```typescript
|
|
258
144
|
const { data, loading, error, execute } = useApi<User>('/users/1')
|
|
259
145
|
|
|
260
|
-
//
|
|
146
|
+
// Trigger manually (e.g., on button click)
|
|
261
147
|
await execute()
|
|
262
148
|
```
|
|
263
149
|
|
|
264
150
|
#### Auto-Fetch on Mount
|
|
265
151
|
```typescript
|
|
266
|
-
useApi<User>('/users/1', {
|
|
267
|
-
immediate: true
|
|
268
|
-
retry: 3, // Retry 3 times on network failure
|
|
269
|
-
retryDelay: 1000 // Wait 1s between retries
|
|
152
|
+
const { data } = useApi<User>('/users/1', {
|
|
153
|
+
immediate: true // Fetches automatically
|
|
270
154
|
})
|
|
271
155
|
```
|
|
272
156
|
|
|
273
|
-
####
|
|
274
|
-
Use a **getter function** for reactive URLs - simple and efficient:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
const userId = ref(1)
|
|
278
|
-
|
|
279
|
-
// ✅ Preferred: Getter function (no computed needed!)
|
|
280
|
-
const { data } = useApi(() => `/users/${userId.value}`, {
|
|
281
|
-
watch: userId,
|
|
282
|
-
immediate: true
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
// Also works: computed ref
|
|
286
|
-
const url = computed(() => `/users/${userId.value}`)
|
|
287
|
-
const { data } = useApi(url, { watch: userId, immediate: true })
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
#### Query Parameters
|
|
291
|
-
Pass `params` as static object or reactive ref:
|
|
292
|
-
|
|
157
|
+
#### With Query Parameters
|
|
293
158
|
```typescript
|
|
294
159
|
const filters = ref({
|
|
295
160
|
status: 'active',
|
|
296
|
-
sort: 'name',
|
|
297
161
|
limit: 20
|
|
298
162
|
})
|
|
299
163
|
|
|
300
164
|
const { data } = useApi('/users', {
|
|
301
|
-
params: filters,
|
|
302
|
-
watch: filters,
|
|
303
|
-
|
|
165
|
+
params: filters, // Automatically unwrapped
|
|
166
|
+
watch: filters, // Re-fetch when filters change
|
|
167
|
+
immediate: true
|
|
304
168
|
})
|
|
305
|
-
|
|
306
|
-
// URL becomes: /users?status=active&sort=name&limit=20
|
|
307
169
|
```
|
|
308
170
|
|
|
309
171
|
---
|
|
310
172
|
|
|
311
173
|
### POST/PUT/PATCH Requests
|
|
312
174
|
|
|
313
|
-
#### Form Submission
|
|
175
|
+
#### Simple Form Submission
|
|
314
176
|
```vue
|
|
315
177
|
<script setup lang="ts">
|
|
316
178
|
import { ref } from 'vue'
|
|
317
179
|
import { useApi } from '@ametie/vue-muza-use'
|
|
318
180
|
|
|
319
|
-
|
|
320
|
-
email: string
|
|
321
|
-
password: string
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
interface LoginResponse {
|
|
325
|
-
token: string
|
|
326
|
-
user: { id: number; name: string }
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const form = ref<LoginForm>({
|
|
181
|
+
const form = ref({
|
|
330
182
|
email: '',
|
|
331
183
|
password: ''
|
|
332
184
|
})
|
|
333
185
|
|
|
334
|
-
const { execute, loading, error } = useApi
|
|
186
|
+
const { execute, loading, error } = useApi('/auth/login', {
|
|
335
187
|
method: 'POST',
|
|
336
|
-
|
|
337
|
-
data: form, // Ref is auto-unwrapped
|
|
188
|
+
data: form, // Ref is auto-unwrapped
|
|
338
189
|
onSuccess: (response) => {
|
|
339
|
-
|
|
340
|
-
router.push('/dashboard')
|
|
190
|
+
console.log('Logged in!', response.data)
|
|
341
191
|
}
|
|
342
192
|
})
|
|
343
|
-
|
|
344
|
-
const handleSubmit = async () => {
|
|
345
|
-
await execute()
|
|
346
|
-
}
|
|
347
193
|
</script>
|
|
348
194
|
|
|
349
195
|
<template>
|
|
350
|
-
<form @submit.prevent="
|
|
351
|
-
<input v-model="form.email" type="email"
|
|
352
|
-
<input v-model="form.password" type="password"
|
|
353
|
-
|
|
354
|
-
<button type="submit" :disabled="loading">
|
|
196
|
+
<form @submit.prevent="execute">
|
|
197
|
+
<input v-model="form.email" type="email" />
|
|
198
|
+
<input v-model="form.password" type="password" />
|
|
199
|
+
<button :disabled="loading">
|
|
355
200
|
{{ loading ? 'Signing in...' : 'Sign In' }}
|
|
356
201
|
</button>
|
|
357
|
-
|
|
358
|
-
<p v-if="error" class="error">{{ error.message }}</p>
|
|
202
|
+
<p v-if="error">{{ error.message }}</p>
|
|
359
203
|
</form>
|
|
360
204
|
</template>
|
|
361
205
|
```
|
|
362
206
|
|
|
363
|
-
#### Update with Optimistic UI
|
|
364
|
-
```typescript
|
|
365
|
-
const { execute: updateProfile } = useApi('/user/profile', {
|
|
366
|
-
method: 'PUT',
|
|
367
|
-
onBefore: () => {
|
|
368
|
-
// Show optimistic update
|
|
369
|
-
localProfile.value = { ...localProfile.value, ...changes }
|
|
370
|
-
},
|
|
371
|
-
onSuccess: (response) => {
|
|
372
|
-
toast.success('Profile updated!')
|
|
373
|
-
},
|
|
374
|
-
onError: () => {
|
|
375
|
-
// Rollback on error
|
|
376
|
-
localProfile.value = originalProfile
|
|
377
|
-
}
|
|
378
|
-
})
|
|
379
|
-
```
|
|
380
|
-
|
|
381
207
|
---
|
|
382
208
|
|
|
383
|
-
|
|
209
|
+
## 🎯 Core Features
|
|
384
210
|
|
|
385
|
-
Watch
|
|
211
|
+
### Watch & Auto-Refetch
|
|
386
212
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
213
|
+
Watch refs and automatically refetch when they change. Perfect for filters, search, and dynamic content.
|
|
214
|
+
|
|
215
|
+
#### Single Dependency
|
|
216
|
+
```typescript
|
|
217
|
+
const userId = ref(1)
|
|
218
|
+
|
|
219
|
+
const { data } = useApi(() => `/users/${userId.value}`, {
|
|
220
|
+
watch: userId,
|
|
221
|
+
immediate: true
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Change userId → automatic refetch
|
|
225
|
+
userId.value = 2
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### Multiple Dependencies
|
|
229
|
+
```typescript
|
|
390
230
|
const searchQuery = ref('')
|
|
391
231
|
const category = ref('all')
|
|
392
232
|
|
|
393
|
-
|
|
394
|
-
const { data, loading } = useApi<Product[]>(
|
|
233
|
+
const { data } = useApi(
|
|
395
234
|
() => `/products?q=${searchQuery.value}&category=${category.value}`,
|
|
396
235
|
{
|
|
397
|
-
watch: [searchQuery, category],
|
|
398
|
-
debounce: 500
|
|
236
|
+
watch: [searchQuery, category],
|
|
237
|
+
debounce: 500
|
|
399
238
|
}
|
|
400
239
|
)
|
|
401
|
-
</script>
|
|
402
|
-
|
|
403
|
-
<template>
|
|
404
|
-
<input v-model="searchQuery" placeholder="Search..." />
|
|
405
|
-
<select v-model="category">
|
|
406
|
-
<option value="all">All</option>
|
|
407
|
-
<option value="electronics">Electronics</option>
|
|
408
|
-
</select>
|
|
409
|
-
|
|
410
|
-
<ProductList :products="data" :loading="loading" />
|
|
411
|
-
</template>
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
#### Data Table with Pagination & Sorting
|
|
415
|
-
```vue
|
|
416
|
-
<script setup lang="ts">
|
|
417
|
-
interface PaginatedResponse<T> {
|
|
418
|
-
data: T[]
|
|
419
|
-
total: number
|
|
420
|
-
page: number
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const page = ref(1)
|
|
424
|
-
const sortBy = ref('created_at')
|
|
425
|
-
const sortOrder = ref<'asc' | 'desc'>('desc')
|
|
426
|
-
|
|
427
|
-
const params = computed(() => ({
|
|
428
|
-
page: page.value,
|
|
429
|
-
sort_by: sortBy.value,
|
|
430
|
-
sort_order: sortOrder.value,
|
|
431
|
-
per_page: 20
|
|
432
|
-
}))
|
|
433
|
-
|
|
434
|
-
const { data, loading } = useApi<PaginatedResponse<Order>>('/orders', {
|
|
435
|
-
params,
|
|
436
|
-
watch: params, // Auto-refetch when any param changes
|
|
437
|
-
immediate: true
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
const handleSort = (column: string) => {
|
|
441
|
-
if (sortBy.value === column) {
|
|
442
|
-
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
|
|
443
|
-
} else {
|
|
444
|
-
sortBy.value = column
|
|
445
|
-
sortOrder.value = 'desc'
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
</script>
|
|
449
|
-
|
|
450
|
-
<template>
|
|
451
|
-
<table>
|
|
452
|
-
<thead>
|
|
453
|
-
<tr>
|
|
454
|
-
<th @click="handleSort('id')">ID</th>
|
|
455
|
-
<th @click="handleSort('created_at')">Date</th>
|
|
456
|
-
<th @click="handleSort('total')">Total</th>
|
|
457
|
-
</tr>
|
|
458
|
-
</thead>
|
|
459
|
-
<tbody v-if="!loading">
|
|
460
|
-
<tr v-for="order in data?.data" :key="order.id">
|
|
461
|
-
<td>{{ order.id }}</td>
|
|
462
|
-
<td>{{ order.created_at }}</td>
|
|
463
|
-
<td>${{ order.total }}</td>
|
|
464
|
-
</tr>
|
|
465
|
-
</tbody>
|
|
466
|
-
</table>
|
|
467
|
-
|
|
468
|
-
<Pagination
|
|
469
|
-
v-model="page"
|
|
470
|
-
:total="data?.total"
|
|
471
|
-
:loading="loading"
|
|
472
|
-
/>
|
|
473
|
-
</template>
|
|
474
240
|
```
|
|
475
241
|
|
|
476
242
|
#### Auto-Save Form
|
|
477
243
|
```typescript
|
|
478
244
|
const settings = ref({
|
|
479
245
|
theme: 'dark',
|
|
480
|
-
notifications: true
|
|
481
|
-
language: 'en'
|
|
246
|
+
notifications: true
|
|
482
247
|
})
|
|
483
248
|
|
|
484
249
|
useApi('/user/settings', {
|
|
485
250
|
method: 'PUT',
|
|
486
251
|
data: settings,
|
|
487
|
-
watch: settings,
|
|
488
|
-
debounce: 1000,
|
|
489
|
-
onSuccess: () =>
|
|
490
|
-
toast.success('Settings saved')
|
|
491
|
-
}
|
|
252
|
+
watch: settings, // Deep watch by default
|
|
253
|
+
debounce: 1000, // Save 1s after changes stop
|
|
254
|
+
onSuccess: () => toast.success('Saved!')
|
|
492
255
|
})
|
|
493
256
|
```
|
|
494
257
|
|
|
495
258
|
---
|
|
496
259
|
|
|
497
|
-
###
|
|
260
|
+
### Polling (Background Updates)
|
|
498
261
|
|
|
499
|
-
Keep data fresh with
|
|
262
|
+
Keep data fresh with smart polling. Automatically pauses when browser tab is hidden.
|
|
500
263
|
|
|
501
264
|
#### Simple Polling
|
|
502
265
|
```typescript
|
|
503
|
-
|
|
504
|
-
const { data } = useApi<Notification[]>('/notifications', {
|
|
266
|
+
const { data } = useApi('/notifications', {
|
|
505
267
|
immediate: true,
|
|
506
|
-
poll: 5000
|
|
507
|
-
})
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
#### Smart Polling with Controls
|
|
511
|
-
```typescript
|
|
512
|
-
const { data, loading } = useApi('/stock-prices', {
|
|
513
|
-
immediate: true,
|
|
514
|
-
poll: {
|
|
515
|
-
interval: 1000,
|
|
516
|
-
whenHidden: false // ⚠️ Pause when tab is hidden (default)
|
|
517
|
-
}
|
|
268
|
+
poll: 5000 // Fetch every 5 seconds
|
|
518
269
|
})
|
|
519
270
|
```
|
|
520
271
|
|
|
@@ -522,71 +273,162 @@ const { data, loading } = useApi('/stock-prices', {
|
|
|
522
273
|
```typescript
|
|
523
274
|
const pollInterval = ref(3000)
|
|
524
275
|
|
|
525
|
-
// Start/stop polling by changing the ref
|
|
526
276
|
const { data } = useApi('/live-feed', {
|
|
527
277
|
poll: pollInterval,
|
|
528
278
|
immediate: true
|
|
529
279
|
})
|
|
530
280
|
|
|
531
281
|
// Stop polling
|
|
532
|
-
|
|
282
|
+
pollInterval.value = 0
|
|
533
283
|
|
|
534
|
-
// Resume with
|
|
535
|
-
|
|
284
|
+
// Resume with different interval
|
|
285
|
+
pollInterval.value = 5000
|
|
536
286
|
```
|
|
537
287
|
|
|
538
288
|
---
|
|
539
289
|
|
|
540
|
-
###
|
|
290
|
+
### Error Handling
|
|
541
291
|
|
|
542
|
-
####
|
|
543
|
-
|
|
292
|
+
#### Per-Request Error Handling
|
|
293
|
+
```typescript
|
|
294
|
+
const { error, execute } = useApi('/users', {
|
|
295
|
+
onError: (error) => {
|
|
296
|
+
if (error.status === 404) {
|
|
297
|
+
toast.error('User not found')
|
|
298
|
+
} else {
|
|
299
|
+
toast.error('Something went wrong')
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
skipErrorNotification: true // Skip global handler
|
|
303
|
+
})
|
|
304
|
+
```
|
|
544
305
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
306
|
+
#### Retry on Failure
|
|
307
|
+
```typescript
|
|
308
|
+
useApi('/flaky-endpoint', {
|
|
309
|
+
immediate: true,
|
|
310
|
+
retry: 3, // Retry 3 times
|
|
311
|
+
retryDelay: 1000 // Wait 1s between retries
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
548
316
|
|
|
549
|
-
|
|
317
|
+
### Loading States
|
|
550
318
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
})
|
|
319
|
+
#### Per-Request Loading
|
|
320
|
+
```typescript
|
|
321
|
+
const { data: user, loading: userLoading } = useApi('/user')
|
|
322
|
+
const { data: posts, loading: postsLoading } = useApi('/posts')
|
|
555
323
|
|
|
556
|
-
|
|
557
|
-
|
|
324
|
+
// Each request tracks its own loading state
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### Lifecycle Hooks
|
|
328
|
+
```typescript
|
|
329
|
+
const { execute } = useApi('/analytics', {
|
|
330
|
+
onBefore: () => {
|
|
331
|
+
loadingBar.start()
|
|
332
|
+
},
|
|
333
|
+
onSuccess: (response) => {
|
|
334
|
+
console.log('Success!', response.data)
|
|
335
|
+
},
|
|
336
|
+
onError: (error) => {
|
|
337
|
+
console.error('Failed:', error.message)
|
|
338
|
+
},
|
|
339
|
+
onFinish: () => {
|
|
340
|
+
loadingBar.finish() // Always called
|
|
341
|
+
}
|
|
558
342
|
})
|
|
343
|
+
```
|
|
559
344
|
|
|
560
|
-
|
|
345
|
+
---
|
|
561
346
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
347
|
+
## 📊 Real-World Examples
|
|
348
|
+
|
|
349
|
+
### Data Table with Pagination & Sorting
|
|
350
|
+
```vue
|
|
351
|
+
<script setup lang="ts">
|
|
352
|
+
const page = ref(1)
|
|
353
|
+
const sortBy = ref('created_at')
|
|
354
|
+
const sortOrder = ref<'asc' | 'desc'>('desc')
|
|
355
|
+
|
|
356
|
+
const params = computed(() => ({
|
|
357
|
+
page: page.value,
|
|
358
|
+
sort_by: sortBy.value,
|
|
359
|
+
sort_order: sortOrder.value,
|
|
360
|
+
per_page: 20
|
|
361
|
+
}))
|
|
566
362
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
363
|
+
const { data, loading } = useApi('/orders', {
|
|
364
|
+
params,
|
|
365
|
+
watch: params,
|
|
366
|
+
immediate: true
|
|
570
367
|
})
|
|
571
368
|
</script>
|
|
369
|
+
|
|
370
|
+
<template>
|
|
371
|
+
<table>
|
|
372
|
+
<thead>
|
|
373
|
+
<tr>
|
|
374
|
+
<th @click="sortBy = 'id'">ID</th>
|
|
375
|
+
<th @click="sortBy = 'created_at'">Date</th>
|
|
376
|
+
<th @click="sortBy = 'total'">Total</th>
|
|
377
|
+
</tr>
|
|
378
|
+
</thead>
|
|
379
|
+
<tbody v-if="!loading">
|
|
380
|
+
<tr v-for="order in data?.data" :key="order.id">
|
|
381
|
+
<td>{{ order.id }}</td>
|
|
382
|
+
<td>{{ order.created_at }}</td>
|
|
383
|
+
<td>${{ order.total }}</td>
|
|
384
|
+
</tr>
|
|
385
|
+
</tbody>
|
|
386
|
+
</table>
|
|
387
|
+
|
|
388
|
+
<Pagination v-model="page" :total="data?.total" />
|
|
389
|
+
</template>
|
|
572
390
|
```
|
|
573
391
|
|
|
574
|
-
|
|
392
|
+
### Optimistic UI Updates
|
|
575
393
|
```typescript
|
|
576
|
-
const { execute
|
|
394
|
+
const { execute: updateProfile } = useApi('/user/profile', {
|
|
395
|
+
method: 'PUT',
|
|
396
|
+
onBefore: () => {
|
|
397
|
+
// Show update immediately
|
|
398
|
+
localProfile.value = { ...localProfile.value, ...changes }
|
|
399
|
+
},
|
|
400
|
+
onError: () => {
|
|
401
|
+
// Rollback on error
|
|
402
|
+
localProfile.value = originalProfile
|
|
403
|
+
toast.error('Update failed')
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
```
|
|
577
407
|
|
|
578
|
-
|
|
579
|
-
|
|
408
|
+
### Request Cancellation
|
|
409
|
+
```typescript
|
|
410
|
+
import { useAbortController } from '@ametie/vue-muza-use'
|
|
411
|
+
|
|
412
|
+
const { abortAll } = useAbortController()
|
|
413
|
+
|
|
414
|
+
// Multiple requests
|
|
415
|
+
const { data: products } = useApi('/products', { params: filters })
|
|
416
|
+
const { data: stats } = useApi('/stats', { params: filters })
|
|
580
417
|
|
|
581
|
-
// Cancel
|
|
582
|
-
|
|
418
|
+
// Cancel all when filters reset
|
|
419
|
+
const resetFilters = () => {
|
|
420
|
+
abortAll() // 🛑 Cancel both requests
|
|
421
|
+
filters.value = { /* defaults */ }
|
|
422
|
+
}
|
|
583
423
|
```
|
|
584
424
|
|
|
425
|
+
---
|
|
426
|
+
|
|
585
427
|
## ⚙️ Advanced Configuration
|
|
586
428
|
|
|
587
429
|
### Custom Axios Instance
|
|
588
430
|
|
|
589
|
-
|
|
431
|
+
Full control over Axios configuration:
|
|
590
432
|
|
|
591
433
|
```typescript
|
|
592
434
|
import axios from 'axios'
|
|
@@ -595,14 +437,12 @@ import { createApi } from '@ametie/vue-muza-use'
|
|
|
595
437
|
const customAxios = axios.create({
|
|
596
438
|
baseURL: 'https://api.example.com',
|
|
597
439
|
timeout: 30000,
|
|
598
|
-
headers: {
|
|
599
|
-
'X-Custom-Header': 'value'
|
|
600
|
-
}
|
|
440
|
+
headers: { 'X-Custom-Header': 'value' }
|
|
601
441
|
})
|
|
602
442
|
|
|
603
|
-
// Add
|
|
443
|
+
// Add custom interceptors
|
|
604
444
|
customAxios.interceptors.request.use((config) => {
|
|
605
|
-
//
|
|
445
|
+
// Your logic
|
|
606
446
|
return config
|
|
607
447
|
})
|
|
608
448
|
|
|
@@ -611,42 +451,36 @@ app.use(createApi({ axios: customAxios }))
|
|
|
611
451
|
|
|
612
452
|
---
|
|
613
453
|
|
|
614
|
-
### Error
|
|
615
|
-
|
|
616
|
-
#### Custom Error Parser
|
|
454
|
+
### Global Error Handler
|
|
617
455
|
|
|
618
|
-
|
|
456
|
+
Normalize errors from different backend formats:
|
|
619
457
|
|
|
620
458
|
```typescript
|
|
621
|
-
import { createApi } from '@ametie/vue-muza-use'
|
|
622
|
-
|
|
623
459
|
app.use(createApi({
|
|
624
460
|
axios: api,
|
|
461
|
+
|
|
462
|
+
// Global error handler
|
|
463
|
+
onError: (error) => {
|
|
464
|
+
toast.error(error.message)
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
// Error parser (normalize backend responses)
|
|
625
468
|
errorParser: (error: any) => {
|
|
626
469
|
const response = error.response?.data
|
|
627
470
|
|
|
628
|
-
// Laravel
|
|
471
|
+
// Laravel validation errors
|
|
629
472
|
if (response?.errors) {
|
|
630
473
|
return {
|
|
631
474
|
message: 'Validation Failed',
|
|
632
475
|
status: error.response.status,
|
|
633
476
|
code: 'VALIDATION_ERROR',
|
|
634
|
-
errors: response.errors
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Custom API format
|
|
639
|
-
if (response?.error?.message) {
|
|
640
|
-
return {
|
|
641
|
-
message: response.error.message,
|
|
642
|
-
status: error.response?.status || 500,
|
|
643
|
-
code: response.error.code
|
|
477
|
+
errors: response.errors
|
|
644
478
|
}
|
|
645
479
|
}
|
|
646
480
|
|
|
647
|
-
// Default
|
|
481
|
+
// Default format
|
|
648
482
|
return {
|
|
649
|
-
message: error.message || '
|
|
483
|
+
message: response?.message || error.message || 'Unknown error',
|
|
650
484
|
status: error.response?.status || 500,
|
|
651
485
|
details: error
|
|
652
486
|
}
|
|
@@ -654,245 +488,180 @@ app.use(createApi({
|
|
|
654
488
|
}))
|
|
655
489
|
```
|
|
656
490
|
|
|
657
|
-
#### Using Errors in Components
|
|
658
|
-
|
|
659
|
-
```vue
|
|
660
|
-
<script setup lang="ts">
|
|
661
|
-
const { data, error, execute } = useApi('/users', {
|
|
662
|
-
skipErrorNotification: true // Skip global error handler
|
|
663
|
-
})
|
|
664
|
-
|
|
665
|
-
// Access structured error
|
|
666
|
-
if (error.value) {
|
|
667
|
-
console.log(error.value.message) // User-friendly message
|
|
668
|
-
console.log(error.value.status) // HTTP status code
|
|
669
|
-
console.log(error.value.code) // Custom error code
|
|
670
|
-
console.log(error.value.errors) // Validation errors
|
|
671
|
-
}
|
|
672
|
-
</script>
|
|
673
|
-
|
|
674
|
-
<template>
|
|
675
|
-
<div v-if="error" class="error-banner">
|
|
676
|
-
{{ error.message }}
|
|
677
|
-
|
|
678
|
-
<!-- Display validation errors -->
|
|
679
|
-
<ul v-if="error.errors">
|
|
680
|
-
<li v-for="(msgs, field) in error.errors" :key="field">
|
|
681
|
-
<strong>{{ field }}:</strong> {{ msgs.join(', ') }}
|
|
682
|
-
</li>
|
|
683
|
-
</ul>
|
|
684
|
-
</div>
|
|
685
|
-
</template>
|
|
686
|
-
```
|
|
687
|
-
|
|
688
491
|
---
|
|
689
492
|
|
|
690
|
-
|
|
493
|
+
## 🔐 Authentication & Token Management
|
|
691
494
|
|
|
692
|
-
|
|
495
|
+
> **Note:** Authentication setup is optional. Only add this if your API requires JWT tokens.
|
|
693
496
|
|
|
694
|
-
|
|
497
|
+
### Basic Auth Setup
|
|
695
498
|
|
|
696
|
-
|
|
697
|
-
1. Request fails with 401 Unauthorized
|
|
698
|
-
↓
|
|
699
|
-
2. Add request to queue & pause all new requests
|
|
700
|
-
↓
|
|
701
|
-
3. Attempt token refresh (POST to refreshUrl)
|
|
702
|
-
↓
|
|
703
|
-
4. Success? → Replay all queued requests with new token
|
|
704
|
-
Failure? → Call onTokenRefreshFailed() & reject queue
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
This happens **transparently** — your components just wait a bit longer for the response.
|
|
708
|
-
|
|
709
|
-
#### Configuration
|
|
499
|
+
Add authentication to your API client:
|
|
710
500
|
|
|
711
501
|
```typescript
|
|
712
502
|
const api = createApiClient({
|
|
713
503
|
baseURL: 'https://api.example.com',
|
|
714
|
-
withAuth: true,
|
|
504
|
+
withAuth: true, // Enable automatic token injection
|
|
715
505
|
authOptions: {
|
|
716
|
-
refreshUrl: '/auth/refresh',
|
|
717
|
-
|
|
718
|
-
// ✨ NEW: Handle successful refresh response
|
|
719
|
-
onTokenRefreshed: (response) => {
|
|
720
|
-
// Extract additional data from refresh response
|
|
721
|
-
const { user, permissions } = response.data
|
|
722
|
-
|
|
723
|
-
// Update app state
|
|
724
|
-
store.commit('SET_USER', user)
|
|
725
|
-
store.commit('SET_PERMISSIONS', permissions)
|
|
726
|
-
|
|
727
|
-
// Analytics
|
|
728
|
-
analytics.track('token_refreshed', { userId: user.id })
|
|
729
|
-
},
|
|
730
|
-
|
|
506
|
+
refreshUrl: '/auth/refresh',
|
|
731
507
|
onTokenRefreshFailed: () => {
|
|
732
|
-
//
|
|
733
|
-
localStorage.clear()
|
|
508
|
+
// Redirect to login when refresh fails
|
|
734
509
|
window.location.href = '/login'
|
|
735
510
|
}
|
|
736
511
|
}
|
|
737
512
|
})
|
|
738
513
|
```
|
|
739
514
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
// utils/auth.ts
|
|
746
|
-
export const getAccessToken = (): string | null => {
|
|
747
|
-
return localStorage.getItem('access_token')
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
export const setAccessToken = (token: string): void => {
|
|
751
|
-
localStorage.setItem('access_token', token)
|
|
752
|
-
}
|
|
515
|
+
The library automatically:
|
|
516
|
+
- Injects `Authorization: Bearer <token>` header
|
|
517
|
+
- Refreshes expired tokens
|
|
518
|
+
- Queues requests during token refresh
|
|
519
|
+
- Retries failed requests after refresh
|
|
753
520
|
|
|
754
|
-
|
|
755
|
-
localStorage.removeItem('access_token')
|
|
756
|
-
}
|
|
757
|
-
```
|
|
521
|
+
---
|
|
758
522
|
|
|
759
|
-
|
|
523
|
+
### Token Management Modes
|
|
760
524
|
|
|
761
|
-
####
|
|
525
|
+
#### Mode 1: localStorage (Default)
|
|
762
526
|
|
|
763
|
-
|
|
527
|
+
Simple setup for development or internal tools:
|
|
764
528
|
|
|
765
529
|
```typescript
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
530
|
+
const api = createApiClient({
|
|
531
|
+
baseURL: 'https://api.example.com',
|
|
532
|
+
authOptions: {
|
|
533
|
+
refreshUrl: '/auth/refresh',
|
|
534
|
+
onTokenRefreshFailed: () => router.push('/login')
|
|
535
|
+
}
|
|
770
536
|
})
|
|
771
537
|
```
|
|
772
538
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
```typescript
|
|
778
|
-
import { setAuthMonitor } from '@ametie/vue-muza-use'
|
|
779
|
-
|
|
780
|
-
setAuthMonitor((event, payload) => {
|
|
781
|
-
console.log(`[Auth Event] ${event}`, payload)
|
|
782
|
-
|
|
783
|
-
// Events: 'token_refresh_start', 'token_refresh_success',
|
|
784
|
-
// 'token_refresh_failed', 'token_injected'
|
|
785
|
-
})
|
|
786
|
-
```
|
|
539
|
+
**Storage:** Both `accessToken` and `refreshToken` in localStorage
|
|
540
|
+
**Security:** ⚠️ Vulnerable to XSS attacks
|
|
541
|
+
**Use case:** Development, internal tools
|
|
787
542
|
|
|
788
543
|
---
|
|
789
544
|
|
|
790
|
-
|
|
545
|
+
#### Mode 2: httpOnly Cookies (Production)
|
|
791
546
|
|
|
792
|
-
|
|
547
|
+
Recommended for production apps with sensitive data:
|
|
793
548
|
|
|
794
549
|
```typescript
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
console.log('Success!', response.data)
|
|
802
|
-
analytics.track('api_success', { endpoint: '/analytics' })
|
|
803
|
-
},
|
|
804
|
-
onError: (error) => {
|
|
805
|
-
console.error('Failed:', error.message)
|
|
806
|
-
sentry.captureException(error)
|
|
807
|
-
},
|
|
808
|
-
onFinish: () => {
|
|
809
|
-
console.log('Request finished (success or error)')
|
|
810
|
-
loadingBar.finish()
|
|
550
|
+
const api = createApiClient({
|
|
551
|
+
baseURL: 'https://api.example.com',
|
|
552
|
+
authOptions: {
|
|
553
|
+
refreshUrl: '/auth/refresh',
|
|
554
|
+
refreshWithCredentials: true, // 🔑 Send cookies for refresh
|
|
555
|
+
onTokenRefreshFailed: () => router.push('/login')
|
|
811
556
|
}
|
|
812
557
|
})
|
|
813
558
|
```
|
|
814
559
|
|
|
815
|
-
**
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
3. `onSuccess` OR `onError`
|
|
819
|
-
4. `onFinish` (always runs)
|
|
560
|
+
**Storage:** Only `accessToken` in localStorage, `refreshToken` in httpOnly cookie
|
|
561
|
+
**Security:** 🔒 Protected from XSS attacks
|
|
562
|
+
**Backend requirement:** Must set `Set-Cookie` with `HttpOnly; Secure; SameSite`
|
|
820
563
|
|
|
821
|
-
|
|
564
|
+
**Common Issues:**
|
|
565
|
+
- **Cookie not sent?** Check cookie domain and `SameSite` attribute
|
|
566
|
+
- **CORS error?** Backend must set `Access-Control-Allow-Credentials: true`
|
|
567
|
+
- **401 on refresh?** Verify cookie is included in request headers
|
|
822
568
|
|
|
823
|
-
|
|
569
|
+
---
|
|
824
570
|
|
|
825
|
-
|
|
571
|
+
### Saving Tokens After Login
|
|
826
572
|
|
|
827
573
|
```typescript
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
574
|
+
import { tokenManager } from '@ametie/vue-muza-use'
|
|
575
|
+
|
|
576
|
+
const { execute } = useApi('/auth/login', {
|
|
577
|
+
method: 'POST',
|
|
578
|
+
authMode: 'public', // No auth for login endpoint
|
|
579
|
+
onSuccess(response) {
|
|
580
|
+
tokenManager.setTokens({
|
|
581
|
+
accessToken: response.data.accessToken,
|
|
582
|
+
refreshToken: response.data.refreshToken, // Optional in cookie mode
|
|
583
|
+
expiresIn: response.data.expiresIn
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
router.push('/dashboard')
|
|
833
587
|
}
|
|
834
588
|
})
|
|
835
589
|
```
|
|
836
590
|
|
|
837
|
-
**When retries happen:**
|
|
838
|
-
- Network errors (timeouts, connection refused)
|
|
839
|
-
- 5xx server errors
|
|
840
|
-
- Configurable via axios retry interceptor
|
|
841
|
-
|
|
842
|
-
**When retries DON'T happen:**
|
|
843
|
-
- 4xx client errors (bad request, unauthorized, etc.)
|
|
844
|
-
- Component is unmounted (automatic cleanup)
|
|
845
|
-
|
|
846
591
|
---
|
|
847
592
|
|
|
848
|
-
###
|
|
593
|
+
### Public Endpoints
|
|
594
|
+
|
|
595
|
+
Skip authentication for public endpoints:
|
|
849
596
|
|
|
850
|
-
#### Local Loading (Per Request)
|
|
851
597
|
```typescript
|
|
852
|
-
|
|
853
|
-
|
|
598
|
+
// Login (no auth needed)
|
|
599
|
+
useApi('/auth/login', {
|
|
600
|
+
method: 'POST',
|
|
601
|
+
authMode: 'public',
|
|
602
|
+
data: credentials
|
|
603
|
+
})
|
|
854
604
|
|
|
855
|
-
//
|
|
605
|
+
// Public blog posts
|
|
606
|
+
useApi('/blog/posts', {
|
|
607
|
+
authMode: 'public',
|
|
608
|
+
immediate: true
|
|
609
|
+
})
|
|
856
610
|
```
|
|
857
611
|
|
|
858
|
-
|
|
859
|
-
```typescript
|
|
860
|
-
const globalLoading = ref(false)
|
|
612
|
+
---
|
|
861
613
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
})
|
|
614
|
+
### Advanced: Custom Refresh Payload
|
|
615
|
+
|
|
616
|
+
Send additional data with token refresh requests:
|
|
866
617
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
618
|
+
```typescript
|
|
619
|
+
const api = createApiClient({
|
|
620
|
+
baseURL: 'https://api.example.com',
|
|
621
|
+
authOptions: {
|
|
622
|
+
refreshUrl: '/auth/refresh',
|
|
623
|
+
|
|
624
|
+
// ⚠️ Use function for dynamic data
|
|
625
|
+
refreshPayload: () => ({
|
|
626
|
+
refreshToken: tokenManager.getRefreshToken(),
|
|
627
|
+
deviceId: getDeviceId(),
|
|
628
|
+
timestamp: Date.now()
|
|
629
|
+
})
|
|
630
|
+
}
|
|
870
631
|
})
|
|
871
632
|
```
|
|
872
633
|
|
|
873
|
-
|
|
874
|
-
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
### Advanced: Token Refresh Callback
|
|
637
|
+
|
|
638
|
+
Handle additional data from refresh response:
|
|
875
639
|
|
|
876
640
|
```typescript
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
641
|
+
const api = createApiClient({
|
|
642
|
+
baseURL: 'https://api.example.com',
|
|
643
|
+
authOptions: {
|
|
644
|
+
refreshUrl: '/auth/refresh',
|
|
645
|
+
|
|
646
|
+
// Called after successful token refresh
|
|
647
|
+
onTokenRefreshed: (response) => {
|
|
648
|
+
const { user, permissions } = response.data
|
|
649
|
+
|
|
650
|
+
// Update app state
|
|
651
|
+
store.commit('SET_USER', user)
|
|
652
|
+
store.commit('SET_PERMISSIONS', permissions)
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
onTokenRefreshFailed: () => {
|
|
656
|
+
localStorage.clear()
|
|
657
|
+
window.location.href = '/login'
|
|
658
|
+
}
|
|
892
659
|
}
|
|
893
660
|
})
|
|
894
661
|
```
|
|
895
662
|
|
|
663
|
+
---
|
|
664
|
+
|
|
896
665
|
## 📚 API Reference
|
|
897
666
|
|
|
898
667
|
### `useApi<T, D>(url, options)`
|