/**
* @namespace Stores
* @category Stores
* @module auth-store
* @todo Remove Vue global (Vue3 migration)
* */
import { isTestMode } from '@/services/env-service.js'
import Vue from 'vue'
import api from '@/api'
import jwt_decode from 'jwt-decode'
import cache from '@/utils/cache'
import { getQueryStringValue } from '@/utils/querystring'
import moment from 'moment'
import logginserService from '@/services/logging-service.js'
import APIUrls from '@/config/simpliciti-apis.js'
import { encodeLoginNameClientId } from '@/services/auth-service.js'
import { saveSettingsParamLocally } from '@/services/settings-service.js'
import storage from '@/plugins/vue-local-storage.js'
const shouldLog =
(getQueryStringValue('verbose') || '').includes('1') ||
(getQueryStringValue('verbose') || '').includes('auth')
const apiStorage = storage.fromNamespace('api')
let scope = (window.__authScope = {
syncInProgress: false,
})
/**
* @todo Normalize attribute names (underscore -> snake case)
* @returns {Object} new state
*/
function newState() {
return {
user_logged: false,
user_infos: {
login: '',
client_nom: '',
client_id: '',
/**
* main jwt (tokens containing "to_client" can't be here)
*/
token: '', //Will be used by api wrapper if toClientToken is empty
updated_at: '',
userId: '',
/**
* child token (tokens containing "to_client" token must be here)
*/
toClientToken: '', //Will be used by api wrapper by default (fallbacks to token property)
toClientId: '',
hasChildClients: false,
country: '', //client country code (i.g FR)
},
user_rights: [],
externalRights: {
admin: false,
analyse: false,
citifret: false,
citipav: false,
editour: false, //unused
simpliboard: false, //unused
},
logoutRedirect: false, //Deprecated
logoutRedirectUrl: process.env.VUE_APP_GEORED_HOST, //Deprecated
}
}
/**
* @namespace Stores
* @category Stores
* @module auth-store
*
* @todo Improve cache: Save the entire store state rather than a subset
* @todo Normalize properties once (after API fetch)
* @todo Refactor/Move auth logic to service
*
* 09-02-23 Readonly rights in production
* */
export default {
namespaced: true,
state: newState(),
getters: {
/**
* Will return login/client from a login-as token
* @param {*} state
* @returns {Object} {login:"",client:""} i.g {login:"jarancibia",client:"DGD"}
*/
loginAs(state, getters) {
let decoded = {}
try {
decoded = jwt_decode(state.user_infos.toClientToken)
} catch (err) {
decoded = {}
}
let toClient = decoded.to_client || ''
return {
enabled: !!toClient,
login: state.user_infos.login,
client: toClient || getters.clientName,
}
},
isLoginAs(state, getters) {
return getters['loginAs'].enabled
},
jwt(state) {
return state.user_infos.toClientToken || state.user_infos.token || ''
},
parentJwt(state) {
return state.user_infos.token || ''
},
loginNameClientIdEncoded(state, getters) {
return getters['isLogged']
? encodeLoginNameClientId(getters['loginName'], getters['clientId'])
: ''
},
isSabadmin(state) {
return state.user_infos.login === 'sabadmin'
},
loginName(state) {
return state.user_infos.login
},
clientId(state) {
return state.user_infos.client_id
},
userId(state) {
return state.user_infos.userId
},
toClientId(state) {
return state.user_infos.toClientId
},
clientName(state) {
return state.user_infos.client_nom
},
clientCountry: (state) => (state.user_infos.country || 'fr').toLowerCase(),
isLogged(state) {
return state.user_logged
},
rightsList(state) {
return state.user_rights
},
hasChildClients: (state) => state.user_infos.hasChildClients,
hasRight: (state) => (rightCode) => state.user_rights.includes(rightCode),
hasExternalRight: (state) => (name) =>
(state.externalRights[name] || false) === true,
/**
* @see LoginAsFeature.vue
* @returns {Boolean}
*/
canUseClientSelectionFeature(state, getters) {
//Use case: V2 (login as child client) --> V3 (Parent token is not passed by, so we are not able to switch client again) (Not implemented)
if (!state.user_infos.token) {
return false
}
//Has child clients
return (
getters['hasChildClients'] ||
//Or has "Access to login client"
Vue.$rights.hasFeatureRight('common_login_as_access') ||
//Or has "Connect as" and is not sabatier
(getters['clientName'] !== 'sabatier' &&
Vue.$rights.hasFeatureRight('common_login_as_user'))
)
},
},
mutations: {
setLogged(state, infos = {}) {
infos = infos === null ? {} : infos
if (Object.keys(infos).length > 0) {
state.user_infos = Object.assign(
{},
{
login: infos.user || infos.login || '',
client_id: infos.client_id || infos.clientId || '',
client_nom: infos.client_nom || infos.client || '',
token: infos.token,
updated_at: infos.updated_at || Date.now(),
userId: infos.userId,
toClientToken: infos.toClientToken,
toClientId: infos.toClientId,
hasChildClients:
infos.hasChildClients !== undefined
? infos.hasChildClients
: false,
country: infos.country || '',
}
)
state.user_rights = isTestMode()
? infos.rights || []
: Object.freeze(infos.rights || [])
state.externalRights = infos.externalRights
logginserService.identifyUser({
id: infos.userId,
})
logginserService.setContext('user_details', {
client_id: state.user_infos.client_id,
client_name: state.user_infos.client_nom,
})
} else {
logginserService.identifyUser(null)
logginserService.setContext('user_details', {})
}
state.user_logged =
!!state.user_infos.toClientToken || !!state.user_infos.token
},
/**
* @deprecated
*/
addUserRights(state, rights = []) {
state.user_rights = state.user_rights.concat(rights)
},
/**
* @deprecated
*/
setLogoutRedirect(state, logoutRedirect) {
state.logoutRedirect = logoutRedirect
},
removeCredentials(state) {
Object.assign(state, newState())
},
},
actions: {
/**
* @function authenticateUser
* @returns {Boolean}
* @private
*/
async authenticateUser({ commit, dispatch, rootGetters }, userInfos) {
shouldLog && console.log('auth::authenticateUser')
await cache.setItem('user_infos', userInfos)
let currentLanguage = rootGetters['settings/getParameter'](
'applicationLanguage'
)
if (currentLanguage) {
//Persist the selected language in the login view (Priorized over client country language)
//This should be done before setLogged mutation, otherwise components will try to syncCache right away.
await saveSettingsParamLocally('applicationLanguage', currentLanguage, {
suffix: encodeLoginNameClientId(
userInfos.user || userInfos.login,
userInfos.clientId || userInfos.client_id
),
})
}
;(async () => {
//Update settings from cache, then from server
await dispatch(
'settings/syncFromCache',
{},
{
root: true,
}
)
await dispatch('settings/syncFromServer', '', {
root: true,
})
})()
commit('setLogged', userInfos)
Vue.$loggingService.createSession({
jwt: userInfos.toClientToken || userInfos.token,
})
return true
},
/**
* Will perform a login-as operation without full-refresh (session change)
* @param {*} param0
* @param {*} token
* @returns
*/
async loginAs({ dispatch, commit, state }, { token, clientId }) {
let userInfos = {
...state.user_infos,
toClientToken: token,
toClientId: clientId,
}
return await dispatch('syncLoggedUsingExistingInfos', {
userInfos,
})
},
async removeCredentials({ commit }) {
shouldLog && console.log('syncLogged::removing-credentials')
commit('removeCredentials')
await cache.setItem('user_infos', null)
await cache.removeItem('user_infos_parent_token')
},
/**
* @function logout
*/
async logout({ dispatch }) {
shouldLog && console.log('auth::logout')
await dispatch('removeCredentials')
await dispatch('ecoConduite/resetState', '', {
root: true,
})
await dispatch('search_module/resetStore', '', {
root: true,
})
await dispatch('location_module/resetStore', '', {
root: true,
})
await dispatch('settings/resetStore', '', {
root: true,
})
await dispatch('zones/resetStore', '', {
root: true,
})
await apiStorage.clear()
window._logoutAt = Date.now()
},
/**
* Set a new session on the fly. It will retrieve rights from API.
* @function syncLoggedUsingExistingInfos
* @param {Object} payload.userInfos new session information
* @returns
*/
async syncLoggedUsingExistingInfos(
{ commit, dispatch },
{ userInfos, ...options }
) {
//shouldLog && console.log('syncLogged::syncLoggedUsingExistingInfos::userInfos',userInfos)
await cache.setItem('user_infos', userInfos) //To be able to fetch rigths
let rightsResponse = await getUserRightsNew(
userInfos.token || userInfos.toClientToken
)
userInfos.externalRights = rightsResponse.externalRights
userInfos.rights = rightsResponse.rights
await dispatch('authenticateUser', userInfos)
//console.log('auth login-ok (preserve token)')
return true
},
/**
* Sync session from local cache
* @warning Two function calls at the same time might trigger unexpected results if you do things like removing cached token
* @function syncLogged
* @returns
*/
async syncLogged({ commit, dispatch }, payload = {}) {
shouldLog &&
console.log('syncLogged', {
payload,
})
let userInfos = {}
if (scope.syncInProgress) {
return new Promise((resolve, reject) => {
dispatch('syncLogged', {}).then(resolve).catch(reject)
}, 500)
}
let tokenToAuth
//Use case: Token provided by _token= (Only process once)
if (payload.token) {
scope.syncInProgress = true
try {
tokenToAuth = payload.token
let decoded = jwt_decode(payload.token)
if (decoded.to_client) {
//Use case: V2 (login as child client) --> V3 : Use will not be able to select a client again (Not implemented)
shouldLog &&
console.log('syncLogged::_token::login-as', decoded.to_client)
userInfos.toClientToken = payload.token
} else {
shouldLog &&
console.log('syncLogged::_token::simple', decoded.to_client)
userInfos.token = payload.token //i,g V2 --> V3
}
await cache.setItem('user_infos', userInfos) //Write to cache ASAP because they might be multipe syncLogged calls and API requests that require JWT
scope.syncInProgress = false
} catch (err) {
scope.syncInProgress = false
throw err
}
} else {
userInfos = (await cache.getItem('user_infos')) || {}
tokenToAuth = userInfos.token
}
shouldLog && console.log('syncLogged::tokenToAuth', tokenToAuth)
if (tokenToAuth) {
try {
let decoded = jwt_decode(tokenToAuth)
//Keep using an existing token without server check (expiration check is done in the client) (++performance) (api check takes ~5s)
if (
moment().isBefore(moment(new Date(decoded.exp * 1000)), 'minute') &&
Object.keys(userInfos).length > 1
) {
shouldLog && console.log('syncLogged::token-still-valid')
return dispatch('syncLoggedUsingExistingInfos', { userInfos })
}
let check_token = await api.post('/public/check_token', {
token: tokenToAuth,
})
/*shouldLog &&
console.log('syncLogged.', {
check_token: check_token.data,
})*/
if (check_token.data.userId) {
shouldLog && console.log('auth::success::check-token')
let infos = await buildUserInfos(
{ data: userInfos }, // token will be passed by here: i.g {token:xxx} or {toClientToken:xxx}
//{ data: tokenToAuth },
null,
check_token
)
if (infos.rights.length === 0) {
dispatch(
'alert/addAlert',
{
title: '403',
text: 'alerts.NOT_ENOUGH_RIGHTS',
type: 'warning',
},
{
root: true,
}
)
throw new Error('AUTH_NO_RIGHTS')
}
await dispatch('authenticateUser', infos)
shouldLog && console.log('auth login-ok (token regen)')
return true
} else {
shouldLog && console.log('auth::fail::check-token')
}
} catch (err) {
console.warn(err)
Vue.ErrorLogger.logError(err, {
store: 'auth',
action: 'syncLogged',
})
}
}
return false
},
/**
* Start a new session with login/password
* @function login
* @returns
*/
async login({ commit, dispatch, getters }, { login, client, password }) {
//await dispatch("syncLogged");
if (getters.isLogged) {
return
}
let account_infos = await api.postFormUrlEncoded(
'/public/authentification',
{
login,
client,
password,
}
)
account_infos.data =
account_infos.data.length !== undefined
? account_infos.data[0]
: account_infos.data
let isLoginSuccess =
account_infos && account_infos.data && !!account_infos.data.token
//shouldLog && console.log({ account_infos: account_infos.data })
if (!isLoginSuccess) {
dispatch(
'alert/addAlert',
{
title: '401',
text: 'alerts.LOGIN_FAIL',
type: 'warning',
},
{
root: true,
}
)
throw new Error('AUTH_LOGIN_INFOS_FAIL')
}
let token_info = await api.postFormUrlEncoded('/public/token', {
login,
client,
password,
})
token_info.data =
token_info.data.length !== undefined
? token_info.data[0]
: token_info.data
//shouldLog && console.log({ token_info })
let check_token = await api.post('/public/check_token', {
token: token_info.data,
})
//shouldLog && console.log({ check_token })
if (!(check_token.data && check_token.data.userId)) {
throw new Error('AUTH_LOGIN_TOKEN_CHECK_FAIL')
}
let infos = await buildUserInfos(account_infos, token_info, check_token)
//dispatch("syncExternalUserRights", token_info.data);
if (infos.rights.length === 0) {
dispatch(
'alert/addAlert',
{
title: '403',
text: 'alerts.NOT_ENOUGH_RIGHTS',
type: 'warning',
},
{
root: true,
}
)
throw new Error('AUTH_NO_RIGHTS')
}
await dispatch('authenticateUser', infos)
shouldLog && console.log('auth login-ok')
return {
shouldRouteToClientSelection: getters['canUseClientSelectionFeature'],
}
},
},
}
/**
*
* @param {String} jwt
* @returns
*/
async function getUserRightsNew(jwt) {
shouldLog && console.log('auth::getUserRightsNew::jwt', jwt)
let rightsResponse = (
await api.v3.get(
`${APIUrls.APIV3_USER_ACCESS_RIGHTS}?typeSectionId=12`,
{},
{
jwt,
}
)
).data
let rights = rightsResponse.rights.map((item) => item.code)
if (rightsResponse.commonRights) {
rights = rights.concat(rightsResponse.commonRights.map((r) => r.code))
}
return {
rights,
externalRights: rightsResponse.externalRights,
}
}
/**
* Those informations are stored locally (client-cache)
* 21-01-22: API check_token "token" should override existing token (To handle rights when redirecting applications, for example: V3 -> Citifret)
* @todo Refactor or replace by syncLoggedUsingExistingInfos
*/
async function buildUserInfos(
userInfos = {},
tokenInfos = {},
checkTokenInfos = {}
) {
let token =
(checkTokenInfos.data || {}).token ||
tokenInfos?.data ||
(userInfos?.data || {}).token
shouldLog &&
console.log('auth::buildUserInfos::token', {
token,
userInfos,
tokenInfos,
checkTokenInfos,
})
let anyJWT = token || (userInfos?.data || {}).toClientToken
//New grab rights from new api
let rightsResponse = await getUserRightsNew(
token || (userInfos?.data || {}).toClientToken
)
shouldLog &&
console.log('buildUserInfos', {
userInfos,
tokenInfos,
checkTokenInfos,
})
return {
rights: rightsResponse.rights,
externalRights: rightsResponse.externalRights,
...userInfos.data,
...(checkTokenInfos.data || {}),
token, //parent token only (token should not contain to_client attribute)
toClientToken: userInfos?.data?.toClientToken || '', //child token (login-as feature)
toClientId: userInfos?.data?.toClientId || '',
hasChildClients: userInfos?.data?.childs,
password: '#####',
updated_at: Date.now(),
country:
userInfos?.data?.country ||
(await Vue.$auth.getClientParameter('Pays', {
jwt: anyJWT,
})),
}
}
Source