vue-api-kit 1.5.1 → 1.6.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/README.md +24 -7
- package/dist/index.js +118 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -458,18 +458,23 @@ The client includes built-in CSRF token protection, perfect for Laravel Sanctum
|
|
|
458
458
|
|
|
459
459
|
### How it works
|
|
460
460
|
|
|
461
|
-
|
|
462
|
-
1.
|
|
463
|
-
2.
|
|
464
|
-
3.
|
|
465
|
-
|
|
461
|
+
**Automatic XSRF Token Handling:**
|
|
462
|
+
1. When `withCredentials: true`, the client automatically reads the `XSRF-TOKEN` cookie
|
|
463
|
+
2. Decodes and sends it as `X-XSRF-TOKEN` header with every request
|
|
464
|
+
3. This satisfies Laravel Sanctum's CSRF protection requirements
|
|
465
|
+
|
|
466
|
+
**Automatic CSRF Refresh:**
|
|
467
|
+
1. Detects CSRF errors (403 or 419 status codes)
|
|
468
|
+
2. Calls the CSRF refresh endpoint to get a new token
|
|
469
|
+
3. Retries the original request automatically with the fresh token
|
|
470
|
+
4. Prevents infinite loops and race conditions
|
|
466
471
|
|
|
467
472
|
### Configuration
|
|
468
473
|
|
|
469
474
|
```typescript
|
|
470
475
|
const api = createApiClient({
|
|
471
476
|
baseURL: 'https://api.example.com',
|
|
472
|
-
withCredentials: true, // Required
|
|
477
|
+
withCredentials: true, // Required! Enables cookies AND auto XSRF header
|
|
473
478
|
csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel Sanctum endpoint
|
|
474
479
|
|
|
475
480
|
queries: { /* ... */ },
|
|
@@ -486,7 +491,7 @@ import { z } from 'zod';
|
|
|
486
491
|
|
|
487
492
|
export const api = createApiClient({
|
|
488
493
|
baseURL: 'https://api.example.com',
|
|
489
|
-
withCredentials: true, //
|
|
494
|
+
withCredentials: true, // Enables cookies AND automatic XSRF-TOKEN header
|
|
490
495
|
csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel's CSRF endpoint
|
|
491
496
|
|
|
492
497
|
mutations: {
|
|
@@ -519,12 +524,24 @@ export const api = createApiClient({
|
|
|
519
524
|
|
|
520
525
|
### Benefits
|
|
521
526
|
|
|
527
|
+
- ✅ **Automatic XSRF Header**: Cookie automatically sent as `X-XSRF-TOKEN` header
|
|
522
528
|
- ✅ **Automatic Recovery**: No manual token refresh needed
|
|
523
529
|
- ✅ **Seamless UX**: Users don't experience authentication errors
|
|
524
530
|
- ✅ **Race Condition Safe**: Multiple simultaneous requests share the same refresh
|
|
525
531
|
- ✅ **Infinite Loop Prevention**: Won't retry the CSRF endpoint itself
|
|
526
532
|
- ✅ **Laravel Sanctum Compatible**: Works perfectly with Laravel's SPA authentication
|
|
527
533
|
|
|
534
|
+
### Important Notes
|
|
535
|
+
|
|
536
|
+
1. **withCredentials is required**: Set `withCredentials: true` to enable both cookies and automatic XSRF token handling
|
|
537
|
+
2. **Cookie Domain**: Ensure your API sets cookies with the correct domain (e.g., `.localhost` for local development)
|
|
538
|
+
3. **CORS Configuration**: Your Laravel backend must allow credentials:
|
|
539
|
+
```php
|
|
540
|
+
// config/cors.php
|
|
541
|
+
'supports_credentials' => true,
|
|
542
|
+
'allowed_origins' => ['http://localhost:5173'],
|
|
543
|
+
```
|
|
544
|
+
|
|
528
545
|
|
|
529
546
|
|
|
530
547
|
## 📝 License
|
package/dist/index.js
CHANGED
|
@@ -1,159 +1,163 @@
|
|
|
1
|
-
import { ZodError as
|
|
2
|
-
import * as
|
|
3
|
-
import
|
|
4
|
-
import { AxiosError as
|
|
5
|
-
import { nextTick as
|
|
6
|
-
import { debounce as
|
|
7
|
-
function z(
|
|
8
|
-
const
|
|
9
|
-
baseURL:
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { ZodError as S } from "zod";
|
|
2
|
+
import * as $ from "zod";
|
|
3
|
+
import L, { AxiosError as F } from "axios";
|
|
4
|
+
import { AxiosError as G } from "axios";
|
|
5
|
+
import { nextTick as N, ref as m, onMounted as T, watch as k, onBeforeUnmount as x } from "vue";
|
|
6
|
+
import { debounce as U } from "lodash-es";
|
|
7
|
+
function z(t) {
|
|
8
|
+
const p = L.create({
|
|
9
|
+
baseURL: t.baseURL,
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
Accept: "application/json",
|
|
13
|
+
...t.headers
|
|
14
|
+
},
|
|
15
|
+
withCredentials: t.withCredentials ?? !1
|
|
12
16
|
});
|
|
13
|
-
let
|
|
14
|
-
|
|
17
|
+
let C = !1, q = null;
|
|
18
|
+
t.onBeforeRequest && p.interceptors.request.use(
|
|
15
19
|
async (e) => {
|
|
16
20
|
try {
|
|
17
|
-
return await
|
|
18
|
-
} catch (
|
|
19
|
-
return Promise.reject(
|
|
21
|
+
return await t.onBeforeRequest(e) || e;
|
|
22
|
+
} catch (r) {
|
|
23
|
+
return Promise.reject(r);
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
(e) => Promise.reject(e)
|
|
23
|
-
),
|
|
27
|
+
), t.onStartRequest && p.interceptors.request.use(
|
|
24
28
|
async (e) => {
|
|
25
29
|
try {
|
|
26
|
-
return await
|
|
27
|
-
} catch (
|
|
28
|
-
return Promise.reject(
|
|
30
|
+
return await t.onStartRequest(), e;
|
|
31
|
+
} catch (r) {
|
|
32
|
+
return Promise.reject(r);
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
(e) => Promise.reject(e)
|
|
32
|
-
),
|
|
33
|
-
(e) => (
|
|
34
|
-
(e) => (
|
|
35
|
-
),
|
|
36
|
+
), t.onFinishRequest && p.interceptors.response.use(
|
|
37
|
+
(e) => (t.onFinishRequest(), e),
|
|
38
|
+
(e) => (t.onFinishRequest(), Promise.reject(e))
|
|
39
|
+
), p.interceptors.request.use((e) => {
|
|
36
40
|
if (!e.url) return e;
|
|
37
|
-
const
|
|
41
|
+
const r = (a) => {
|
|
38
42
|
if (a)
|
|
39
|
-
for (const [
|
|
40
|
-
const l = `{${
|
|
43
|
+
for (const [s, R] of Object.entries(a)) {
|
|
44
|
+
const l = `{${s}}`;
|
|
41
45
|
e.url.includes(l) && (e.url = e.url.replace(
|
|
42
46
|
l,
|
|
43
|
-
encodeURIComponent(String(
|
|
44
|
-
), delete a[
|
|
47
|
+
encodeURIComponent(String(R))
|
|
48
|
+
), delete a[s]);
|
|
45
49
|
}
|
|
46
50
|
};
|
|
47
|
-
return e.method !== "get" && e.data?.params &&
|
|
48
|
-
}),
|
|
51
|
+
return e.method !== "get" && e.data?.params && r(e.data.params), r(e.params), e;
|
|
52
|
+
}), t.withCredentials && p.interceptors.request.use((e) => {
|
|
53
|
+
const r = document.cookie.split("; ").find((a) => a.startsWith("XSRF-TOKEN="))?.split("=")[1];
|
|
54
|
+
return r && (e.headers["X-XSRF-TOKEN"] = decodeURIComponent(r)), e;
|
|
55
|
+
}), t.csrfRefreshEndpoint && p.interceptors.response.use(
|
|
49
56
|
(e) => e,
|
|
50
57
|
async (e) => {
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
58
|
+
const r = e.config;
|
|
59
|
+
if (r?.url === t.csrfRefreshEndpoint)
|
|
53
60
|
return Promise.reject(e);
|
|
54
|
-
if (e.response && (e.response.status === 403 || e.response.status === 419) && !
|
|
55
|
-
|
|
61
|
+
if (e.response && (e.response.status === 403 || e.response.status === 419) && !r?._retry) {
|
|
62
|
+
r._retry = !0;
|
|
56
63
|
try {
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}).then(() => {
|
|
61
|
-
P = !1, q = null;
|
|
62
|
-
}), await q), h(s);
|
|
64
|
+
return C && q ? await q : (C = !0, q = p.get(t.csrfRefreshEndpoint).then(() => {
|
|
65
|
+
C = !1, q = null;
|
|
66
|
+
}), await q), p.request(r);
|
|
63
67
|
} catch (a) {
|
|
64
|
-
return
|
|
68
|
+
return C = !1, q = null, Promise.reject(a);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
return Promise.reject(e);
|
|
68
72
|
}
|
|
69
|
-
),
|
|
73
|
+
), p.interceptors.response.use(
|
|
70
74
|
(e) => e,
|
|
71
|
-
(e) => (
|
|
75
|
+
(e) => (N(() => {
|
|
72
76
|
e.code;
|
|
73
77
|
}), Promise.reject(e))
|
|
74
78
|
);
|
|
75
|
-
const
|
|
76
|
-
for (const e in
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
let
|
|
80
|
-
a && typeof a == "object" && ("loadOnMount" in a || "debounce" in a || "onResult" in a || "onError" in a || "data" in a ?
|
|
81
|
-
const
|
|
82
|
-
let
|
|
79
|
+
const P = t.queries ?? {}, M = {};
|
|
80
|
+
for (const e in P) {
|
|
81
|
+
const r = P[e];
|
|
82
|
+
r && (M[e] = (a) => {
|
|
83
|
+
let s;
|
|
84
|
+
a && typeof a == "object" && ("loadOnMount" in a || "debounce" in a || "onResult" in a || "onError" in a || "data" in a ? s = a : s = { params: a });
|
|
85
|
+
const R = m(), l = m(), v = m(), g = m(!1), y = m(!1), A = m(!0);
|
|
86
|
+
let j = new AbortController();
|
|
83
87
|
const u = () => {
|
|
84
|
-
|
|
88
|
+
j?.abort(), j = new AbortController();
|
|
85
89
|
}, c = async () => {
|
|
86
90
|
g.value && u(), g.value = !0, l.value = void 0;
|
|
87
91
|
try {
|
|
88
|
-
|
|
89
|
-
let o =
|
|
90
|
-
|
|
92
|
+
r.params && s?.params && r.params.parse(s.params);
|
|
93
|
+
let o = s?.data;
|
|
94
|
+
r.data && o && r.data.parse(o);
|
|
91
95
|
const f = {
|
|
92
|
-
method:
|
|
93
|
-
url:
|
|
94
|
-
params:
|
|
95
|
-
signal:
|
|
96
|
+
method: r.method ?? "GET",
|
|
97
|
+
url: r.path,
|
|
98
|
+
params: s?.params,
|
|
99
|
+
signal: j.signal
|
|
96
100
|
};
|
|
97
|
-
|
|
98
|
-
const n = await
|
|
99
|
-
|
|
101
|
+
r.method === "POST" && o && (f.data = o);
|
|
102
|
+
const n = await p.request(f), i = r.response ? r.response.parse(n.data) : n.data;
|
|
103
|
+
R.value = i, s?.onResult?.(i);
|
|
100
104
|
} catch (o) {
|
|
101
|
-
if (o instanceof
|
|
105
|
+
if (o instanceof F) {
|
|
102
106
|
if (o.code !== "ERR_CANCELED") {
|
|
103
|
-
const f = o.response?.data?.message || o.message || "An error occurred", n = o.response?.status, i = o.code,
|
|
104
|
-
l.value = f,
|
|
107
|
+
const f = o.response?.data?.message || o.message || "An error occurred", n = o.response?.status, i = o.code, h = o.response?.data;
|
|
108
|
+
l.value = f, s?.onError?.(o), t.onErrorRequest?.({ message: f, status: n, code: i, data: h });
|
|
105
109
|
}
|
|
106
|
-
} else if (o instanceof
|
|
107
|
-
|
|
108
|
-
const n = `Validation error: ${
|
|
110
|
+
} else if (o instanceof S) {
|
|
111
|
+
v.value = o.issues || [];
|
|
112
|
+
const n = `Validation error: ${v.value.map(
|
|
109
113
|
(i) => `${i.path.join(".")}: ${i.message}`
|
|
110
114
|
).join(", ")}`;
|
|
111
|
-
l.value = n,
|
|
115
|
+
l.value = n, s?.onError?.(o), s?.onZodError?.(v.value), t.onErrorRequest?.({ message: n, code: "VALIDATION_ERROR" }), t.onZodError && t.onZodError(v.value);
|
|
112
116
|
} else {
|
|
113
117
|
const f = o.message || "An error occurred";
|
|
114
|
-
l.value = f,
|
|
118
|
+
l.value = f, s?.onError?.(f), t.onErrorRequest?.({ message: f });
|
|
115
119
|
}
|
|
116
120
|
} finally {
|
|
117
121
|
g.value = !1, y.value = !0;
|
|
118
122
|
}
|
|
119
|
-
}, E =
|
|
123
|
+
}, E = s?.debounce ? U(c, s.debounce) : c;
|
|
120
124
|
let d = null;
|
|
121
|
-
return (
|
|
122
|
-
d && d(), d =
|
|
123
|
-
() => JSON.stringify({ params:
|
|
125
|
+
return (s?.params || s?.data) && (T(() => {
|
|
126
|
+
d && d(), d = k(
|
|
127
|
+
() => JSON.stringify({ params: s.params, data: s.data }),
|
|
124
128
|
() => {
|
|
125
129
|
E();
|
|
126
130
|
},
|
|
127
131
|
{ immediate: !1 }
|
|
128
132
|
);
|
|
129
|
-
}),
|
|
130
|
-
d && d(),
|
|
131
|
-
})), (
|
|
133
|
+
}), x(() => {
|
|
134
|
+
d && d(), j?.abort();
|
|
135
|
+
})), (s?.loadOnMount === void 0 || s.loadOnMount) && !y.value && (A.value ? (A.value = !1, c()) : E()), { result: R, errorMessage: l, zodErrors: v, isLoading: g, isDone: y, refetch: c };
|
|
132
136
|
});
|
|
133
137
|
}
|
|
134
|
-
const D =
|
|
138
|
+
const D = t.mutations ?? {}, w = {};
|
|
135
139
|
for (const e in D) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
return { result:
|
|
140
|
-
if (!
|
|
141
|
-
|
|
140
|
+
const r = D[e];
|
|
141
|
+
r && (w[e] = (a) => {
|
|
142
|
+
const s = m(), R = m(), l = m(), v = m(!1), g = m(!1), y = m(0);
|
|
143
|
+
return { result: s, errorMessage: R, zodErrors: l, isLoading: v, isDone: g, uploadProgress: y, mutate: async (j) => {
|
|
144
|
+
if (!v.value) {
|
|
145
|
+
v.value = !0, R.value = void 0, y.value = 0;
|
|
142
146
|
try {
|
|
143
|
-
const { data: u = {}, params: c } =
|
|
147
|
+
const { data: u = {}, params: c } = j ?? {};
|
|
144
148
|
let E = u ?? {}, d = {};
|
|
145
|
-
if (
|
|
149
|
+
if (r.isMultipart) {
|
|
146
150
|
const n = new FormData();
|
|
147
|
-
for (const [i,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}) : typeof
|
|
151
|
+
for (const [i, h] of Object.entries(u))
|
|
152
|
+
h instanceof File || h instanceof Blob ? n.append(i, h) : Array.isArray(h) ? h.forEach((b) => {
|
|
153
|
+
b instanceof File || b instanceof Blob ? n.append(i, b) : n.append(i, JSON.stringify(b));
|
|
154
|
+
}) : typeof h == "object" && h !== null ? n.append(i, JSON.stringify(h)) : n.append(i, String(h));
|
|
151
155
|
E = n, d["Content-Type"] = "multipart/form-data";
|
|
152
|
-
} else
|
|
153
|
-
|
|
154
|
-
const o = await
|
|
155
|
-
method:
|
|
156
|
-
url:
|
|
156
|
+
} else r.data && r.data.parse(u);
|
|
157
|
+
r.params && c && r.params.parse(c);
|
|
158
|
+
const o = await p.request({
|
|
159
|
+
method: r.method,
|
|
160
|
+
url: r.path,
|
|
157
161
|
data: E,
|
|
158
162
|
params: c,
|
|
159
163
|
headers: d,
|
|
@@ -163,24 +167,24 @@ function z(r) {
|
|
|
163
167
|
y.value = i, a?.onUploadProgress?.(i);
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
|
-
}), f =
|
|
167
|
-
|
|
170
|
+
}), f = r.response ? r.response.parse(o.data) : o.data;
|
|
171
|
+
s.value = f, a?.onResult?.(f);
|
|
168
172
|
} catch (u) {
|
|
169
|
-
if (u instanceof
|
|
173
|
+
if (u instanceof F) {
|
|
170
174
|
const c = u.response?.data?.message || u.message || "An error occurred", E = u.response?.status, d = u.code;
|
|
171
|
-
|
|
172
|
-
} else if (u instanceof
|
|
175
|
+
R.value = c, a?.onError?.(u), t.onErrorRequest?.({ message: c, status: E, code: d });
|
|
176
|
+
} else if (u instanceof S) {
|
|
173
177
|
l.value = u.issues || [];
|
|
174
178
|
const E = `Validation error: ${l.value.map(
|
|
175
179
|
(d) => `${d.path.join(".")}: ${d.message}`
|
|
176
180
|
).join(", ")}`;
|
|
177
|
-
|
|
181
|
+
R.value = E, a?.onError?.(u), a?.onZodError?.(l.value), t.onErrorRequest?.({ message: E, code: "VALIDATION_ERROR" }), t.onZodError && t.onZodError(l.value);
|
|
178
182
|
} else {
|
|
179
183
|
const c = u.message || "An error occurred";
|
|
180
|
-
|
|
184
|
+
R.value = c, a?.onError?.(u), t.onErrorRequest?.({ message: c });
|
|
181
185
|
}
|
|
182
186
|
} finally {
|
|
183
|
-
|
|
187
|
+
v.value = !1, g.value = !0;
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
} };
|
|
@@ -191,24 +195,24 @@ function z(r) {
|
|
|
191
195
|
mutation: w
|
|
192
196
|
};
|
|
193
197
|
}
|
|
194
|
-
function V(
|
|
195
|
-
return
|
|
198
|
+
function V(t) {
|
|
199
|
+
return t;
|
|
196
200
|
}
|
|
197
|
-
function J(
|
|
198
|
-
return
|
|
201
|
+
function J(t) {
|
|
202
|
+
return t;
|
|
199
203
|
}
|
|
200
|
-
function Q(...
|
|
201
|
-
return Object.assign({}, ...
|
|
204
|
+
function Q(...t) {
|
|
205
|
+
return Object.assign({}, ...t);
|
|
202
206
|
}
|
|
203
|
-
function W(...
|
|
204
|
-
return Object.assign({}, ...
|
|
207
|
+
function W(...t) {
|
|
208
|
+
return Object.assign({}, ...t);
|
|
205
209
|
}
|
|
206
210
|
export {
|
|
207
|
-
|
|
211
|
+
G as AxiosError,
|
|
208
212
|
z as createApiClient,
|
|
209
213
|
J as defineMutation,
|
|
210
214
|
V as defineQuery,
|
|
211
215
|
W as mergeMutations,
|
|
212
216
|
Q as mergeQueries,
|
|
213
|
-
|
|
217
|
+
$ as z
|
|
214
218
|
};
|
package/package.json
CHANGED