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