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">
|
<div id="app" class="container">
|
||||||
<header>
|
<header>
|
||||||
<b-navbar>
|
<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">
|
<img alt="Yunohost logo" src="./assets/logo.png">
|
||||||
</b-navbar-brand>
|
</b-navbar-brand>
|
||||||
<b-navbar-nav class="ml-auto">
|
<b-navbar-nav class="ml-auto">
|
||||||
|
@ -27,23 +27,24 @@
|
||||||
<breadcrumb v-if="$route.meta.breadcrumb" />
|
<breadcrumb v-if="$route.meta.breadcrumb" />
|
||||||
|
|
||||||
<main id="main">
|
<main id="main">
|
||||||
<router-view v-if="isReady" />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<nav>
|
<nav>
|
||||||
<b-nav>
|
<b-nav class="justify-content-center">
|
||||||
<b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes="text-secondary">
|
<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>
|
||||||
<b-nav-item href="https://yunohost.org/help" target="_blank" link-classes="text-secondary">
|
<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>
|
||||||
<b-nav-item href="https://donate.yunohost.org/" target="_blank" link-classes="text-secondary">
|
<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>
|
</b-nav-item>
|
||||||
<i18n v-if="yunohost" path="footer_version" tag="b-nav-text"
|
<i18n
|
||||||
id="yunohost-version" class="ml-auto"
|
v-if="yunohost" path="footer.version" tag="b-nav-text"
|
||||||
|
id="yunohost-version" class="ml-md-auto text-center"
|
||||||
>
|
>
|
||||||
<template v-slot:ynh>
|
<template v-slot:ynh>
|
||||||
<b-link href="https://yunohost.org">
|
<b-link href="https://yunohost.org">
|
||||||
|
@ -69,51 +70,25 @@ import { mapGetters } from 'vuex'
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
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: {
|
computed: {
|
||||||
...mapGetters(['connected', 'yunohost'])
|
...mapGetters(['connected', 'yunohost'])
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async logout () {
|
async logout () {
|
||||||
this.$store.dispatch('LOGOUT').then(() => {
|
this.$store.dispatch('LOGOUT')
|
||||||
this.$router.push({ name: 'login' })
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// This hook is only triggered at page first load
|
// This hook is only triggered at page first load
|
||||||
async created () {
|
async created () {
|
||||||
// From this hook the value of `connected` always come from the localStorage.
|
// From this hook the value of `connected` always come from the localStorage.
|
||||||
if (!this.connected) {
|
// This state may be `true` but session may have expired, by querying
|
||||||
// user is not connected: allow the login view to be rendered.
|
// yunohost infos, api may respond with `Unauthorized` in which case the `connected`
|
||||||
this.isReady = true
|
// state will be automaticly reseted and user will be prompt with the login view.
|
||||||
return
|
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>
|
</script>
|
||||||
|
|
|
@ -5,6 +5,22 @@
|
||||||
|
|
||||||
import store from '@/store'
|
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
|
* Converts an object literal into an `URLSearchParams` that can be turned into a
|
||||||
* query string or used as a body in a `fetch` call.
|
* 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) {
|
export async function handleErrors (response) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
|
store.dispatch('DISCONNECT')
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
} else if (response.status === 400) {
|
} else if (response.status === 400) {
|
||||||
const message = await response.text()
|
const message = await response.text()
|
||||||
|
|
|
@ -119,7 +119,12 @@
|
||||||
"everything_good": "Everything good!",
|
"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.",
|
"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",
|
"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": {
|
"form_errors": {
|
||||||
"email_syntax": "Invalid email: Must be alphanumeric, underscore and dash characters only (e.g. someone@example.com, s0me-1@example.com)",
|
"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",
|
"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 Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
export default new VueRouter({
|
const router = new VueRouter({
|
||||||
// mode: 'history', // this allow all routes to be real ones (without '#')
|
// mode: 'history', // this allow all routes to be real ones (without '#')
|
||||||
base: process.env.BASE_URL,
|
base: process.env.BASE_URL,
|
||||||
routes
|
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
|
if (currentState !== undefined && cache) return currentState
|
||||||
|
|
||||||
return api.get(param ? `${uri}/${param}` : uri).then(responseData => {
|
return api.get(param ? `${uri}/${param}` : uri).then(responseData => {
|
||||||
console.log('api')
|
|
||||||
const data = responseData[storeKey] ? responseData[storeKey] : responseData
|
const data = responseData[storeKey] ? responseData[storeKey] : responseData
|
||||||
commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data)
|
commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data)
|
||||||
return param ? state[storeKey][param] : state[storeKey]
|
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 {
|
export default {
|
||||||
state: {
|
state: {
|
||||||
|
@ -18,30 +19,58 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
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(() => {
|
return api.post('login', { password }).then(() => {
|
||||||
commit('SET_CONNECTED', true)
|
dispatch('CONNECT')
|
||||||
}).catch(err => {
|
|
||||||
commit('SET_CONNECTED', false)
|
|
||||||
throw err
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
'RESET_CONNECTED' ({ commit }) {
|
|
||||||
commit('SET_CONNECTED', false)
|
|
||||||
commit('SET_YUNOHOST_INFOS', null)
|
|
||||||
},
|
|
||||||
|
|
||||||
'LOGOUT' ({ dispatch }) {
|
'LOGOUT' ({ dispatch }) {
|
||||||
return api.get('logout').then(() => {
|
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 }) {
|
'GET_YUNOHOST_INFOS' ({ commit }) {
|
||||||
return api.get('versions').then(versions => {
|
return api.get('versions').then(versions => {
|
||||||
commit('SET_YUNOHOST_INFOS', versions.yunohost)
|
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>
|
<template>
|
||||||
<b-form @submit.prevent="login">
|
<div class="login">
|
||||||
<!-- TODO add hidden domain input -->
|
<b-alert v-if="apiError" variant="danger" show>
|
||||||
<b-input-group>
|
<icon iname="exclamation-triangle" /> {{ $t(apiError) }}
|
||||||
<template v-slot:prepend>
|
</b-alert>
|
||||||
<b-input-group-text>
|
|
||||||
<label class="sr-only" for="input-password">{{ $t('password') }}</label>
|
<b-form @submit.prevent="login">
|
||||||
<icon iname="lock" class="sm" />
|
<!-- FIXME add hidden domain input ? -->
|
||||||
</b-input-group-text>
|
<b-input-group>
|
||||||
</template>
|
<template v-slot:prepend>
|
||||||
<b-form-input
|
<b-input-group-text>
|
||||||
id="input-password"
|
<label class="sr-only" for="input-password">{{ $t('password') }}</label>
|
||||||
required type="password"
|
<icon iname="lock" class="sm" />
|
||||||
v-model="password"
|
</b-input-group-text>
|
||||||
:placeholder="$t('administration_password')" :state="isValid"
|
</template>
|
||||||
/>
|
<b-form-input
|
||||||
<template v-slot:append>
|
id="input-password"
|
||||||
<b-button type="submit" variant="success">
|
required type="password"
|
||||||
{{ $t('login') }}
|
v-model="password" :disabled="disabled"
|
||||||
</b-button>
|
:placeholder="$t('administration_password')" :state="isValid"
|
||||||
</template>
|
/>
|
||||||
</b-input-group>
|
<template v-slot:append>
|
||||||
<b-form-invalid-feedback :state="isValid">
|
<b-button type="submit" variant="success" :disabled="disabled">
|
||||||
{{ $t('wrong_password') }}
|
{{ $t('login') }}
|
||||||
</b-form-invalid-feedback>
|
</b-button>
|
||||||
</b-form>
|
</template>
|
||||||
|
</b-input-group>
|
||||||
|
<b-form-invalid-feedback :state="isValid">
|
||||||
|
{{ $t('wrong_password') }}
|
||||||
|
</b-form-invalid-feedback>
|
||||||
|
</b-form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -32,26 +38,32 @@ export default {
|
||||||
|
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
|
disabled: false,
|
||||||
password: '',
|
password: '',
|
||||||
isValid: null
|
isValid: null,
|
||||||
|
apiError: undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async login () {
|
async login () {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch('LOGIN', this.password).catch(() => {
|
||||||
'LOGIN', this.password
|
|
||||||
).then(() => {
|
|
||||||
this.$store.dispatch('GET_YUNOHOST_INFOS')
|
|
||||||
this.$router.push(this.$route.query.redirect || '/')
|
|
||||||
}).catch(() => {
|
|
||||||
this.isValid = false
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue