mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
add installed check and rework login/redirect mecanism
This commit is contained in:
parent
66e4584d2b
commit
7710e6a1c3
8 changed files with 140 additions and 102 deletions
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 } })
|
||||
}
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue