@visns-studio/visns-components 6.17.1 → 6.20.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/package.json +10 -7
- package/src/components/AssociationManager.jsx +344 -177
- package/src/components/AsyncSelect.jsx +2 -2
- package/src/components/Autocomplete.jsx +57 -12
- package/src/components/Breadcrumb.jsx +23 -4
- package/src/components/ClientAssociationManager.jsx +321 -145
- package/src/components/DataGrid.jsx +625 -20
- package/src/components/DropZone.jsx +4 -1
- package/src/components/Form.jsx +233 -6
- package/src/components/MultiSelect.jsx +2 -2
- package/src/components/Navigation.jsx +45 -25
- package/src/components/TableFilter.jsx +172 -12
- package/src/components/UniversalSearch.jsx +390 -0
- package/src/components/auth/Login.jsx +164 -8
- package/src/components/auth/Profile.jsx +209 -22
- package/src/components/auth/authEndpoints.js +4 -0
- package/src/components/auth/webauthn.js +194 -0
- package/src/components/columns/ColumnRenderers.jsx +6 -1
- package/src/components/controls/DataGridSearch.jsx +11 -5
- package/src/components/generic/GenericAuth.jsx +6 -0
- package/src/components/generic/GenericDashboard.jsx +93 -36
- package/src/components/generic/GenericDetail.jsx +146 -61
- package/src/components/generic/GenericIndex.jsx +134 -38
- package/src/components/generic/GenericSettings.jsx +220 -0
- package/src/components/generic/StandardModal.jsx +165 -8
- package/src/components/generic/dashboardChartDefaults.js +230 -0
- package/src/components/generic/shared/indexLayout.js +38 -0
- package/src/components/navActive.js +25 -0
- package/src/components/settings/IntegrationsManager.jsx +474 -0
- package/src/components/sms/SmsLineSettings.jsx +16 -12
- package/src/components/styles/AssociationManager.module.scss +4 -363
- package/src/components/styles/AuditLog.module.scss +6 -6
- package/src/components/styles/AuditLogs.module.scss +3 -3
- package/src/components/styles/Autocomplete.module.scss +7 -7
- package/src/components/styles/BusinessCardOcr.module.scss +6 -1
- package/src/components/styles/CallQueueSettings.module.scss +4 -1
- package/src/components/styles/CameraPlacement.module.scss +19 -19
- package/src/components/styles/CategorizedDropZone.module.scss +6 -1
- package/src/components/styles/ClientAssociationManager.module.scss +5 -289
- package/src/components/styles/ClientAuth.module.scss +1 -1
- package/src/components/styles/DataGrid.module.scss +455 -8
- package/src/components/styles/Field.module.scss +32 -24
- package/src/components/styles/Form.module.scss +123 -3
- package/src/components/styles/GenericDashboard.module.scss +612 -825
- package/src/components/styles/GenericDetail.module.scss +406 -61
- package/src/components/styles/GenericDynamic.module.scss +25 -25
- package/src/components/styles/GenericEditableTable.module.scss +33 -33
- package/src/components/styles/GenericFormBuilder.module.scss +119 -119
- package/src/components/styles/GenericIndex.module.scss +483 -38
- package/src/components/styles/GenericMain.module.scss +6 -3
- package/src/components/styles/GenericQuote.module.scss +399 -81
- package/src/components/styles/GenericReport.module.scss +8 -8
- package/src/components/styles/GenericSettings.module.scss +209 -0
- package/src/components/styles/GenericSort.module.scss +9 -9
- package/src/components/styles/Integrations.module.scss +321 -0
- package/src/components/styles/List.module.scss +4 -4
- package/src/components/styles/Login.module.scss +57 -0
- package/src/components/styles/MultiSelect.module.scss +4 -4
- package/src/components/styles/Navigation.module.scss +12 -12
- package/src/components/styles/Notification.module.scss +12 -8
- package/src/components/styles/NotificationList.module.scss +5 -5
- package/src/components/styles/Profile.module.scss +5 -5
- package/src/components/styles/QrCode.module.scss +4 -4
- package/src/components/styles/QuickAction.module.scss +20 -20
- package/src/components/styles/SortableQuoteItems.module.scss +4 -4
- package/src/components/styles/TableFilter.module.scss +128 -17
- package/src/components/styles/TicketConversation.module.scss +497 -0
- package/src/components/styles/UniversalSearch.module.scss +229 -0
- package/src/components/styles/Vault.module.scss +413 -0
- package/src/components/styles/_associationManager.scss +467 -0
- package/src/components/styles/_authBrandPanel.scss +15 -9
- package/src/components/styles/_controls.scss +43 -39
- package/src/components/styles/global-datagrid.css +196 -0
- package/src/components/styles/global.css +168 -17
- package/src/components/tickets/TicketConversation.jsx +455 -0
- package/src/components/utils/rowActionSettings.js +117 -0
- package/src/components/vault/VaultClientPicker.jsx +183 -0
- package/src/components/vault/VaultEntryForm.jsx +23 -0
- package/src/components/vault/VaultManager.jsx +382 -34
- package/src/components/vault/VaultShareModal.jsx +454 -0
- package/src/components/vault/vaultEndpoints.js +16 -2
- package/src/components/vault/vaultShares.js +247 -0
- package/src/index.js +35 -0
- package/src/utils/fetchUtil.js +38 -14
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The share modal's pure logic.
|
|
3
|
+
*
|
|
4
|
+
* Kept React-free so `node --test` can import it: the parts of this feature
|
|
5
|
+
* worth testing are the field whitelist, the request body and the words put
|
|
6
|
+
* next to a link's state, none of which need a DOM.
|
|
7
|
+
*
|
|
8
|
+
* ONE THING TO KEEP IN MIND THROUGHOUT. A share link is the credential — there
|
|
9
|
+
* is no password on the far side of it — so every default in here leans
|
|
10
|
+
* conservative: the shortest expiry list that is still useful, the narrowest
|
|
11
|
+
* default field set, and a status vocabulary that never says "active" about a
|
|
12
|
+
* link that is not.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The fields a link can carry, in the order the modal lists them and the order
|
|
17
|
+
* the server stores them.
|
|
18
|
+
*
|
|
19
|
+
* `totp` is the CURRENT SIX-DIGIT CODE, computed server-side when the
|
|
20
|
+
* recipient reveals — never the seed. There is no option to send the seed and
|
|
21
|
+
* that is deliberate: a seed is a permanent second factor, and one sitting in
|
|
22
|
+
* the same message as the password is not a second factor at all.
|
|
23
|
+
*/
|
|
24
|
+
export const SHARE_FIELDS = [
|
|
25
|
+
{ key: 'username', label: 'Username' },
|
|
26
|
+
{ key: 'password', label: 'Password' },
|
|
27
|
+
{ key: 'totp', label: '2FA code', hint: 'The current code, not the secret' },
|
|
28
|
+
{ key: 'url', label: 'Website' },
|
|
29
|
+
{ key: 'notes', label: 'Notes' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** What a new share starts with: the pair that is useful on its own. */
|
|
33
|
+
export const DEFAULT_SHARE_FIELDS = ['username', 'password'];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The expiry choices, in hours.
|
|
37
|
+
*
|
|
38
|
+
* No "never" option anywhere, at any layer: the server requires an expiry and
|
|
39
|
+
* caps it, and offering one here that it would refuse is a worse experience
|
|
40
|
+
* than not offering it.
|
|
41
|
+
*/
|
|
42
|
+
export const EXPIRY_OPTIONS = [
|
|
43
|
+
{ value: 1, label: '1 hour' },
|
|
44
|
+
{ value: 24, label: '24 hours' },
|
|
45
|
+
{ value: 168, label: '7 days' },
|
|
46
|
+
{ value: 720, label: '30 days' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_EXPIRY_HOURS = 24;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The view budget. `null` is "no limit", which is still bounded by the expiry —
|
|
53
|
+
* unlimited views is not the same as an unlimited link.
|
|
54
|
+
*/
|
|
55
|
+
export const VIEW_LIMIT_OPTIONS = [
|
|
56
|
+
{ value: 1, label: '1 view' },
|
|
57
|
+
{ value: 5, label: '5 views' },
|
|
58
|
+
{ value: null, label: 'No limit' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Which fields this entry can actually offer.
|
|
63
|
+
*
|
|
64
|
+
* The LIST payload deliberately carries no password and no notes — that is the
|
|
65
|
+
* vault's whole design — so those two cannot be known here and are always
|
|
66
|
+
* offered; the server refuses a link to a field the entry does not hold, with a
|
|
67
|
+
* message saying which. The three that ARE knowable from a row are disabled up
|
|
68
|
+
* front, because a checkbox that turns into a 422 is a worse answer than one
|
|
69
|
+
* that was never tickable.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} entry a row from the list endpoint
|
|
72
|
+
* @returns {Array<{key: string, label: string, hint?: string, disabled: boolean, reason?: string}>}
|
|
73
|
+
*/
|
|
74
|
+
export const availableShareFields = (entry) => {
|
|
75
|
+
const row = entry || {};
|
|
76
|
+
|
|
77
|
+
return SHARE_FIELDS.map((field) => {
|
|
78
|
+
let disabled = false;
|
|
79
|
+
let reason;
|
|
80
|
+
|
|
81
|
+
if (field.key === 'username' && !row.username) {
|
|
82
|
+
disabled = true;
|
|
83
|
+
reason = 'This entry has no username';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (field.key === 'url' && !row.url) {
|
|
87
|
+
disabled = true;
|
|
88
|
+
reason = 'This entry has no address';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (field.key === 'totp' && !row.has_totp) {
|
|
92
|
+
disabled = true;
|
|
93
|
+
reason = 'This entry has no 2FA';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { ...field, disabled, ...(reason ? { reason } : {}) };
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The starting selection for an entry: the defaults, minus anything it cannot
|
|
102
|
+
* offer. An entry with no username shares just the password.
|
|
103
|
+
*
|
|
104
|
+
* @returns {Array<string>}
|
|
105
|
+
*/
|
|
106
|
+
export const initialShareFields = (entry) =>
|
|
107
|
+
availableShareFields(entry)
|
|
108
|
+
.filter((field) => DEFAULT_SHARE_FIELDS.includes(field.key) && !field.disabled)
|
|
109
|
+
.map((field) => field.key);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Add or remove one field, keeping the module's own order.
|
|
113
|
+
*
|
|
114
|
+
* Order matters more than it looks: two shares of the same fields should store
|
|
115
|
+
* the same array, so a list of shares does not read as though they differ.
|
|
116
|
+
*
|
|
117
|
+
* @param {Array<string>} fields
|
|
118
|
+
* @param {string} key
|
|
119
|
+
* @returns {Array<string>}
|
|
120
|
+
*/
|
|
121
|
+
export const toggleShareField = (fields, key) => {
|
|
122
|
+
const current = Array.isArray(fields) ? fields : [];
|
|
123
|
+
const known = SHARE_FIELDS.map((field) => field.key);
|
|
124
|
+
|
|
125
|
+
if (!known.includes(key)) return current.slice();
|
|
126
|
+
|
|
127
|
+
const next = current.includes(key)
|
|
128
|
+
? current.filter((field) => field !== key)
|
|
129
|
+
: current.concat([key]);
|
|
130
|
+
|
|
131
|
+
return known.filter((field) => next.includes(field));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The body the create endpoint wants.
|
|
136
|
+
*
|
|
137
|
+
* `max_views` is omitted rather than sent as null when there is no limit: the
|
|
138
|
+
* server treats an absent key and a null the same way, and a body that carries
|
|
139
|
+
* only what was chosen is easier to read in a network panel.
|
|
140
|
+
*
|
|
141
|
+
* @param {{fields: Array<string>, expiresInHours: number, maxViews: (number|null)}} choice
|
|
142
|
+
* @returns {object}
|
|
143
|
+
*/
|
|
144
|
+
export const shareRequestBody = ({ fields, expiresInHours, maxViews } = {}) => {
|
|
145
|
+
const known = SHARE_FIELDS.map((field) => field.key);
|
|
146
|
+
const clean = known.filter((field) => (fields || []).includes(field));
|
|
147
|
+
|
|
148
|
+
const hours = Number(expiresInHours);
|
|
149
|
+
const views = Number(maxViews);
|
|
150
|
+
|
|
151
|
+
const body = {
|
|
152
|
+
fields: clean,
|
|
153
|
+
expires_in_hours:
|
|
154
|
+
Number.isFinite(hours) && hours > 0
|
|
155
|
+
? Math.round(hours)
|
|
156
|
+
: DEFAULT_EXPIRY_HOURS,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (Number.isFinite(views) && views > 0) {
|
|
160
|
+
body.max_views = Math.round(views);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return body;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* One word for a link's state, matching the server's own vocabulary.
|
|
168
|
+
*
|
|
169
|
+
* Falls back to computing it from the row when the server did not say, so a
|
|
170
|
+
* list rendered from a stale response still labels an expired link as expired
|
|
171
|
+
* rather than as active.
|
|
172
|
+
*/
|
|
173
|
+
export const shareStatus = (share) => {
|
|
174
|
+
const row = share || {};
|
|
175
|
+
|
|
176
|
+
if (typeof row.status === 'string' && row.status !== '') return row.status;
|
|
177
|
+
|
|
178
|
+
if (row.revoked_at) return 'revoked';
|
|
179
|
+
|
|
180
|
+
if (row.expires_at && new Date(row.expires_at).getTime() <= Date.now()) {
|
|
181
|
+
return 'expired';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
row.max_views !== null &&
|
|
186
|
+
row.max_views !== undefined &&
|
|
187
|
+
Number(row.views || 0) >= Number(row.max_views)
|
|
188
|
+
) {
|
|
189
|
+
return 'spent';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return 'active';
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const STATUS_LABELS = {
|
|
196
|
+
active: 'Active',
|
|
197
|
+
expired: 'Expired',
|
|
198
|
+
spent: 'Views used up',
|
|
199
|
+
revoked: 'Revoked',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const shareStatusLabel = (share) =>
|
|
203
|
+
STATUS_LABELS[shareStatus(share)] || 'Unknown';
|
|
204
|
+
|
|
205
|
+
/** Only an active link can be closed; the rest are already closed. */
|
|
206
|
+
export const isShareRevocable = (share) => shareStatus(share) === 'active';
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* How many times this link has been opened, said in English.
|
|
210
|
+
*
|
|
211
|
+
* "0 of 1 view" rather than "0 views" when there is a budget: the number that
|
|
212
|
+
* matters to whoever is looking is how many are left, and stating the ceiling
|
|
213
|
+
* is how that is read off in one glance.
|
|
214
|
+
*/
|
|
215
|
+
export const shareViewsLabel = (share) => {
|
|
216
|
+
const row = share || {};
|
|
217
|
+
const used = Number(row.views || 0);
|
|
218
|
+
|
|
219
|
+
if (row.max_views === null || row.max_views === undefined) {
|
|
220
|
+
return `${used} ${used === 1 ? 'view' : 'views'}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const max = Number(row.max_views);
|
|
224
|
+
|
|
225
|
+
return `${used} of ${max} ${max === 1 ? 'view' : 'views'}`;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The field list as a sentence, for the row in the shares list.
|
|
230
|
+
*
|
|
231
|
+
* Unknown keys are printed rather than dropped: the server is free to grow a
|
|
232
|
+
* shareable field this build has not heard of, and a list with a silent gap in
|
|
233
|
+
* it is worse than one with an ugly word.
|
|
234
|
+
*/
|
|
235
|
+
export const shareFieldsLabel = (share) => {
|
|
236
|
+
const fields = Array.isArray(share?.fields) ? share.fields : [];
|
|
237
|
+
|
|
238
|
+
if (fields.length === 0) return 'Nothing';
|
|
239
|
+
|
|
240
|
+
return fields
|
|
241
|
+
.map((key) => {
|
|
242
|
+
const known = SHARE_FIELDS.find((field) => field.key === key);
|
|
243
|
+
|
|
244
|
+
return known ? known.label : String(key);
|
|
245
|
+
})
|
|
246
|
+
.join(', ');
|
|
247
|
+
};
|
package/src/index.js
CHANGED
|
@@ -88,6 +88,14 @@ import {
|
|
|
88
88
|
syncCsrfToken,
|
|
89
89
|
twoFactorSuccessTarget,
|
|
90
90
|
} from './components/auth/authHelpers';
|
|
91
|
+
import {
|
|
92
|
+
base64UrlToBytes,
|
|
93
|
+
bytesToBase64Url,
|
|
94
|
+
createPasskeyCredential,
|
|
95
|
+
getPasskeyCredential,
|
|
96
|
+
isPasskeyCancellation,
|
|
97
|
+
isPasskeySupported,
|
|
98
|
+
} from './components/auth/webauthn';
|
|
91
99
|
import {
|
|
92
100
|
CLIENT_AUTH_PROTOCOLS,
|
|
93
101
|
DEFAULT_CLIENT_PROTOCOL,
|
|
@@ -104,9 +112,16 @@ import CallQueueSettings from './components/callQueue/CallQueueSettings';
|
|
|
104
112
|
import OtpChip from './components/vault/OtpChip';
|
|
105
113
|
import VaultAccessLog from './components/vault/VaultAccessLog';
|
|
106
114
|
import VaultConfirmPanel from './components/vault/VaultConfirmPanel';
|
|
115
|
+
import GenericSettings from './components/generic/GenericSettings';
|
|
116
|
+
import IntegrationsManager from './components/settings/IntegrationsManager';
|
|
117
|
+
import TicketConversation from './components/tickets/TicketConversation';
|
|
118
|
+
import VaultClientPicker from './components/vault/VaultClientPicker';
|
|
107
119
|
import VaultEntryForm from './components/vault/VaultEntryForm';
|
|
108
120
|
import VaultManager from './components/vault/VaultManager';
|
|
109
121
|
import VaultQuickSearch from './components/vault/VaultQuickSearch';
|
|
122
|
+
// External share links. VaultManager already mounts this from the Share action
|
|
123
|
+
// on a row; it is exported for a consumer that wants the dialog somewhere else.
|
|
124
|
+
import VaultShareModal from './components/vault/VaultShareModal';
|
|
110
125
|
import PasswordGenerator from './components/vault/PasswordGenerator';
|
|
111
126
|
import QrScanner from './components/vault/QrScanner';
|
|
112
127
|
import useVaultReveal from './components/vault/useVaultReveal';
|
|
@@ -206,6 +221,10 @@ import GenericClientPortal from './components/generic/GenericClientPortal';
|
|
|
206
221
|
import GenericDashboard from './components/generic/GenericDashboard';
|
|
207
222
|
import GenericDetail from './components/generic/GenericDetail';
|
|
208
223
|
import GenericDynamic from './components/generic/GenericDynamic';
|
|
224
|
+
import UniversalSearch, {
|
|
225
|
+
DEFAULT_UNIVERSAL_SEARCH_ENDPOINT,
|
|
226
|
+
} from './components/UniversalSearch';
|
|
227
|
+
import ExcelStyleGrid from './components/generic/ExcelStyleGrid';
|
|
209
228
|
import GenericFormBuilder from './components/generic/GenericFormBuilder';
|
|
210
229
|
import GenericIndex from './components/generic/GenericIndex';
|
|
211
230
|
import GenericGrid from './components/generic/GenericGrid';
|
|
@@ -280,9 +299,14 @@ export {
|
|
|
280
299
|
useVaultReveal,
|
|
281
300
|
VaultAccessLog,
|
|
282
301
|
VaultConfirmPanel,
|
|
302
|
+
GenericSettings,
|
|
303
|
+
IntegrationsManager,
|
|
304
|
+
TicketConversation,
|
|
305
|
+
VaultClientPicker,
|
|
283
306
|
VaultEntryForm,
|
|
284
307
|
VaultManager,
|
|
285
308
|
VaultQuickSearch,
|
|
309
|
+
VaultShareModal,
|
|
286
310
|
clientRecipient,
|
|
287
311
|
conversationCountLabel,
|
|
288
312
|
DEFAULT_SMS_ENDPOINTS,
|
|
@@ -313,6 +337,7 @@ export {
|
|
|
313
337
|
threadDisplayName,
|
|
314
338
|
toE164,
|
|
315
339
|
useSmsLive,
|
|
340
|
+
UniversalSearch,
|
|
316
341
|
useSmsThreads,
|
|
317
342
|
CameraPlacement,
|
|
318
343
|
CellWithTooltip,
|
|
@@ -352,6 +377,8 @@ export {
|
|
|
352
377
|
GenericDashboard,
|
|
353
378
|
GenericDetail,
|
|
354
379
|
GenericDynamic,
|
|
380
|
+
DEFAULT_UNIVERSAL_SEARCH_ENDPOINT,
|
|
381
|
+
ExcelStyleGrid,
|
|
355
382
|
GenericFormBuilder,
|
|
356
383
|
GenericIndex,
|
|
357
384
|
GenericGrid,
|
|
@@ -402,6 +429,14 @@ export {
|
|
|
402
429
|
resolveAuthEndpoints,
|
|
403
430
|
resolveClientPaths,
|
|
404
431
|
resolveClientProtocol,
|
|
432
|
+
// Passkeys (WebAuthn). Exported so a consuming app can build its own
|
|
433
|
+
// enrolment screen against the same marshalling the login screen uses.
|
|
434
|
+
base64UrlToBytes,
|
|
435
|
+
bytesToBase64Url,
|
|
436
|
+
createPasskeyCredential,
|
|
437
|
+
getPasskeyCredential,
|
|
438
|
+
isPasskeyCancellation,
|
|
439
|
+
isPasskeySupported,
|
|
405
440
|
SectionEditor,
|
|
406
441
|
SectionGroupedReport,
|
|
407
442
|
SectionTypeSelector,
|
package/src/utils/fetchUtil.js
CHANGED
|
@@ -92,23 +92,47 @@ const fetchUtil = async (config) => {
|
|
|
92
92
|
requestHeaders.set('Content-Type', 'application/json');
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
/*
|
|
96
|
+
* Laravel's session headers are for Laravel — this app's own origin.
|
|
97
|
+
*
|
|
98
|
+
* Attached to every request they also went to third parties: the address
|
|
99
|
+
* autocomplete handed the user's CSRF token to api.mapbox.com on every
|
|
100
|
+
* keystroke. A token another host has no use for and no business seeing,
|
|
101
|
+
* and because they are not CORS-simple headers each request also paid for
|
|
102
|
+
* a preflight it did not need.
|
|
103
|
+
*/
|
|
104
|
+
const isSameOrigin = (() => {
|
|
105
|
+
try {
|
|
106
|
+
return (
|
|
107
|
+
new URL(requestUrl, window.location.href).origin ===
|
|
108
|
+
window.location.origin
|
|
109
|
+
);
|
|
110
|
+
} catch {
|
|
111
|
+
// A url this fetch could not parse is one the browser will reject
|
|
112
|
+
// anyway; treat it as ours rather than silently dropping headers.
|
|
113
|
+
return true;
|
|
103
114
|
}
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
})();
|
|
116
|
+
|
|
117
|
+
if (isSameOrigin) {
|
|
118
|
+
// Add CSRF token for Laravel if available
|
|
119
|
+
const csrfToken = document
|
|
120
|
+
.querySelector('meta[name="csrf-token"]')
|
|
121
|
+
?.getAttribute('content');
|
|
122
|
+
if (csrfToken) {
|
|
123
|
+
// Laravel accepts the CSRF token in different headers
|
|
124
|
+
if (!requestHeaders.has('X-CSRF-TOKEN')) {
|
|
125
|
+
requestHeaders.set('X-CSRF-TOKEN', csrfToken);
|
|
126
|
+
}
|
|
127
|
+
if (!requestHeaders.has('X-XSRF-TOKEN')) {
|
|
128
|
+
requestHeaders.set('X-XSRF-TOKEN', csrfToken);
|
|
129
|
+
}
|
|
106
130
|
}
|
|
107
|
-
}
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
// Add X-Requested-With header for Laravel to recognize AJAX requests
|
|
133
|
+
if (!requestHeaders.has('X-Requested-With')) {
|
|
134
|
+
requestHeaders.set('X-Requested-With', 'XMLHttpRequest');
|
|
135
|
+
}
|
|
112
136
|
}
|
|
113
137
|
|
|
114
138
|
// Handle credentials
|