/**
* 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 '@quasar/extras/material-icons/material-icons.css'
import 'quasar/src/css/index.sass'
import './style.css'
import '../specific/style.css'
import { store } from "./store.js"
import { GlobalMixin } from "./mixins/global.js"
import { GlobalApiMixin } from "./mixins/global-api.js"
import router from '../router.js'
import Keycloak from 'keycloak-js';
import { icons } from '../specific/additional-imports.js'
// common extra icons
import { symOutlinedTextToSpeech } from '@quasar/extras/material-symbols-outlined'
icons.text_to_speech = symOutlinedTextToSpeech;
import { loadComponent } from './component-loader.js'
let Header = loadComponent('header');
let HelpButton = loadComponent('help-button');
let CustomDialog = loadComponent('custom-dialog');
// 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({
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.
*/
function logout() {
console.log('Logging out');
// if (store.userData) {
store.userData = null;
app.config.globalProperties.$q.localStorage.remove("token");
app.config.globalProperties.$q.localStorage.remove('userData');
if (app.config.globalProperties.$keycloak) {
app.config.globalProperties.$keycloak.logout();
app.config.globalProperties.$keycloak.token = null;
}
// }
}
/**
* Handles the response from an Axios request.
* @param {object} response - The response object from Axios.
* @returns {object} - The modified response object.
*/
async function handleAxiosResponse(response) {
store.working = false;
if (response.data) {
if (response.data.error) {
await 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) {
await 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;
}
async function handleAxiosError(error) {
store.working = false;
let reason = "";
let expired = false;
console.log("Error", error);
if (error.response) {
let response = error.response;
if (response.data instanceof Blob) {
let reader = new FileReader();
reader.onload = function () {
reason = JSON.parse(reader.result);
let message = JSON.parse(reason.message);
app.config.globalProperties.$q.dialog({
component: CustomDialog,
componentProps: {
error: true, title: i18n.global.t("Error"), message: message.error, type: 'Ok'
}
});
}
await reader.readAsText(response.data);
} else if (response.status == 401) {
// extract www-authenticate header from response
//let header = response.headers.get("WWW-Authenticate");
let header = response.headers["www-authenticate"];
if (header && header.indexOf("expired") > 0) {
reason = i18n.global.t("Session expired - please login again");
expired = true;
} else {
reason = i18n.global.t("Unauthorized - please login again");
expired = true;
}
} else if (response.status == 429) {
reason = i18n.global.t("Too many requests in a short time. Please try again a bit later.");
} else {
console.log("Error", response);
reason = error.message;
// 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) {
reason = i18n.global.t("Error request") + ": " + error; //i18n.global.t("No response from server");
//logout();
return { data: null };
// expired = true;
} else {
reason = i18n.global.t("Error else") + ": " + error;
}
if (reason > "") {
await app.config.globalProperties.$q.dialog({
component: CustomDialog,
componentProps: {
error: true, title: i18n.global.t("Error"), message: reason, type: 'Ok'
}
});
}
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 && app.config.globalProperties.$keycloak.token) {
//console.log('Adding token to request', app.config.globalProperties.$keycloak.token);
config.headers['Authorization'] = 'Bearer ' + app.config.globalProperties.$keycloak.token;
}
config.headers['LangId'] = store.langId;
if (store.EU) config.headers['EU'] = store.EU.value;
return config;
},
(error) => handleAxiosError(error)
);
axiosInstanceGeneric.interceptors.response.use(
(response) => handleAxiosResponse(response),
(error) => handleAxiosError(error)
);
const app = createApp(App);
app.config.globalProperties.axios = {
API: axiosInstance,
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);
import packageJson from '../../package.json';
app.config.globalProperties.$store.version = packageJson.version; //import.meta.env.VITE_BUILD;
app.mixin(GlobalMixin);
app.mixin(GlobalApiMixin);
router.app = app;
app.use(router);
app.use(Quasar, {
plugins: {
LocalStorage,
SessionStorage,
Dialog,
Notify
},
lang: {},
config: store.config,
// extras: ['material-icons-outlined']
})
// Tell app to use the I18n instance
app.use(i18n);
app.component('Header', Header);
app.component('HelpButton', HelpButton);
app.component('CustomDialog', CustomDialog);
store.app = app;
store.q = app.config.globalProperties.$q;
function checkOnlineStatusSync() {
const xhr = new XMLHttpRequest();
xhr.open('GET', import.meta.env.VITE_ROOT_API + 'CommonAnon/Ping', false); // false za synchronous
try {
xhr.send();
console.log('Checking online status', xhr.status);
return xhr.status >= 200 && xhr.status < 300;
} catch (error) {
console.log('Checking online status', 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;
}
// }
}
}
console.log("main isOnline: " + store.isOnline);
initKeycloak();
// 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;
console.log(`Main: online ${navigator.onLine}`);
// if(!isKeycloakInitialized){
initKeycloak();
// }
// initKeycloak(store.isOnline);
});
window.addEventListener('offline', () => {
console.log(`Main: offline ${navigator.onLine}`);
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';