/**
* The main JavaScript file for the AI4SoilHealthClient application.
* It imports necessary dependencies, sets up configurations, and initializes the Vue app.
* It also defines utility functions for handling Axios responses and errors,
* as well as a function for logging out the user.
* @module main
*/
import { createApp } from 'vue'
import App from './App.vue'
import axios from 'axios'
import {
Quasar,
LocalStorage,
SessionStorage,
Dialog,
Notify
} from 'quasar'
//import iconSet from 'quasar/icon-set/material-symbols-outlined.js'
//import '@quasar/extras/material-symbols-outlined/material-symbols-outlined.css'
import '@quasar/extras/material-icons/material-icons.css'
// additional icons
import { symOutlinedDragPan, symOutlinedArrowSelectorTool, symOutlinedMyLocation }
from '@quasar/extras/material-symbols-outlined'
let icons = {
drag_pan: symOutlinedDragPan,
arrow_selector_tool: symOutlinedArrowSelectorTool,
my_location: symOutlinedMyLocation
}
import 'quasar/src/css/index.sass'
import 'ol/ol.css'
import './css/style.css'
import { store } from "./store.js"
import { GlobalMixin } from "./mixins/global.js"
import { GlobalApiMixin } from "./mixins/global-api.js"
import { GlobalTableMixin } from "./mixins/global-table.js"
import router from './router.js'
import Header from './components/header.vue'
import Keycloak from 'keycloak-js';
import CustomDialog from './components/custom-dialog.vue';
// testing in local network:
// .env:
// VITE_ROOT_API=http://localIP:port/api
// launchsettings.json:
// "applicationUrl": "http://localhost:port"
// applicationhost.config:
// <binding protocol="http" bindingInformation="*:port:*" />
// run:
// iisexpress-proxy port to port
import { createI18n } from 'vue-i18n';
/**
* Internationalization object for language translation.
* @type {object}
*/
const i18n = createI18n({
// locale: store.locale,
globalInjection: true,
silentTranslationWarn: true,
missingWarn: false,
silentFallbackWarn: true,
fallbackWarn: false,
messages: {} //langI.default
});
/**
* Logs out the user by removing the token from local storage and clearing user data.
* If Keycloak is available, it also performs a Keycloak logout.
*/
async function logout() {
app.config.globalProperties.$q.localStorage.remove("token");
store.userData = null;
app.config.globalProperties.$q.localStorage.remove('userData');
if (app.config.globalProperties.$keycloak)
app.config.globalProperties.$keycloak.logout();
router.push({ name: 'Home' });
}
/**
* Handles the response from an Axios request.
* @param {object} response - The response object from Axios.
* @returns {object} - The modified response object.
*/
function handleAxiosResponse(response) {
store.working = false;
if (response.data) {
if (response.data.error) {
app.config.globalProperties.$q.dialog({
component: CustomDialog,
componentProps: {
error: true, title: i18n.global.t("Error"),
message: response.data.error, type: 'Ok'
}
});
return { data: null };
} else if (response.data.message) {
app.config.globalProperties.$q.dialog({
component: CustomDialog,
componentProps: {
error: true, title: i18n.global.t("Message"),
message: response.data.message, type: 'Ok',
persistent: true
}
});
}
}
return response;
}
function handleAxiosError(error) {
store.working = false;
let reason = "";
let expired = false;
if (error.response) {
let response = error.response;
reason = error.message;
if (response.status == 401) {
console.log("Error 401");
// extract www-authenticate header from response
let header = response.headers.get("WWW-Authenticate");
if (header && header.indexOf("expired") > 0) {
expired = true;
} else {
reason = i18n.global.t("Unauthorized");
expired = true;
}
} else {
console.log("Error", response);
// get detailed error message from response.data.errors object
if (response.data.errors) {
reason += '<br>' + Object.values(response.data.errors).join("<br>");
}
}
} else if (error.request) {
if (error.request.status == 0) {
reason = i18n.global.t("No response from server");
store.serverDown = true;
} else {
console.log("Error request", error.request);
expired = true;
}
}
if (expired) reason = i18n.global.t("Session expired - please login again");
app.config.globalProperties.$q.dialog({component: CustomDialog,
componentProps: {
error: true, title: i18n.global.t("Error"),
message: reason, type: 'Ok'
}
}).onDismiss(() => {
if (expired) logout();
});
return { data: null };
}
console.dir(import.meta.env);
let errors = [];
window.onerror = function (messageOrEvent, source, lineno, colno, error) {
let s = "Error:" + messageOrEvent + "\n" +
"Source:" + source + " Line:" + lineno + " Col:" + colno;
errors.push(s);
return true;
}
/**
* Axios instance for making HTTP requests.
*/
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_ROOT_API,
});
/**
* Axios instance for making generic API requests.
*/
const axiosInstanceGeneric = axios.create({
baseURL: "" //import.meta.env.VITE_STAC_API
});
axiosInstance.interceptors.response.use(
(response) => handleAxiosResponse(response),
(error) => handleAxiosError(error)
);
axiosInstance.interceptors.request.use(
(config) => {
if (app.config.globalProperties.$keycloak.token) {
config.headers['Authorization'] = 'Bearer ' + app.config.globalProperties.$keycloak.token;
}
config.headers['LangId'] = store.langId;
return config;
},
(error) => handleAxiosError(error)
);
axiosInstanceGeneric.interceptors.response.use(
(response) => handleAxiosResponse(response),
(error) => handleAxiosError(error)
);
const app = createApp(App);
import mitt from 'mitt';
const emitter = mitt();
app.config.globalProperties.$mitt = emitter;
app.config.globalProperties.axios = {
API: axiosInstance,
//APIAuth: axiosInstanceAuthorized,
APIGen: axiosInstanceGeneric
};
let authenticated = false;
app.config.errorHandler = function (err, vm, info) {
console.error(`Error: ${err.toString()}\nInfo: ${info}`)
if (err.stack) {
const stack = err.stack.split('\n')[1].trim()
const [moduleName, lineNo, colNo] = stack.match(/at\s+(.+):(\d+):(\d+)/).slice(1)
let s = "Error:" + err.message + "\n" +
"Source:" + moduleName + " Line:" + lineNo + " Col:" + colNo;
console.error(`Module: ${moduleName}, Line: ${lineNo}, Column: ${colNo}`)
errors.push(s);
}
}
app.config.globalProperties.$icons = icons;
app.config.globalProperties.$logout = logout;
app.config.globalProperties.$errors = errors;
app.config.globalProperties.$store = store;
app.config.globalProperties.$store.version = GlobalMixin.methods.cleanDateTime(import.meta.env.VITE_BUILD);
app.mixin(GlobalMixin);
app.mixin(GlobalApiMixin);
app.mixin(GlobalTableMixin);
router.app = app;
app.use(router);
//import Vue3TouchEvents from 'vue3-touch-events'
//app.use(Vue3TouchEvents);
// import { GesturePlugin } from '@vueuse/gesture'
// app.use(GesturePlugin);
app.use(Quasar, {
plugins: {
LocalStorage,
SessionStorage,
Dialog,
Notify
}, // import Quasar plugins and add here
lang: {}, //langQ.default,
//iconSet: iconSet,
config: {
dark: false,
brand: {
primary: '#315440',
//background: 'rgba(49, 84, 64, 0.15)',
//background: rgb(224, 229, 226),
background: '#E0E5E2',
tooltip: 'rgba(49, 84, 64, 0.85)',
secondary: '#26A69A',
accent: '#9C27B0',
dark: '#1d1d1d',
'dark-page': '#121212',
positive: '#21BA45',
negative: '#C10015',
info: '#31CCEC',
warning: '#F2C037'
}
},
// extras: [
// 'material-symbols-outlined',
// ]
})
// Tell app to use the I18n instance
app.use(i18n);
app.component('Header', Header);
// async function checkOnlineStatus() {
// try {
// const response = await fetch('https://www.google.com/', {
// method: 'HEAD',
// mode: 'no-cors'
// });
// return response && (response.ok || response.type === 'opaque');
// } catch (error) {
// return false;
// }
// }
// if(navigator.isOnline){
// // checkOnlineStatus().then(isOnline => {
// // store.isOnline = isOnline
// // });
// (async () => {
// store.isOnline = await checkOnlineStatus();
// })();
// }
function checkOnlineStatusSync() {
const xhr = new XMLHttpRequest();
xhr.open('GET', import.meta.env.VITE_ROOT_API + 'Home/Ping', false); // false za synchronous
try {
xhr.send();
return xhr.status >= 200 && xhr.status < 300;
} catch (error) {
return false;
}
}
if(navigator.onLine){
store.isOnline = checkOnlineStatusSync();
}
console.log(`Main: navigator.onLine: ${navigator.onLine}`);
console.log(`Main: store.isOnline: ${store.isOnline}`);
let keycloak = new Keycloak({
url: import.meta.env.VITE_KEYCLOAK_URL,
realm: import.meta.env.VITE_KEYCLOAK_REALM,
clientId: import.meta.env.VITE_KEYCLOAK_CLIENTID
});
function saveTokens() {
app.config.globalProperties.$q.localStorage.setItem('accessToken', keycloak.token);
// localStorage.setItem('user-token', keycloak.token);
app.config.globalProperties.$q.localStorage.setItem('refreshToken', keycloak.refreshToken);
app.config.globalProperties.$q.localStorage.setItem('tokenExpiry', Date.now() + keycloak.tokenParsed.exp * 1000);
}
function getStoredTokens() {
return {
accessToken: app.config.globalProperties.$q.localStorage.getItem('accessToken'),
// accessToken: localStorage.getItem('user-token'),
refreshToken: app.config.globalProperties.$q.localStorage.getItem('refreshToken'),
tokenExpiry: app.config.globalProperties.$q.localStorage.getItem('tokenExpiry')
};
}
function startTokenRefresh() {
setInterval(() => {
if (navigator.onLine) {
keycloak.updateToken(60).then(refreshed => {
if (refreshed) {
saveTokens();
console.log('Token refreshed');
}
}).catch(() => {
console.log('Failed to refresh token, logging in');
// keycloak.login();
});
}
}, 60000);
}
let isKeycloakInitialized = false;
let isAppInitialized = false;
function initKeycloak() {
const tokens = getStoredTokens();
if(store.isOnline){
if (tokens.accessToken && Date.now() < tokens.tokenExpiry) {
keycloak.init({
onLoad: 'check-sso',
// onLoad: store.isOnline ? 'check-sso' : undefined,
token: tokens.accessToken,
refreshToken: tokens.refreshToken,
enableLogging: true,
checkLoginIframe: true
// checkLoginIframe: store.isOnline
}).then(auth => {
isKeycloakInitialized = true;
if (auth) {
console.log('Authenticated with stored token');
saveTokens();
} else {
console.log('Not authenticated');
logout();
}
// Token refresh
startTokenRefresh();
}).catch(error => {
console.error('Failed to initialize Keycloak', error);
// keycloak.login();
}).finally(() => {
app.config.globalProperties.$keycloak = keycloak;
if(!isAppInitialized){
app.mount("#app");
isAppInitialized = true;
}
});
} else {
keycloak.init({
// onLoad: 'check-sso',
onLoad: store.isOnline ? 'check-sso' : undefined,
enableLogging: true,
// checkLoginIframe: true
checkLoginIframe: store.isOnline
}).then(auth => {
isKeycloakInitialized = true;
if (auth) {
saveTokens();
startTokenRefresh();
} else {
console.log('Not authenticated');
logout();
}
}).catch(error => {
console.error('Failed to initialize Keycloak', error);
}).finally(() => {
app.config.globalProperties.$keycloak = keycloak;
if(!isAppInitialized){
app.mount("#app");
isAppInitialized = true;
}
});
}
} else {
// Handle offline mode
console.log('Offline, skipping Keycloak initialization');
// if(pp.config.globalProperties.$q.localStorage.get('userData')){
app.config.globalProperties.$keycloak = keycloak;
if(!isAppInitialized){
app.mount("#app");
isAppInitialized = true;
}
// }
}
// if(!isAppInitialized){
// app.mount("#app");
// app.config.globalProperties.$keycloak = keycloak;
// isAppInitialized = true;
// }
}
initKeycloak();
// function initKeycloak(online) {
// let keycloak = new Keycloak({
// url: import.meta.env.VITE_KEYCLOAK_URL,
// realm: import.meta.env.VITE_KEYCLOAK_REALM,
// clientId: import.meta.env.VITE_KEYCLOAK_CLIENTID
// });
// keycloak.init({
// onLoad: 'check-sso',
// // onLoad: online ? 'check-sso' : undefined,
// enableLogging: true,
// checkLoginIframe: true
// // checkLoginIframe: online
// }).then((auth) => {
// if (!auth) {
// authenticated = false;
// logout();
// } else {
// localStorage.setItem('user-token', keycloak.token);
// authenticated = true;
// }
// //Token Refresh
// setInterval(() => {
// keycloak.updateToken(60).then((refreshed) => {
// if (refreshed) {
// localStorage.setItem('user-token', keycloak.token);
// }
// }).catch(() => {
// });
// }, 6000);
// }).catch((error) => {
// }).finally(() => {
// app.config.globalProperties.$keycloak = keycloak;
// app.mount("#app");
// });
// }
// initKeycloak(navigator.online);
// console.log(`Main: $store.isOnline: ${store.isOnline}`);
// store.isOnline = navigator.onLine;
// forced software updates check
function checkForSWUpdates() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration().then(registration => {
if (registration) {
registration.update();
}
});
}
}
setInterval(checkForSWUpdates, 60000); // check every 1min
//
window.addEventListener('online', () => {
store.isOnline = true;
// if(!isKeycloakInitialized){
initKeycloak();
// }
// initKeycloak(store.isOnline);
});
window.addEventListener('offline', () => {
store.isOnline = false;
// initKeycloak(store.isOnline);
});
//frame-src 'self'; frame-ancestors 'self' http://localhost:8080 http://161.53.18.28:8080 https://app.ai4soilhealth.eu; object-src 'none';