Auth system refactoring (#812)

This brings a new number of improvements regarding auth:

- The SSE connections will be made using _event-source-polyfill_
which allows setting headers on the request, and setting
authorization headers or tokens as required has been added.
The main UI can now be used when the API is fully protected,
including the user operations like controlling items ("Implicit
user role for unauthenticated requests" disabled in Settings >
API Security). This closes #699.
We try to avoid the use of the polyfill if it's not needed, as the
native implementation is deemed better if it can be used. To
figure out whether that's the case, a `HTTP HEAD /rest/events`
is performed when setting the access token to determine if
that endpoint is secured - set the requireToken in case of a
401 response indicating it is.

- The UI now detects when the first /rest/ API calls fails with
a 401 Unauthorized error. This can happen when it's installed
as a standalone PWA ("Add to Home Screen" or "Install" in Chrome).
In that case, it will display a f7 login dialog and will also try
to use the Credentials API to store the passed credentials in
the browser, and retrieve them back to log in automatically the
next time. It will use these as Basic authentication credentials.
This prevents "empty screen" errors when using e.g.
myopenhab.org and the cloud credentials couldn't be passed.
The Basic credentials for proxy services can also be passed from
mobile with the following Javascript functions in the `OHApp`
object:
  * `string getBasicCredentialsUsername()`
  * `string getBasicCredentialsPassword()`
Also in that case, image handling in various parts of the UI has
to be modified to use blobs. This allows passing the credentials
if they're needed and known.

- Detects whether the API auth bundle is disabled (no /auth
endpoint), hides the authorize button and unlocks the admin menus.

Signed-off-by: Yannick Schaus <github@schaus.net>
3.0.x
Yannick Schaus 2021-01-16 17:52:51 +01:00
parent 7384ec9387
commit 6a7ad46fdf
17 changed files with 23119 additions and 813 deletions

File diff suppressed because it is too large Load Diff

View File

@ -61,6 +61,7 @@
"dayjs": "^1.9.6",
"dom7": "^2.1.5",
"echarts": "^4.9.0",
"event-source-polyfill": "^1.0.22",
"expression-eval": "^2.1.0",
"framework7": "^5.7.12",
"framework7-icons": "^3.0.1",
@ -119,6 +120,7 @@
"eslint-loader": "^2.2.1",
"eslint-plugin-vue": "^5.2.3",
"file-loader": "^3.0.1",
"global-prefix": "^3.0.0",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0",
"jest-serializer-vue": "^2.0.2",
@ -130,6 +132,7 @@
"ora": "^3.4.0",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"resolve-dir": "^1.0.1",
"rimraf": "^2.7.1",
"standard": "^12.0.1",
"strip-ansi": "=3.0.1",
@ -139,12 +142,11 @@
"swagger-ui-dist": "^3.32.5",
"uglifyjs-webpack-plugin": "^2.2.0",
"url-loader": "^1.1.2",
"vue-jest": "^3.0.6",
"vue-loader": "^15.9.3",
"vue-masonry-css": "^1.0.3",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.1",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",

View File

@ -1,5 +1,5 @@
<template>
<f7-app v-if="init" :style="{ visibility: !($store.getters.user || $store.getters.page('overview')) ? 'hidden' : '' }" :params="f7params" :class="{ 'theme-dark': this.themeOptions.dark === 'dark', 'theme-filled': this.themeOptions.bars === 'filled' }">
<f7-app v-if="init" :style="{ visibility: (($store.getters.user || $store.getters.page('overview')) || loginScreenOpened) ? '' : 'hidden' }" :params="f7params" :class="{ 'theme-dark': this.themeOptions.dark === 'dark', 'theme-filled': this.themeOptions.bars === 'filled' }">
<!-- Left Panel -->
<f7-panel v-show="ready" left :cover="showSidebar" class="sidebar" :visible-breakpoint="1024">
@ -94,7 +94,7 @@
<f7-icon slot="media" size="14" :f7="this.visibleBreakpointDisabled ? 'pin_slash' : 'pin'" color="gray"></f7-icon>
</f7-link>
<div slot="fixed" class="account" v-if="ready">
<div slot="fixed" class="account" v-if="ready && this.$store.getters.apiEndpoint('auth')">
<div class="display-flex justify-content-center">
<div class="hint-signin" v-if="!$store.getters.user && !$store.getters.pages.filter((p) => p.uid !== 'overview').length">
<em>{{ $t('sidebar.tip.signIn') }}<br /><f7-icon f7="arrow_down" size="20"></f7-icon></em>
@ -112,7 +112,7 @@
</f7-panel>
<!-- Right Panel -->
<f7-panel right reveal theme-dark>
<f7-panel right reveal theme-dark v-if="ready">
<panel-right />
<!-- <f7-view url="/panel-right/"></f7-view> -->
</f7-panel>
@ -123,7 +123,7 @@
<f7-view main v-show="ready" class="safe-areas" url="/" :master-detail-breakpoint="960" :animate="themeOptions.pageTransitionAnimation !== 'disabled'"></f7-view>
<f7-login-screen id="my-login-screen" :opened="loginScreenOpened">
<!-- <f7-login-screen id="my-login-screen" :opened="loginScreenOpened">
<f7-view name="login" v-if="$device.cordova">
<f7-page login-screen>
<f7-login-screen-title><img width="200px" src="res/img/openhab-logo.png"><br>Login</f7-login-screen-title>
@ -158,7 +158,7 @@
</f7-list>
</f7-page>
</f7-view>
</f7-login-screen>
</f7-login-screen> -->
</f7-app>
</template>
@ -360,6 +360,7 @@ export default {
},
computed: {
isAdmin () {
if (!this.$store.getters.apiEndpoint('auth')) return true
return this.ready && this.user && this.user.roles && this.user.roles.indexOf('administrator') >= 0
},
serverDisplayUrl () {
@ -367,12 +368,48 @@ export default {
}
},
methods: {
loadData () {
return this.$oh.api.get('/rest/')
loadData (useCredentials) {
const useCredentialsPromise = (useCredentials) ? this.setBasicCredentials() : Promise.resolve()
return useCredentialsPromise
.then(() => { return this.$oh.api.get('/rest/') })
.catch((err) => {
if (err === 'Unauthorized') {
if (!useCredentials) {
// try again with credentials
this.loadData(true)
return Promise.reject()
}
this.loginScreenOpened = true
this.$nextTick(() => {
this.$f7.dialog.login(
window.location.host,
'openHAB',
(username, password) => {
this.setBasicCredentials(username, password)
this.$oh.api.get('/rest/').then((rootResponse) => {
this.storeBasicCredentials()
this.loadData()
}).catch((err) => {
if (err === 'Unauthorized') {
this.clearBasicCredentials()
this.loadData()
return Promise.reject()
}
})
},
() => {
return Promise.reject()
}
)
})
return Promise.reject()
}
})
.then((rootResponse) => {
// store the REST API services present on the system
this.$store.commit('setRootResource', { rootResponse })
this.updateLocale()
if (!this.$store.getters.apiEndpoint('auth')) this.$store.commit('setNoAuth', true)
return rootResponse
}).then((rootResponse) => {
let locale = this.$store.state.locale
@ -409,6 +446,7 @@ export default {
},
pageIsVisible (page) {
if (!page.config.visibleTo) return true
if (this.$store.getters.noAuth) return true
const user = this.$store.getters.user
if (!user) return false
if (user.roles && user.roles.some(r => page.config.visibleTo.indexOf('role:' + r) >= 0)) return true

View File

@ -1,5 +1,7 @@
import { Utils } from 'framework7'
import { authorize, setBasicCredentials, clearBasicCredentials, storeBasicCredentials } from '@/js/openhab/auth'
export default {
data () {
return {
@ -7,27 +9,10 @@ export default {
}
},
methods: {
authorize,
getRefreshToken () {
return localStorage.getItem('openhab.ui:refreshToken') || null
},
authorize (setup) {
import('pkce-challenge').then((PkceChallenge) => {
const pkceChallenge = PkceChallenge.default()
const authState = (setup ? 'setup-' : '') + this.$f7.utils.id()
sessionStorage.setItem('openhab.ui:codeVerifier', pkceChallenge.code_verifier)
sessionStorage.setItem('openhab.ui:authState', authState)
window.location = '/auth' +
'?response_type=code' +
'&client_id=' + encodeURIComponent(window.location.origin) +
'&redirect_uri=' + encodeURIComponent(window.location.origin) +
'&scope=admin' +
'&code_challenge_method=S256' +
'&code_challenge=' + encodeURIComponent(pkceChallenge.code_challenge) +
'&state=' + authState
})
},
tryExchangeAuthorizationCode () {
return new Promise((resolve, reject) => {
const queryParams = Utils.parseUrlQuery(window.location.href)
@ -52,19 +37,20 @@ export default {
'code_verifier': codeVerifier
})
this.$oh.api.setAccessToken(null)
this.$oh.auth.clearAccessToken()
this.$oh.api.postPlain('/rest/auth/token?useCookie=true', payload, 'application/json', 'application/x-www-form-urlencoded').then((data) => {
const resp = JSON.parse(data)
localStorage.setItem('openhab.ui:refreshToken', resp.refresh_token)
this.$oh.api.setAccessToken(resp.access_token)
// schedule the next token refresh when 95% of this token's lifetime has elapsed, i.e. 3 minutes before a 1-hour token is due to expire
setTimeout(this.refreshAccessToken, resp.expires_in * 950)
this.$store.commit('setUser', { user: resp.user })
return this.$oh.auth.setAccessToken(resp.access_token).then(() => {
// schedule the next token refresh when 95% of this token's lifetime has elapsed, i.e. 3 minutes before a 1-hour token is due to expire
setTimeout(this.refreshAccessToken, resp.expires_in * 950)
this.$store.commit('setUser', { user: resp.user })
const nextRoute = authState.indexOf('setup') === 0 ? '/setup-wizard/' : '/'
this.$f7.views.main.router.navigate(nextRoute, { animate: false, clearPreviousHistory: true })
const nextRoute = authState.indexOf('setup') === 0 ? '/setup-wizard/' : '/'
this.$f7.views.main.router.navigate(nextRoute, { animate: false, clearPreviousHistory: true })
resolve(resp.user)
resolve(resp.user)
})
}).catch((err) => {
console.log(err)
reject(err)
@ -84,17 +70,18 @@ export default {
'refresh_token': refreshToken
})
this.$oh.api.setAccessToken(null)
this.$oh.auth.clearAccessToken()
this.$oh.api.postPlain('/rest/auth/token', payload, 'application/json', 'application/x-www-form-urlencoded').then((data) => {
const resp = JSON.parse(data)
this.$oh.api.setAccessToken(resp.access_token)
// schedule the next token refresh when 95% of this token's lifetime has elapsed, i.e. 3 minutes before a 1-hour token is due to expire
setTimeout(this.refreshAccessToken, resp.expires_in * 950)
// also make sure to check the token and renew it when the app becomes visible again
this.currentTokenExpireTime = new Date().getTime() + resp.expires_in * 950
document.addEventListener('visibilitychange', this.checkTokenAfterVisibilityChange)
this.$store.commit('setUser', { user: resp.user })
resolve(resp)
return this.$oh.auth.setAccessToken(resp.access_token).then(() => {
// schedule the next token refresh when 95% of this token's lifetime has elapsed, i.e. 3 minutes before a 1-hour token is due to expire
setTimeout(this.refreshAccessToken, resp.expires_in * 950)
// also make sure to check the token and renew it when the app becomes visible again
this.currentTokenExpireTime = new Date().getTime() + resp.expires_in * 950
document.addEventListener('visibilitychange', this.checkTokenAfterVisibilityChange)
this.$store.commit('setUser', { user: resp.user })
resolve(resp)
})
}).catch((err) => {
console.log(err)
reject(err)
@ -116,16 +103,19 @@ export default {
localStorage.removeItem('openhab.ui:refreshToken')
this.$oh.api.postPlain('/rest/auth/logout', payload, 'application/json', 'application/x-www-form-urlencoded').then((data) => {
console.log('Logged out')
this.$oh.api.setAccessToken(null)
this.$oh.auth.clearAccessToken()
this.$store.commit('setUser', { user: null })
resolve()
}).catch((err) => {
console.log(err)
this.$oh.api.setAccessToken(null)
this.$oh.auth.clearAccessToken()
this.$store.commit('setUser', { user: null })
reject(err)
})
})
}
},
setBasicCredentials,
clearBasicCredentials,
storeBasicCredentials
}
}

View File

@ -1,9 +1,9 @@
<template>
<f7-card expandable ref="card" class="model-card" :class="type + '-card'" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
<f7-card-content :padding="false">
<div :class="(!config.backgroundImage) ? `bg-color-${color}` : undefined" :style="{ height: `calc(var(--f7-safe-area-top) + ${headerHeight})` }">
<div :class="(!backgroundImageUrl) ? `bg-color-${color}` : undefined" :style="{ height: `calc(var(--f7-safe-area-top) + ${headerHeight})` }">
<f7-card-header :text-color="config.invertText ? 'black' : 'white'" class="display-block card-header" :style="{ height: `calc(var(--f7-safe-area-top) + ${headerHeight})` }">
<img v-if="config.backgroundImage" class="card-background" :src="config.backgroundImage" :style="config.backgroundImageStyle" />
<img v-if="config.backgroundImage" class="card-background lazy" :src="backgroundImageUrl" :style="config.backgroundImageStyle" />
<slot name="header">
<div v-if="context && context.component.slots && context.component.slots.header">
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.header" :key="'header-' + idx" @command="onCommand" />
@ -89,6 +89,15 @@ export default {
mixins: [CardMixin],
props: ['headerHeight'],
methods: {
},
asyncComputed: {
backgroundImageUrl () {
if (this.config.backgroundImage) {
return this.$oh.media.getImage(this.config.backgroundImage)
} else {
return Promise.resolve(null)
}
}
}
}
</script>

View File

@ -21,7 +21,7 @@
@update:center="centerUpdate"
@update:zoom="zoomUpdate">
<l-image-overlay
:url="config.imageUrl"
:url="backgroundImageUrl"
:bounds="bounds"
/>
<l-feature-group ref="featureGroup" v-if="context.component.slots && ready">
@ -87,7 +87,7 @@ export default {
minZoom: -2,
zoom: -0.5,
crs: CRS.Simple,
showMap: true,
showMap: false,
mapKey: this.$f7.utils.id(),
markers: []
}
@ -110,12 +110,18 @@ export default {
} : {})
}
},
mounted () {
this.fitMapBounds()
asyncComputed: {
backgroundImageUrl () {
return this.$oh.media.getImage(this.config.imageUrl)
}
},
watch: {
'config.noZoomOrDrag': function (val) {
this.refreshMap()
},
backgroundImageUrl (val) {
this.showMap = true
this.refreshMap()
}
},
methods: {
@ -144,7 +150,7 @@ export default {
onMarkerUpdate () {
},
fitMapBounds () {
this.$refs.map.mapObject.fitBounds(this.bounds)
if (this.$refs.map) this.$refs.map.mapObject.fitBounds(this.bounds)
},
refreshMap () {
this.mapKey = this.$f7.utils.id()

View File

@ -23,7 +23,9 @@ export default {
},
watch: {
url (val) {
this.src = this.$oh.media.getImage(val)
this.$oh.media.getImage(val).then((url) => {
this.src = url
})
},
src (val) {
if (this.config.lazy) this.$nextTick(() => { this.$f7.lazy.loadImage(this.$refs.lazyImage) })
@ -47,7 +49,9 @@ export default {
if (this.config.item) {
this.loadItemImage()
} else {
this.src = this.$oh.media.getImage(this.config.url)
this.$oh.media.getImage(this.config.url).then((url) => {
this.src = url
})
}
},
methods: {

View File

@ -1,6 +1,5 @@
import Framework7 from 'framework7/framework7-lite.esm.bundle.js'
let accessToken = null
import { getAccessToken, getTokenInCustomHeader, getBasicCredentials } from './auth'
function wrapPromise (f7promise) {
return new Promise((resolve, reject) => {
@ -13,20 +12,21 @@ function wrapPromise (f7promise) {
Framework7.request.setup({
xhrFields: { withCredentials: true },
beforeSend (xhr) {
if (accessToken) {
if (document.cookie.indexOf('X-OPENHAB-AUTH-HEADER') >= 0) {
xhr.setRequestHeader('X-OPENHAB-TOKEN', accessToken)
if (getAccessToken()) {
if (getTokenInCustomHeader()) {
xhr.setRequestHeader('X-OPENHAB-TOKEN', getAccessToken())
} else {
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken)
xhr.setRequestHeader('Authorization', 'Bearer ' + getAccessToken())
}
}
if (getBasicCredentials()) {
const creds = getBasicCredentials()
xhr.setRequestHeader('Authorization', 'Basic ' + btoa(creds.id + ':' + creds.password))
}
}
})
export default {
setAccessToken (token) {
accessToken = token
},
get (uri, data) {
return wrapPromise(Framework7.request.promise.json(uri, data))
},

View File

@ -0,0 +1,113 @@
import Framework7 from 'framework7/framework7-lite.esm.bundle.js'
/**
* The current access token
*/
let accessToken = null
/**
* The access token should be passed in the X-OPENHAB-TOKEN header instead of Authorization: Bearer
*/
let tokenInCustomHeader = false
/**
* The PasswordCredential to authenticate to a reverse proxy service like openHAB Cloud
*/
let basicCredentials = null
/**
* The token is required for all requests, including SSE
*/
let requireToken = false
export function getAccessToken () { return accessToken }
export function getTokenInCustomHeader () { return tokenInCustomHeader }
export function getBasicCredentials () { return basicCredentials }
export function getRequireToken () { return requireToken }
if (document.cookie.indexOf('X-OPENHAB-AUTH-HEADER')) tokenInCustomHeader = true
export function authorize (setup) {
import('pkce-challenge').then((PkceChallenge) => {
const pkceChallenge = PkceChallenge.default()
const authState = (setup ? 'setup-' : '') + Framework7.utils.id()
sessionStorage.setItem('openhab.ui:codeVerifier', pkceChallenge.code_verifier)
sessionStorage.setItem('openhab.ui:authState', authState)
window.location = '/auth' +
'?response_type=code' +
'&client_id=' + encodeURIComponent(window.location.origin) +
'&redirect_uri=' + encodeURIComponent(window.location.origin) +
'&scope=admin' +
'&code_challenge_method=S256' +
'&code_challenge=' + encodeURIComponent(pkceChallenge.code_challenge) +
'&state=' + authState
})
}
export function setBasicCredentials (username, password) {
if (username && password) {
console.log('Using passed credentials')
basicCredentials = { id: username, password: password }
tokenInCustomHeader = true
return Promise.resolve()
} else if (window.OHApp && window.OHApp.getBasicCredentialsUsername) {
const usernameFromApp = window.OHApp.getBasicCredentialsUsername()
const passwordFromApp = window.OHApp.getBasicCredentialsPassword()
basicCredentials = { id: usernameFromApp, password: passwordFromApp }
tokenInCustomHeader = true
return Promise.resolve()
} else if (navigator.credentials && navigator.credentials.preventSilentAccess && window.PasswordCredential) {
return navigator.credentials.get({ password: true }).then((creds) => {
if (creds) {
console.log('Using stored Basic credentials to sign in to a reverse proxy service')
basicCredentials = { id: creds.id, password: creds.password }
tokenInCustomHeader = true
}
return Promise.resolve()
})
} else {
return Promise.resolve()
}
}
export function clearBasicCredentials () {
basicCredentials = null
tokenInCustomHeader = document.cookie.indexOf('X-OPENHAB-AUTH-HEADER')
}
export function storeBasicCredentials () {
if (basicCredentials && navigator.credentials && navigator.credentials.preventSilentAccess && window.PasswordCredential) {
navigator.credentials.store(new window.PasswordCredential(basicCredentials))
}
}
export function setAccessToken (token) {
accessToken = token
// determine whether the token is required for user operations
let headers = {}
if (getBasicCredentials()) {
const creds = getBasicCredentials()
headers.Authorization = 'Basic ' + btoa(creds.id + ':' + creds.password)
}
return fetch('rest/events', { method: 'HEAD', credentials: 'include', headers })
.then((resp) => {
if (resp.status === 401) {
requireToken = true
if (!token) authorize()
}
return Promise.resolve()
})
}
export function clearAccessToken () {
accessToken = null
}
export default {
setAccessToken,
clearAccessToken,
setBasicCredentials
}

View File

@ -1,10 +1,12 @@
import api from './api'
import auth from '../auth'
import sse from './sse'
import media from './media'
import speech from './speech'
export default {
api,
auth,
sse,
media,
speech

View File

@ -34,6 +34,6 @@ export default {
}
},
getImage (url) {
return url
return Promise.resolve(url)
}
}

View File

@ -1,10 +1,12 @@
import api from './api'
import auth from './auth'
import sse from './sse'
import media from './media'
import speech from './speech'
export default {
api,
auth,
sse,
media,
speech

View File

@ -1,33 +1,39 @@
import { getBasicCredentials } from '@/js/openhab/auth'
import Framework7 from 'framework7/framework7-lite.esm.bundle.js'
export default {
getIcon: (icon, format, state) => {
if (!format) format = 'svg'
let url = `/icon/${icon}?format=${format}&anyFormat=true`
if (state) url += `&state=${encodeURIComponent(state)}`
// TODO handle basic auth with blobs and data URIs if necessary
// return new Promise((resolve, reject) => {
// Framework7.request.promise({ url, xhrFields: { responseType: 'blob' } }).then((resp) => {
// let reader = new FileReader()
// reader.readAsDataURL(resp.data)
// reader.onload = () => {
// return resolve(reader.result)
// }
// })
// })
return Promise.resolve(url)
if (getBasicCredentials()) {
return new Promise((resolve, reject) => {
Framework7.request.promise({ url, xhrFields: { responseType: 'blob' } }).then((resp) => {
let reader = new FileReader()
reader.readAsDataURL(resp.data)
reader.onload = () => {
return resolve(reader.result)
}
})
})
} else {
return Promise.resolve(url)
}
},
getImage: (url) => {
return url
// TODO handle basic auth with blobs and data URIs if necessary
// return new Promise((resolve, reject) => {
// Framework7.request.promise({ url, xhrFields: { responseType: 'blob' } }).then((resp) => {
// let reader = new FileReader()
// reader.readAsDataURL(resp.data)
// reader.onload = () => {
// return resolve(reader.result)
// }
// })
// })
if (getBasicCredentials()) {
return new Promise((resolve, reject) => {
Framework7.request.promise({ url, xhrFields: { responseType: 'blob' } }).then((resp) => {
let reader = new FileReader()
reader.readAsDataURL(resp.data)
reader.onload = () => {
return resolve(reader.result)
}
})
})
} else {
return Promise.resolve(url)
}
}
}

View File

@ -1,9 +1,27 @@
import { EventSourcePolyfill, NativeEventSource } from 'event-source-polyfill'
import { getAccessToken, getTokenInCustomHeader, getBasicCredentials, getRequireToken } from './auth'
let openSSEClients = []
function newSSEConnection (path, readyCallback, messageCallback, errorCallback) {
let eventSource
// TODO handle basic auth with polyfill if necessary
eventSource = new EventSource(path)
const headers = {}
if (getAccessToken() && getRequireToken()) {
if (getTokenInCustomHeader()) {
headers['Authorization'] = 'Bearer ' + getAccessToken()
} else {
headers['X-OPENHAB-TOKEN'] = getAccessToken()
}
}
if (getBasicCredentials()) {
const creds = getBasicCredentials()
headers['Authorization'] = 'Basic ' + btoa(creds.id + ':' + creds.password)
}
if (Object.keys(headers).length > 0) {
eventSource = new EventSourcePolyfill(path, { headers })
} else {
eventSource = new NativeEventSource(path)
}
eventSource.addEventListener('ready', (e) => {
readyCallback(e.data)

View File

@ -1,15 +1,20 @@
const state = {
user: null
user: null,
noAuth: false
}
const getters = {
user: (state) => state.user,
isAdmin: (state) => state.user && state.user.roles && state.user.roles.indexOf('administrator') >= 0
noAuth: (state) => state.noAuth,
isAdmin: (state) => state.noAuth || (state.user && state.user.roles && state.user.roles.indexOf('administrator') >= 0)
}
const mutations = {
setUser (state, { user }) {
state.user = user
},
setNoAuth (state, value) {
state.noAuth = value
}
}

View File

@ -145,6 +145,7 @@ export default {
ready (val) {
if (val) {
this.loadModel()
this.$store.dispatch('startTrackingStates')
}
}
},
@ -153,8 +154,10 @@ export default {
this.overviewPageKey = this.$utils.id()
},
onPageAfterIn () {
this.$store.dispatch('startTrackingStates')
if (this.ready) this.loadModel()
if (this.ready) {
this.loadModel()
this.$store.dispatch('startTrackingStates')
}
},
onPageBeforeOut () {
this.$store.dispatch('stopTrackingStates')

View File

@ -1,6 +1,7 @@
import cardGroups from './homecards-grouping'
import { compareItems } from '@/components/widgets/widget-order'
import { loadLocaleMessages } from '@/js/i18n'
import { authorize } from '@/js/openhab/auth'
export default {
i18n: {
@ -50,76 +51,83 @@ export default {
}
},
loadModel (page) {
this.$oh.api.get('/rest/items?metadata=semantics,listWidget,widgetOrder').then((data) => {
this.items = data
// get the location items
const locations = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics.value.indexOf('Location') === 0
}).sort(this.compareObjects).map((l) => {
return {
item: l,
properties: data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.config.relatesTo &&
item.metadata.semantics.config.hasLocation === l.name
}).sort(this.compareObjects),
equipment: data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.value.indexOf('Equipment') === 0 &&
item.metadata.semantics.config.hasLocation === l.name
}).sort(this.compareObjects).map((item) => {
return {
item: item,
points: data.filter((item2, index, items) => {
return item2.metadata && item2.metadata.semantics &&
item2.metadata.semantics && item2.metadata.semantics.config &&
item2.metadata.semantics.config.isPointOf === item.name
}).sort(this.compareObjects)
}
})
this.$oh.api.get('/rest/items?metadata=semantics,listWidget,widgetOrder')
.then((data) => {
this.items = data
// get the location items
const locations = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics.value.indexOf('Location') === 0
}).sort(this.compareObjects).map((l) => {
return {
item: l,
properties: data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.config.relatesTo &&
item.metadata.semantics.config.hasLocation === l.name
}).sort(this.compareObjects),
equipment: data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.value.indexOf('Equipment') === 0 &&
item.metadata.semantics.config.hasLocation === l.name
}).sort(this.compareObjects).map((item) => {
return {
item: item,
points: data.filter((item2, index, items) => {
return item2.metadata && item2.metadata.semantics &&
item2.metadata.semantics && item2.metadata.semantics.config &&
item2.metadata.semantics.config.isPointOf === item.name
}).sort(this.compareObjects)
}
})
}
})
// get the equipment items
const equipment = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics &&
item.metadata.semantics.value.indexOf('Equipment') === 0
}).sort(this.compareObjects).reduce((prev, item, i, properties) => {
const equipmentType = item.metadata.semantics.value.substring(item.metadata.semantics.value.lastIndexOf('_')).replace('_', '')
if (!prev[equipmentType]) prev[equipmentType] = []
const equipmentWithPoints = {
item: item,
points: data.filter((item2, index, items) => {
return item2.metadata && item2.metadata.semantics &&
item2.metadata.semantics && item2.metadata.semantics.config &&
item2.metadata.semantics.config.isPointOf === item.name
}).sort(this.compareObjects)
}
prev[equipmentType].push(equipmentWithPoints)
return prev
}, {})
// get the property items
const properties = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.config.relatesTo
}).sort(this.compareObjects).reduce((prev, item, i, properties) => {
const property = item.metadata.semantics.config.relatesTo.split('_')[1]
if (!prev[property]) prev[property] = []
prev[property].push(item)
return prev
}, {})
this.model.locations = locations.map(l => this.buildModelCard('location', l, l.item.name, page))
this.model.equipment = Object.keys(equipment).sort((a, b) => this.$t(a).localeCompare(this.$t(b))).map(k => this.buildModelCard('equipment', equipment[k], k, page))
this.model.properties = Object.keys(properties).sort((a, b) => this.$t(a).localeCompare(this.$t(b))).map(k => this.buildModelCard('property', properties[k], k, page))
this.modelReady = true
})
.catch((err) => {
console.log('Error while loading model: ' + err)
if (err === 'Unauthorized') {
authorize()
}
})
// get the equipment items
const equipment = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics &&
item.metadata.semantics.value.indexOf('Equipment') === 0
}).sort(this.compareObjects).reduce((prev, item, i, properties) => {
const equipmentType = item.metadata.semantics.value.substring(item.metadata.semantics.value.lastIndexOf('_')).replace('_', '')
if (!prev[equipmentType]) prev[equipmentType] = []
const equipmentWithPoints = {
item: item,
points: data.filter((item2, index, items) => {
return item2.metadata && item2.metadata.semantics &&
item2.metadata.semantics && item2.metadata.semantics.config &&
item2.metadata.semantics.config.isPointOf === item.name
}).sort(this.compareObjects)
}
prev[equipmentType].push(equipmentWithPoints)
return prev
}, {})
// get the property items
const properties = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.config.relatesTo
}).sort(this.compareObjects).reduce((prev, item, i, properties) => {
const property = item.metadata.semantics.config.relatesTo.split('_')[1]
if (!prev[property]) prev[property] = []
prev[property].push(item)
return prev
}, {})
this.model.locations = locations.map(l => this.buildModelCard('location', l, l.item.name, page))
this.model.equipment = Object.keys(equipment).sort((a, b) => this.$t(a).localeCompare(this.$t(b))).map(k => this.buildModelCard('equipment', equipment[k], k, page))
this.model.properties = Object.keys(properties).sort((a, b) => this.$t(a).localeCompare(this.$t(b))).map(k => this.buildModelCard('property', properties[k], k, page))
this.modelReady = true
})
}
}
}