// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import Cookies from 'js-cookie' import message from 'ant-design-vue/es/message' import notification from 'ant-design-vue/es/notification' import semver from 'semver' import { vueProps } from '@/vue-app' import router from '@/router' import store from '@/store' import { oauthlogin, login, logout, api } from '@/api' import { i18n } from '@/locales' import { axios } from '../../utils/request' import { getParsedVersion } from '@/utils/util' import { ACCESS_TOKEN, CURRENT_PROJECT, DEFAULT_THEME, APIS, ZONES, SHOW_SECURTIY_GROUPS, TIMEZONE_OFFSET, USE_BROWSER_TIMEZONE, HEADER_NOTICES, DOMAIN_STORE, DARK_MODE, CUSTOM_COLUMNS, OAUTH_DOMAIN, OAUTH_PROVIDER, LATEST_CS_VERSION } from '@/store/mutation-types' const user = { state: { token: '', name: '', avatar: '', info: {}, apis: {}, features: {}, project: {}, headerNotices: [], isLdapEnabled: false, cloudian: {}, zones: {}, timezoneoffset: 0.0, usebrowsertimezone: false, domainStore: {}, darkMode: false, defaultListViewPageSize: 20, countNotify: 0, loginFlag: false, logoutFlag: false, customColumns: {}, shutdownTriggered: false, twoFaEnabled: false, twoFaProvider: '', twoFaIssuer: '', customHypervisorName: 'Custom', readyForShutdownPollingJob: '' }, mutations: { SET_TOKEN: (state, token) => { state.token = token }, SET_TIMEZONE_OFFSET: (state, timezoneoffset) => { vueProps.$localStorage.set(TIMEZONE_OFFSET, timezoneoffset) state.timezoneoffset = timezoneoffset }, SET_USE_BROWSER_TIMEZONE: (state, bool) => { vueProps.$localStorage.set(USE_BROWSER_TIMEZONE, bool) state.usebrowsertimezone = bool }, SET_PROJECT: (state, project = {}) => { vueProps.$localStorage.set(CURRENT_PROJECT, project) state.project = project }, SET_NAME: (state, name) => { state.name = name }, SET_AVATAR: (state, avatar) => { state.avatar = avatar }, SET_INFO: (state, info) => { state.info = info }, SET_APIS: (state, apis) => { state.apis = apis vueProps.$localStorage.set(APIS, apis) }, SET_FEATURES: (state, features) => { state.features = features }, SET_HEADER_NOTICES: (state, noticeJsonArray) => { vueProps.$localStorage.set(HEADER_NOTICES, noticeJsonArray) state.headerNotices = noticeJsonArray }, SET_LDAP: (state, isLdapEnabled) => { state.isLdapEnabled = isLdapEnabled }, SET_CLOUDIAN: (state, cloudian) => { state.cloudian = cloudian }, RESET_THEME: (state) => { vueProps.$localStorage.set(DEFAULT_THEME, 'light') }, SET_ZONES: (state, zones) => { state.zones = zones vueProps.$localStorage.set(ZONES, zones) }, SET_SHOW_SECURITY_GROUPS: (state, show) => { state.showSecurityGroups = show vueProps.$localStorage.set(SHOW_SECURTIY_GROUPS, show) }, SET_DOMAIN_STORE (state, domainStore) { state.domainStore = domainStore vueProps.$localStorage.set(DOMAIN_STORE, domainStore) }, SET_DARK_MODE (state, darkMode) { state.darkMode = darkMode vueProps.$localStorage.set(DARK_MODE, darkMode) }, SET_DEFAULT_LISTVIEW_PAGE_SIZE: (state, defaultListViewPageSize) => { state.defaultListViewPageSize = defaultListViewPageSize }, SET_COUNT_NOTIFY (state, number) { state.countNotify = number }, SET_CUSTOM_COLUMNS: (state, customColumns) => { vueProps.$localStorage.set(CUSTOM_COLUMNS, customColumns) state.customColumns = customColumns }, SET_SHUTDOWN_TRIGGERED: (state, shutdownTriggered) => { state.shutdownTriggered = shutdownTriggered }, SET_LOGOUT_FLAG: (state, flag) => { state.logoutFlag = flag }, SET_2FA_ENABLED: (state, flag) => { state.twoFaEnabled = flag }, SET_2FA_PROVIDER: (state, flag) => { state.twoFaProvider = flag }, SET_2FA_ISSUER: (state, flag) => { state.twoFaIssuer = flag }, SET_LOGIN_FLAG: (state, flag) => { state.loginFlag = flag }, SET_CUSTOM_HYPERVISOR_NAME (state, name) { state.customHypervisorName = name }, SET_READY_FOR_SHUTDOWN_POLLING_JOB: (state, job) => { state.readyForShutdownPollingJob = job }, SET_DOMAIN_USED_TO_LOGIN: (state, domain) => { vueProps.$localStorage.set(OAUTH_DOMAIN, domain) }, SET_OAUTH_PROVIDER_USED_TO_LOGIN: (state, provider) => { vueProps.$localStorage.set(OAUTH_PROVIDER, provider) }, SET_LATEST_VERSION: (state, version) => { if (version?.fetchedTs > 0) { vueProps.$localStorage.set(LATEST_CS_VERSION, version) state.latestVersion = version } } }, actions: { SetProject ({ commit }, project) { commit('SET_PROJECT', project) }, Login ({ commit }, userInfo) { return new Promise((resolve, reject) => { login(userInfo).then(response => { const result = response.loginresponse || {} Cookies.set('account', result.account, { expires: 1 }) Cookies.set('domainid', result.domainid, { expires: 1 }) Cookies.set('role', result.type, { expires: 1 }) Cookies.set('timezone', result.timezone, { expires: 1 }) Cookies.set('timezoneoffset', result.timezoneoffset, { expires: 1 }) Cookies.set('userfullname', result.firstname + ' ' + result.lastname, { expires: 1 }) Cookies.set('userid', result.userid, { expires: 1 }) Cookies.set('username', result.username, { expires: 1 }) vueProps.$localStorage.set(ACCESS_TOKEN, result.sessionkey, 24 * 60 * 60 * 1000) commit('SET_TOKEN', result.sessionkey) commit('SET_TIMEZONE_OFFSET', result.timezoneoffset) const cachedUseBrowserTimezone = vueProps.$localStorage.get(USE_BROWSER_TIMEZONE, false) commit('SET_USE_BROWSER_TIMEZONE', cachedUseBrowserTimezone) const darkMode = vueProps.$localStorage.get(DARK_MODE, false) commit('SET_DARK_MODE', darkMode) const cachedCustomColumns = vueProps.$localStorage.get(CUSTOM_COLUMNS, {}) commit('SET_CUSTOM_COLUMNS', cachedCustomColumns) commit('SET_APIS', {}) commit('SET_NAME', '') commit('SET_AVATAR', '') commit('SET_INFO', {}) commit('SET_PROJECT', {}) commit('SET_HEADER_NOTICES', []) commit('SET_FEATURES', {}) commit('SET_LDAP', {}) commit('SET_CLOUDIAN', {}) commit('SET_DOMAIN_STORE', {}) commit('SET_LOGOUT_FLAG', false) commit('SET_2FA_ENABLED', (result.is2faenabled === 'true')) commit('SET_2FA_PROVIDER', result.providerfor2fa) commit('SET_2FA_ISSUER', result.issuerfor2fa) commit('SET_LOGIN_FLAG', false) const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 }) commit('SET_LATEST_VERSION', latestVersion) notification.destroy() resolve() }).catch(error => { reject(error) }) }) }, OauthLogin ({ commit }, userInfo) { return new Promise((resolve, reject) => { oauthlogin(userInfo).then(response => { const result = response.loginresponse || {} Cookies.set('account', result.account, { expires: 1 }) Cookies.set('domainid', result.domainid, { expires: 1 }) Cookies.set('role', result.type, { expires: 1 }) Cookies.set('timezone', result.timezone, { expires: 1 }) Cookies.set('timezoneoffset', result.timezoneoffset, { expires: 1 }) Cookies.set('userfullname', result.firstname + ' ' + result.lastname, { expires: 1 }) Cookies.set('userid', result.userid, { expires: 1 }) Cookies.set('username', result.username, { expires: 1 }) vueProps.$localStorage.set(ACCESS_TOKEN, result.sessionkey, 24 * 60 * 60 * 1000) commit('SET_TOKEN', result.sessionkey) commit('SET_TIMEZONE_OFFSET', result.timezoneoffset) const cachedUseBrowserTimezone = vueProps.$localStorage.get(USE_BROWSER_TIMEZONE, false) commit('SET_USE_BROWSER_TIMEZONE', cachedUseBrowserTimezone) const darkMode = vueProps.$localStorage.get(DARK_MODE, false) commit('SET_DARK_MODE', darkMode) const cachedCustomColumns = vueProps.$localStorage.get(CUSTOM_COLUMNS, {}) commit('SET_CUSTOM_COLUMNS', cachedCustomColumns) commit('SET_APIS', {}) commit('SET_NAME', '') commit('SET_AVATAR', '') commit('SET_INFO', {}) commit('SET_PROJECT', {}) commit('SET_HEADER_NOTICES', []) commit('SET_FEATURES', {}) commit('SET_LDAP', {}) commit('SET_CLOUDIAN', {}) commit('SET_DOMAIN_STORE', {}) commit('SET_LOGOUT_FLAG', false) commit('SET_2FA_ENABLED', (result.is2faenabled === 'true')) commit('SET_2FA_PROVIDER', result.providerfor2fa) commit('SET_2FA_ISSUER', result.issuerfor2fa) commit('SET_LOGIN_FLAG', false) const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 }) commit('SET_LATEST_VERSION', latestVersion) notification.destroy() resolve() }).catch(error => { reject(error) }) }) }, GetInfo ({ commit }, switchDomain) { return new Promise((resolve, reject) => { const cachedApis = switchDomain ? {} : vueProps.$localStorage.get(APIS, {}) const cachedZones = vueProps.$localStorage.get(ZONES, []) const cachedTimezoneOffset = vueProps.$localStorage.get(TIMEZONE_OFFSET, 0.0) const cachedUseBrowserTimezone = vueProps.$localStorage.get(USE_BROWSER_TIMEZONE, false) const cachedCustomColumns = vueProps.$localStorage.get(CUSTOM_COLUMNS, {}) const domainStore = vueProps.$localStorage.get(DOMAIN_STORE, {}) const cachedShowSecurityGroups = vueProps.$localStorage.get(SHOW_SECURTIY_GROUPS, false) const darkMode = vueProps.$localStorage.get(DARK_MODE, false) const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 }) const hasAuth = Object.keys(cachedApis).length > 0 commit('SET_DOMAIN_STORE', domainStore) commit('SET_DARK_MODE', darkMode) commit('SET_LATEST_VERSION', latestVersion) if (hasAuth) { console.log('Login detected, using cached APIs') commit('SET_ZONES', cachedZones) commit('SET_SHOW_SECURITY_GROUPS', cachedShowSecurityGroups) commit('SET_APIS', cachedApis) commit('SET_TIMEZONE_OFFSET', cachedTimezoneOffset) commit('SET_USE_BROWSER_TIMEZONE', cachedUseBrowserTimezone) commit('SET_CUSTOM_COLUMNS', cachedCustomColumns) // Ensuring we get the user info so that store.getters.user is never empty when the page is freshly loaded api('listUsers', { id: Cookies.get('userid'), listall: true }).then(response => { const result = response.listusersresponse.user[0] commit('SET_INFO', result) commit('SET_NAME', result.firstname + ' ' + result.lastname) resolve(cachedApis) }).catch(error => { reject(error) }) } else if (store.getters.loginFlag) { const hide = message.loading(i18n.global.t('message.discovering.feature'), 0) api('listZones').then(json => { const zones = json.listzonesresponse.zone || [] commit('SET_ZONES', zones) }).catch(error => { reject(error) }) api('listApis').then(response => { const apis = {} const apiList = response.listapisresponse.api for (var idx = 0; idx < apiList.length; idx++) { const api = apiList[idx] const apiName = api.name apis[apiName] = { params: api.params, response: api.response, isasync: api.isasync, since: api.since, description: api.description } } commit('SET_APIS', apis) resolve(apis) store.dispatch('GenerateRoutes', { apis }).then(() => { store.getters.addRouters.map(route => { router.addRoute(route) }) }) hide() message.success(i18n.global.t('message.sussess.discovering.feature')) }).catch(error => { reject(error) }) api('listNetworks', { restartrequired: true, forvpc: false }).then(response => { if (response.listnetworksresponse.count > 0) { store.dispatch('AddHeaderNotice', { key: 'NETWORK_RESTART_REQUIRED', title: i18n.global.t('label.network.restart.required'), description: i18n.global.t('message.network.restart.required'), path: '/guestnetwork/', query: { restartrequired: true, forvpc: false }, status: 'done', timestamp: new Date() }) } }).catch(ignored => {}) api('listVPCs', { restartrequired: true }).then(response => { if (response.listvpcsresponse.count > 0) { store.dispatch('AddHeaderNotice', { key: 'VPC_RESTART_REQUIRED', title: i18n.global.t('label.vpc.restart.required'), description: i18n.global.t('message.vpc.restart.required'), path: '/vpc/', query: { restartrequired: true }, status: 'done', timestamp: new Date() }) } }).catch(ignored => {}) } api('listUsers', { id: Cookies.get('userid') }).then(response => { const result = response.listusersresponse.user[0] commit('SET_INFO', result) commit('SET_NAME', result.firstname + ' ' + result.lastname) store.dispatch('SetCsLatestVersion', result.rolename) }).catch(error => { reject(error) }) api( 'listNetworkServiceProviders', { name: 'SecurityGroupProvider', state: 'Enabled' } ).then(response => { const showSecurityGroups = response.listnetworkserviceprovidersresponse.count > 0 commit('SET_SHOW_SECURITY_GROUPS', showSecurityGroups) }).catch(ignored => { }) api('listCapabilities').then(response => { const result = response.listcapabilitiesresponse.capability commit('SET_FEATURES', result) if (result && result.defaultuipagesize) { commit('SET_DEFAULT_LISTVIEW_PAGE_SIZE', result.defaultuipagesize) } if (result && result.customhypervisordisplayname) { commit('SET_CUSTOM_HYPERVISOR_NAME', result.customhypervisordisplayname) } if (result && result.securitygroupsenabled) { commit('SET_SHOW_SECURITY_GROUPS', result.securitygroupsenabled) } }).catch(error => { reject(error) }) api('listLdapConfigurations').then(response => { const ldapEnable = (response.ldapconfigurationresponse.count > 0) commit('SET_LDAP', ldapEnable) }).catch(error => { reject(error) }) api('cloudianIsEnabled').then(response => { const cloudian = response.cloudianisenabledresponse.cloudianisenabled || {} commit('SET_CLOUDIAN', cloudian) }).catch(ignored => { }) }).catch(error => { console.error(error) }) }, Logout ({ commit, state }) { return new Promise((resolve) => { var cloudianUrl = null if (state.cloudian.url && state.cloudian.enabled) { cloudianUrl = state.cloudian.url + 'logout.htm?redirect=' + encodeURIComponent(window.location.href) } commit('SET_TOKEN', '') commit('SET_APIS', {}) commit('SET_PROJECT', {}) commit('SET_HEADER_NOTICES', []) commit('SET_FEATURES', {}) commit('SET_LDAP', {}) commit('SET_CLOUDIAN', {}) commit('RESET_THEME') commit('SET_DOMAIN_STORE', {}) commit('SET_LOGOUT_FLAG', true) commit('SET_2FA_ENABLED', false) commit('SET_2FA_PROVIDER', '') commit('SET_2FA_ISSUER', '') commit('SET_LOGIN_FLAG', false) vueProps.$localStorage.remove(CURRENT_PROJECT) vueProps.$localStorage.remove(ACCESS_TOKEN) vueProps.$localStorage.remove(HEADER_NOTICES) logout(state.token).then(() => { message.destroy() if (cloudianUrl) { window.location.href = cloudianUrl } else { resolve() } }).catch(() => { resolve() }).finally(() => { const paths = ['/', '/client'] const hostname = window.location.hostname const domains = [undefined, hostname, `.${hostname}`] Object.keys(Cookies.get()).forEach(cookieName => { paths.forEach(path => { domains.forEach(domain => { const options = { path } if (domain) options.domain = domain Cookies.remove(cookieName, options) }) }) }) }) }) }, AddHeaderNotice ({ commit }, noticeJson) { if (!noticeJson || !noticeJson.title) { return } const noticeArray = vueProps.$localStorage.get(HEADER_NOTICES, []) const noticeIdx = noticeArray.findIndex(notice => notice.key === noticeJson.key) if (noticeIdx === -1) { noticeArray.push(noticeJson) } else { const existingNotice = noticeArray[noticeIdx] noticeJson.timestamp = existingNotice.timestamp noticeArray[noticeIdx] = noticeJson } noticeArray.sort(function (a, b) { return new Date(b.timestamp) - new Date(a.timestamp) }) commit('SET_HEADER_NOTICES', noticeArray) }, ProjectView ({ commit }, projectid) { return new Promise((resolve, reject) => { api('listApis', { projectid: projectid }).then(response => { const apis = {} const apiList = response.listapisresponse.api for (var idx = 0; idx < apiList.length; idx++) { const api = apiList[idx] const apiName = api.name apis[apiName] = { params: api.params, response: api.response } } commit('SET_APIS', apis) resolve(apis) store.dispatch('GenerateRoutes', { apis }).then(() => { store.getters.addRouters.map(route => { router.addRoute(route) }) }) }).catch(error => { reject(error) }) }) }, RefreshFeatures ({ commit }) { return new Promise((resolve, reject) => { api('listCapabilities').then(response => { const result = response.listcapabilitiesresponse.capability resolve(result) commit('SET_FEATURES', result) }).catch(error => { reject(error) }) api('listConfigurations', { name: 'hypervisor.custom.display.name' }).then(json => { if (json.listconfigurationsresponse.configuration !== null) { const config = json.listconfigurationsresponse.configuration[0] commit('SET_CUSTOM_HYPERVISOR_NAME', config.value) } }).catch(error => { reject(error) }) }) }, UpdateConfiguration ({ commit }) { return new Promise((resolve, reject) => { api('listLdapConfigurations').then(response => { const ldapEnable = (response.ldapconfigurationresponse.count > 0) commit('SET_LDAP', ldapEnable) }).catch(error => { reject(error) }) }) }, SetDomainStore ({ commit }, domainStore) { commit('SET_DOMAIN_STORE', domainStore) }, SetCsLatestVersion ({ commit }, rolename) { if (!vueProps.$config.notifyLatestCSVersion) { return } const lastFetchTs = store.getters.latestVersion?.fetchedTs ? store.getters.latestVersion.fetchedTs : 0 if (rolename === 'Root Admin' && (+new Date() - lastFetchTs) > 24 * 60 * 60 * 1000) { axios.get( 'https://api.github.com/repos/apache/cloudstack/releases' ).then(response => { let latestReleaseVersion = getParsedVersion(response[0].tag_name) let latestTag = response[0].tag_name for (const release of response) { if (release.tag_name.toLowerCase().includes('rc')) { continue } const parsedVersion = getParsedVersion(release.tag_name) if (semver.gte(parsedVersion, latestReleaseVersion)) { latestReleaseVersion = parsedVersion latestTag = release.tag_name commit('SET_LATEST_VERSION', { version: latestTag, fetchedTs: (+new Date()) }) } } }).catch(ignored => {}) } }, SetDarkMode ({ commit }, darkMode) { commit('SET_DARK_MODE', darkMode) }, SetLoginFlag ({ commit }, loggedIn) { commit('SET_LOGIN_FLAG', loggedIn) }, SetCustomHypervisorName ({ commit }, name) { commit('SET_CUSTOM_HYPERVISOR_NAME', name) } } } export default user