add installed check and rework login/redirect mecanism

This commit is contained in:
Axolotle 2020-09-27 15:18:07 +02:00
parent 66e4584d2b
commit 7710e6a1c3
8 changed files with 140 additions and 102 deletions

View file

@ -2,7 +2,7 @@
<div id="app" class="container">
<header>
<b-navbar>
<b-navbar-brand to="/" exact exact-active-class="active">
<b-navbar-brand :to="{ name: 'home' }" exact exact-active-class="active">
<img alt="Yunohost logo" src="./assets/logo.png">
</b-navbar-brand>
<b-navbar-nav class="ml-auto">
@ -27,23 +27,24 @@
<breadcrumb v-if="$route.meta.breadcrumb" />
<main id="main">
<router-view v-if="isReady" />
<router-view />
</main>
<footer>
<nav>
<b-nav>
<b-nav class="justify-content-center">
<b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes="text-secondary">
<icon iname="book" /> Documentation
<icon iname="book" /> {{ $t('footer.documentation') }}
</b-nav-item>
<b-nav-item href="https://yunohost.org/help" target="_blank" link-classes="text-secondary">
<icon iname="life-ring" /> Need help?
<icon iname="life-ring" /> {{ $t('footer.help') }}
</b-nav-item>
<b-nav-item href="https://donate.yunohost.org/" target="_blank" link-classes="text-secondary">
<icon iname="heart" /> Donate
<icon iname="heart" /> {{ $t('footer.donate') }}
</b-nav-item>
<i18n v-if="yunohost" path="footer_version" tag="b-nav-text"
id="yunohost-version" class="ml-auto"
<i18n
v-if="yunohost" path="footer.version" tag="b-nav-text"
id="yunohost-version" class="ml-md-auto text-center"
>
<template v-slot:ynh>
<b-link href="https://yunohost.org">
@ -69,51 +70,25 @@ import { mapGetters } from 'vuex'
export default {
name: 'App',
data () {
return {
// isReady blocks the rendering of the rooter-view until we have a true info
// about the connected state of the user.
isReady: false
}
},
computed: {
...mapGetters(['connected', 'yunohost'])
},
methods: {
async logout () {
this.$store.dispatch('LOGOUT').then(() => {
this.$router.push({ name: 'login' })
})
this.$store.dispatch('LOGOUT')
}
},
// This hook is only triggered at page first load
async created () {
// From this hook the value of `connected` always come from the localStorage.
if (!this.connected) {
// user is not connected: allow the login view to be rendered.
this.isReady = true
return
// This state may be `true` but session may have expired, by querying
// yunohost infos, api may respond with `Unauthorized` in which case the `connected`
// state will be automaticly reseted and user will be prompt with the login view.
if (this.connected) {
this.$store.dispatch('GET_YUNOHOST_INFOS')
}
// localStorage 'connected' value may be true, but session may have expired.
// Try to get the yunohost version.
this.$store.dispatch(
'GET_YUNOHOST_INFOS'
).catch(() => {
// Session expired, reset the 'connected' state and redirect with a query
// FIXME is there a case where the error may not be a 401 therefor requires
// better handling ?
this.$store.dispatch('RESET_CONNECTED')
this.$router.push({
name: 'login',
query: { redirect: this.$route.path !== '/login' ? this.$route.path : '/' }
})
}).finally(() => {
// in any case allow the router-view to be rendered
this.isReady = true
})
}
}
</script>

View file

@ -5,6 +5,22 @@
import store from '@/store'
/**
* Allow to set a timeout on a `Promise` expected response.
* The returned Promise will be rejected if the original Promise is not resolved or
* rejected before the delay.
*
* @param {Promise} promise - A promise (like a fetch call).
* @param {number} delay - delay after which the promise is rejected
* @return {Promise}
*/
export function timeout (promise, delay) {
return new Promise((resolve, reject) => {
setTimeout(() => (reject(new Error('api_not_responding'))), delay)
promise.then(resolve, reject)
})
}
/**
* Converts an object literal into an `URLSearchParams` that can be turned into a
* query string or used as a body in a `fetch` call.
@ -54,6 +70,7 @@ export async function handleResponse (response) {
*/
export async function handleErrors (response) {
if (response.status === 401) {
store.dispatch('DISCONNECT')
throw new Error('Unauthorized')
} else if (response.status === 400) {
const message = await response.text()

View file

@ -119,7 +119,12 @@
"everything_good": "Everything good!",
"experimental_warning": "Warning: this feature is experimental and not consider stable, you shouldn't be using it except if you know what you are doing.",
"firewall": "Firewall",
"footer_version": "Powered by {ynh} {version} ({repo}).",
"footer": {
"version": "Powered by {ynh} {version} ({repo}).",
"documentation": "Documentation",
"help": "Need help?",
"donate": "Donate"
},
"form_errors": {
"email_syntax": "Invalid email: Must be alphanumeric, underscore and dash characters only (e.g. someone@example.com, s0me-1@example.com)",
"groupname_syntax": "Invalid group name: Must be alphanumeric, underscore and space characters only",

View file

@ -1,12 +0,0 @@
import router from './index.js'
import store from '@/store'
// Before each route request hook
router.beforeEach((to, from, next) => {
// Allow if connected or route is not protected
if (store.getters.connected || to.meta.noAuth) {
next()
} else {
next({ name: 'login', query: { redirect: to.path } })
}
})

View file

@ -1,11 +1,24 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'
import store from '@/store'
Vue.use(VueRouter)
export default new VueRouter({
const router = new VueRouter({
// mode: 'history', // this allow all routes to be real ones (without '#')
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
// Allow if connected or route is not protected
if (store.getters.connected || to.meta.noAuth) {
next()
} else {
store.dispatch('DISCONNECT', to)
// next({ name: 'login', query: { redirect: to.path } })
}
})
export default router

View file

@ -91,7 +91,6 @@ export default {
if (currentState !== undefined && cache) return currentState
return api.get(param ? `${uri}/${param}` : uri).then(responseData => {
console.log('api')
const data = responseData[storeKey] ? responseData[storeKey] : responseData
commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data)
return param ? state[storeKey][param] : state[storeKey]

View file

@ -1,4 +1,5 @@
import api from '@/helpers/api'
import api, { timeout } from '@/helpers/api'
import router from '@/router'
export default {
state: {
@ -18,30 +19,58 @@ export default {
},
actions: {
'LOGIN' ({ commit }, password) {
'LOGIN' ({ dispatch }, password) {
// Entering a wrong password will trigger a 401 api response.
// action `DISCONNECT` will then be triggered by the response handler but will not
// redirect to `/login` so the view can display the catched error.
return api.post('login', { password }).then(() => {
commit('SET_CONNECTED', true)
}).catch(err => {
commit('SET_CONNECTED', false)
throw err
dispatch('CONNECT')
})
},
'RESET_CONNECTED' ({ commit }) {
commit('SET_CONNECTED', false)
commit('SET_YUNOHOST_INFOS', null)
},
'LOGOUT' ({ dispatch }) {
return api.get('logout').then(() => {
dispatch('RESET_CONNECTED')
dispatch('DISCONNECT')
})
},
'DISCONNECT' ({ commit }, route) {
commit('SET_CONNECTED', false)
commit('SET_YUNOHOST_INFOS', null)
// Do not redirect if the current route is `login` so the view can display an error.
if (router.currentRoute.name === 'login') return
router.push({
name: 'login',
// Add a redirect query if next route is not unknown (like `logout`) or `login`
query: route && !['login', null].includes(route.name)
? { redirect: route.path }
: {}
})
},
'CONNECT' ({ commit, dispatch }) {
commit('SET_CONNECTED', true)
dispatch('GET_YUNOHOST_INFOS')
router.push(router.currentRoute.query.redirect || { name: 'home' })
},
'GET_YUNOHOST_INFOS' ({ commit }) {
return api.get('versions').then(versions => {
commit('SET_YUNOHOST_INFOS', versions.yunohost)
})
},
'CHECK_INSTALL' ({ dispatch }, retry = 2) {
// this action will try to query the `/installed` route 3 times every 5 s with
// a timeout of the same delay.
return timeout(api.get('installed'), 5000).then(({ installed }) => {
return installed
}).catch(err => {
if (retry > 0) {
return dispatch('CHECK_INSTALL', --retry)
}
throw err
})
}
},

View file

@ -1,29 +1,35 @@
<template>
<b-form @submit.prevent="login">
<!-- TODO add hidden domain input -->
<b-input-group>
<template v-slot:prepend>
<b-input-group-text>
<label class="sr-only" for="input-password">{{ $t('password') }}</label>
<icon iname="lock" class="sm" />
</b-input-group-text>
</template>
<b-form-input
id="input-password"
required type="password"
v-model="password"
:placeholder="$t('administration_password')" :state="isValid"
/>
<template v-slot:append>
<b-button type="submit" variant="success">
{{ $t('login') }}
</b-button>
</template>
</b-input-group>
<b-form-invalid-feedback :state="isValid">
{{ $t('wrong_password') }}
</b-form-invalid-feedback>
</b-form>
<div class="login">
<b-alert v-if="apiError" variant="danger" show>
<icon iname="exclamation-triangle" /> {{ $t(apiError) }}
</b-alert>
<b-form @submit.prevent="login">
<!-- FIXME add hidden domain input ? -->
<b-input-group>
<template v-slot:prepend>
<b-input-group-text>
<label class="sr-only" for="input-password">{{ $t('password') }}</label>
<icon iname="lock" class="sm" />
</b-input-group-text>
</template>
<b-form-input
id="input-password"
required type="password"
v-model="password" :disabled="disabled"
:placeholder="$t('administration_password')" :state="isValid"
/>
<template v-slot:append>
<b-button type="submit" variant="success" :disabled="disabled">
{{ $t('login') }}
</b-button>
</template>
</b-input-group>
<b-form-invalid-feedback :state="isValid">
{{ $t('wrong_password') }}
</b-form-invalid-feedback>
</b-form>
</div>
</template>
<script>
@ -32,26 +38,32 @@ export default {
data: () => {
return {
disabled: false,
password: '',
isValid: null
isValid: null,
apiError: undefined
}
},
methods: {
async login () {
this.$store.dispatch(
'LOGIN', this.password
).then(() => {
this.$store.dispatch('GET_YUNOHOST_INFOS')
this.$router.push(this.$route.query.redirect || '/')
}).catch(() => {
this.$store.dispatch('LOGIN', this.password).catch(() => {
this.isValid = false
})
}
},
created () {
this.$store.dispatch('CHECK_INSTALL').then(installed => {
if (installed) {
this.disabled = false
} else {
this.$router.push({ name: 'postinstall' })
}
}).catch(err => {
this.apiError = err.message
})
}
// TODO checkInstall
// beforeRouteEnter (to, from, next) {
// },
}
</script>