1import Axios from 'axios'; 2import router from '../router'; 3import { setupCache, buildWebStorage } from 'axios-cache-interceptor'; 4import { toRaw } from 'vue'; 5 6//Do not change store import. 7//Exact match alias set to support 8//dotenv customizations. 9import store from '.'; 10 11Axios.defaults.headers.common['Accept'] = 'application/json'; 12Axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 13 14const axiosInstance = Axios.create({ 15 withCredentials: true, 16}); 17 18const api = setupCache(axiosInstance, { 19 debug: process.env.NODE_ENV === 'development' ? console.log : undefined, 20 methods: ['get'], 21 interpretHeader: false, 22 etag: true, 23 modifiedSince: false, 24 staleIfError: false, 25 ttl: 0, 26 storage: buildWebStorage(localStorage, 'webui-vue-cache:'), 27}); 28 29/** 30 * Strip Vue 3 reactivity properties and Event objects from request payloads. 31 * 32 * Vue 3 Migration Issue: 33 * In Bootstrap-Vue-Next, @change events pass Event objects instead of values. 34 * When reactive form data accidentally includes Event properties (isTrusted, 35 * _vts, etc.), the API receives malformed payloads like: 36 * {"LocationIndicatorActive": {"isTrusted": true, "_vts": 1765562875420}} 37 * instead of: 38 * {"LocationIndicatorActive": true} 39 * 40 * This function provides a safety net by: 41 * 1. Unwrapping Vue Proxy objects using toRaw() 42 * 2. Detecting Event-like objects and logging warnings 43 * 3. Using JSON round-trip to strip non-serializable properties 44 */ 45function stripVueReactivity(obj) { 46 if (obj === null || obj === undefined) return obj; 47 48 // Get raw value (unwrap Vue Proxy) 49 const raw = toRaw(obj); 50 51 // Return primitives as-is 52 if (typeof raw !== 'object') return raw; 53 54 // Skip binary/upload types - they must be passed through unchanged 55 // These types lose their data when JSON serialized 56 if ( 57 raw instanceof File || 58 raw instanceof Blob || 59 raw instanceof FormData || 60 ArrayBuffer.isView(raw) || 61 raw instanceof ArrayBuffer 62 ) { 63 return raw; 64 } 65 66 // Detect Event objects that were accidentally passed as values 67 // This happens when @change handlers pass the event instead of the value 68 if (raw instanceof Event || typeof raw.isTrusted === 'boolean') { 69 console.warn( 70 'API payload contains Event object - check component @change handlers:', 71 raw, 72 ); 73 } 74 75 // For plain objects, use JSON round-trip to strip non-serializable properties 76 // This removes Vue internal properties (_vts, __v_isRef, etc.) and Event metadata 77 try { 78 return JSON.parse(JSON.stringify(raw)); 79 } catch (e) { 80 // If serialization fails (circular refs, etc.), log and return raw 81 console.warn('Could not serialize API payload:', e); 82 return raw; 83 } 84} 85 86// Add request interceptor to strip Vue reactivity from payloads 87api.interceptors.request.use( 88 (config) => { 89 if (config.data) { 90 config.data = stripVueReactivity(config.data); 91 } 92 return config; 93 }, 94 (error) => Promise.reject(error), 95); 96 97api.interceptors.response.use( 98 (response) => { 99 return response; 100 }, 101 (error) => { 102 const response = error?.response; 103 const status = response?.status; 104 105 if (!status) return Promise.reject(error); 106 107 // Auth handling 108 if (status == 401) { 109 if (response.config.url != '/login') { 110 store.commit('authentication/logout'); 111 router.push('/login'); 112 } 113 } 114 115 if (status == 403) { 116 if (isPasswordExpired(response.data)) { 117 router.push('/change-password'); 118 } else { 119 store.commit('global/setUnauthorized'); 120 } 121 } 122 123 return Promise.reject(error); 124 }, 125); 126 127export default { 128 get(path, config) { 129 return api.get(path, config); 130 }, 131 delete(path, config) { 132 return api.delete(path, config); 133 }, 134 post(path, payload, config) { 135 return api.post(path, payload, config); 136 }, 137 patch(path, payload, config) { 138 return api.patch(path, payload, config); 139 }, 140 put(path, payload, config) { 141 return api.put(path, payload, config); 142 }, 143 all(promises) { 144 return Axios.all(promises); 145 }, 146 spread(callback) { 147 return Axios.spread(callback); 148 }, 149 set_auth_token(token) { 150 axiosInstance.defaults.headers.common['X-Auth-Token'] = token; 151 }, 152}; 153 154export const getResponseCount = (responses) => { 155 let successCount = 0; 156 let errorCount = 0; 157 158 responses.forEach((response) => { 159 if (response instanceof Error) errorCount++; 160 else successCount++; 161 }); 162 163 return { 164 successCount, 165 errorCount, 166 }; 167}; 168 169export const isPasswordExpired = (data) => { 170 return !!findMessageId(data, 'PasswordChangeRequired'); 171}; 172 173/** 174 * Returns the first ExtendedInfo.Message to start with the 175 * Registry Name (Default: "Base") and end with the given key 176 * Ignore versions (.<X>.<Y>) --or-- (.<X>.<Y>.<Z>.), 177 * but adhere to Registry namespace 178 * @param {object} data - AxiosResponse.data 179 * @param { {MessageKey: string}} key - key into the message registry 180 * @param { {MessageRegistryPrefix: string}} [registry=Base] - the name of the 181 * message registry, undefined param defaults to "Base" 182 * @returns {ExtendedInfo.Message} ExtendedInfo.Message | undefined 183 */ 184export const findMessageId = (data, key, registry = 'Base') => { 185 let extInfoMsgs = data?.['@Message.ExtendedInfo']; 186 187 return ( 188 extInfoMsgs && 189 extInfoMsgs.find((i) => { 190 const words = i.MessageId.split('.'); 191 return words[words.length - 1] === key && words[0] === registry; 192 }) 193 ); 194}; 195