Merge pull request #476 from YunoHost/up-postinstall

postinstall: Update steps with first admin user creation
This commit is contained in:
Alexandre Aubin 2022-10-18 18:52:01 +02:00 committed by GitHub
commit 64b43be9cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 83 additions and 113 deletions

View file

@ -389,15 +389,17 @@
"ports": "Ports", "ports": "Ports",
"postinstall": { "postinstall": {
"force": "Force the post-install", "force": "Force the post-install",
"title": "Postinstall" "title": "Postinstall",
"user": {
"title": "Create first admin user",
"first_user_help": "This user will be granted admin privileges and will be allowed to connect to this administration interface as well as directly to the server via SSH.\nAs it is a regular user, you will also be able to connect to the user portal (SSO) with its credentials.\nOnce the post-installation is complete, you will be able to create other admin users by adding them into the 'admins' group."
}
}, },
"postinstall_domain": "This is the first domain name linked to your YunoHost server, but also the one which will be used by your server's users to access the authentication portal. Accordingly, it will be visible by everyone, so choose it carefully.", "postinstall_domain": "This is the first domain name linked to your YunoHost server, but also the one which will be used by your server's users to access the authentication portal. Accordingly, it will be visible by everyone, so choose it carefully.",
"postinstall_intro_1": "Congratulations! YunoHost has been successfully installed.", "postinstall_intro_1": "Congratulations! YunoHost has been successfully installed.",
"postinstall_intro_2": "Two more configuration steps are required to activate you server's services.", "postinstall_intro_2": "Two more configuration steps are required to activate you server's services.",
"postinstall_intro_3": "You can obtain more information by visiting the <a href='//yunohost.org/en/install/hardware:vps_debian#fa-cog-proceed-with-the-initial-configuration' target='_blank'>appropriate documentation page</a>", "postinstall_intro_3": "You can obtain more information by visiting the <a href='//yunohost.org/en/install/hardware:vps_debian#fa-cog-proceed-with-the-initial-configuration' target='_blank'>appropriate documentation page</a>",
"postinstall_password": "This password will be used to manage everything on your server. Take the time to choose it wisely.",
"postinstall_set_domain": "Set main domain", "postinstall_set_domain": "Set main domain",
"postinstall_set_password": "Set administration password",
"previous": "Previous", "previous": "Previous",
"protocol": "Protocol", "protocol": "Protocol",
"readme": "Readme", "readme": "Readme",

View file

@ -12,7 +12,7 @@
<span v-html="$t('postinstall_intro_3')" /> <span v-html="$t('postinstall_intro_3')" />
</p> </p>
<b-button size="lg" variant="primary" @click="goToStep('domain')"> <b-button size="lg" variant="success" @click="goToStep('domain')">
{{ $t('begin') }} {{ $t('begin') }}
</b-button> </b-button>
</template> </template>
@ -33,16 +33,23 @@
</b-button> </b-button>
</template> </template>
<!-- PASSWORD SETUP STEP --> <!-- FIRST USER SETUP STEP -->
<template v-else-if="step === 'password'"> <template v-else-if="step === 'user'">
<password-form <card-form
:title="$t('postinstall_set_password')" :submit-text="$t('next')" :server-error="serverError" :title="$t('postinstall.user.title')" icon="user-plus"
@submit="setPassword" :validation="$v" :server-error="serverError"
:submit-text="$t('next')" @submit.prevent="setUser"
> >
<template #disclaimer> <read-only-alert-item
<p class="alert alert-warning" v-t="'postinstall_password'" /> :label="$t('postinstall.user.first_user_help')"
</template> type="info"
</password-form> />
<form-field
v-for="(field, name) in fields" :key="name"
v-bind="field" v-model="user[name]" :validation="$v.user[name]"
/>
</card-form>
<b-button variant="primary" @click="goToStep('domain')" class="mt-3"> <b-button variant="primary" @click="goToStep('domain')" class="mt-3">
<icon iname="chevron-left" /> {{ $t('previous') }} <icon iname="chevron-left" /> {{ $t('previous') }}
@ -74,25 +81,58 @@
</template> </template>
<script> <script>
import { validationMixin } from 'vuelidate'
import api from '@/api' import api from '@/api'
import { DomainForm, PasswordForm } from '@/views/_partials' import { DomainForm } from '@/views/_partials'
import Login from '@/views/Login' import Login from '@/views/Login'
import { alphalownum_, required, minLength, name, sameAs } from '@/helpers/validators'
export default { export default {
name: 'PostInstall', name: 'PostInstall',
mixins: [validationMixin],
components: { components: {
DomainForm, DomainForm,
PasswordForm,
Login Login
}, },
data () { data () {
return { return {
step: 'start', step: 'start',
serverError: '',
domain: undefined, domain: undefined,
password: undefined, user: {
serverError: '' username: '',
fullname: '',
password: '',
confirmation: ''
},
fields: {
username: {
label: this.$i18n.t('user_username'),
props: { id: 'username', placeholder: this.$i18n.t('placeholder.username') }
},
fullname: {
label: this.$i18n.t('user_fullname'),
props: { id: 'fullname', placeholder: this.$i18n.t('placeholder.fullname') }
},
password: {
label: this.$i18n.t('password'),
description: this.$i18n.t('good_practices_about_admin_password'),
descriptionVariant: 'warning',
props: { id: 'password', placeholder: '••••••••', type: 'password' }
},
confirmation: {
label: this.$i18n.t('password_confirmation'),
props: { id: 'confirmation', placeholder: '••••••••', type: 'password' }
}
}
} }
}, },
@ -104,11 +144,10 @@ export default {
setDomain ({ domain }) { setDomain ({ domain }) {
this.domain = domain this.domain = domain
this.goToStep('password') this.goToStep('user')
}, },
async setPassword ({ password }) { async setUser () {
this.password = password
const confirmed = await this.$askConfirmation( const confirmed = await this.$askConfirmation(
this.$i18n.t('confirm_postinstall', { domain: this.domain }) this.$i18n.t('confirm_postinstall', { domain: this.domain })
) )
@ -117,22 +156,29 @@ export default {
}, },
performPostInstall (force = false) { performPostInstall (force = false) {
const data = {
domain: this.domain,
username: this.user.username,
fullname: this.user.fullname,
password: this.user.password
}
// FIXME does the api will throw an error for bad passwords ? // FIXME does the api will throw an error for bad passwords ?
api.post( api.post(
'postinstall' + (force ? '?force_diskspace' : ''), 'postinstall' + (force ? '?force_diskspace' : ''),
{ domain: this.domain, password: this.password }, data,
{ key: 'postinstall' } { key: 'postinstall' }
).then(() => { ).then(() => {
// Display success message and allow the user to login // Display success message and allow the user to login
this.goToStep('login') this.goToStep('login')
}).catch(err => { }).catch(err => {
const hasWordsInError = (words) => words.some((word) => (err.key || err.message).includes(word))
if (err.name !== 'APIBadRequestError') throw err if (err.name !== 'APIBadRequestError') throw err
if (err.key === 'postinstall_low_rootfsspace') { if (err.key === 'postinstall_low_rootfsspace') {
this.step = 'rootfsspace-error' this.step = 'rootfsspace-error'
} else if (err.key.includes('password')) { } else if (hasWordsInError(['domain', 'dyndns'])) {
this.step = 'password'
} else if (['domain', 'dyndns'].some(word => err.key.includes(word))) {
this.step = 'domain' this.step = 'domain'
} else if (hasWordsInError(['password', 'user'])) {
this.step = 'user'
} else { } else {
throw err throw err
} }
@ -141,6 +187,17 @@ export default {
} }
}, },
validations () {
return {
user: {
username: { required, alphalownum_ },
fullname: { required, name },
password: { required, passwordLenght: minLength(8) },
confirmation: { required, passwordMatch: sameAs('password') }
}
}
},
created () { created () {
this.$store.dispatch('CHECK_INSTALL').then(installed => { this.$store.dispatch('CHECK_INSTALL').then(installed => {
if (installed) { if (installed) {

View file

@ -1,88 +0,0 @@
<template>
<card-form
:title="title" icon="key-modern" :submit-text="submitText"
:validation="$v" :server-error="serverError"
@submit.prevent="onSubmit"
>
<template #disclaimer>
<p class="alert alert-warning">
{{ $t('good_practices_about_admin_password') }}
</p>
<slot name="disclaimer" />
<hr>
</template>
<slot name="extra" v-bind="{ v: $v, fields, form }">
<form-field
v-for="(value, key) in extra.fields" :key="key"
v-bind="value" v-model="$v.form.$model[key]" :validation="$v.form[key]"
/>
</slot>
<!-- ADMIN PASSWORD -->
<form-field v-bind="fields.password" v-model="form.password" :validation="$v.form.password" />
<!-- ADMIN PASSWORD CONFIRMATION -->
<form-field v-bind="fields.confirmation" v-model="form.confirmation" :validation="$v.form.confirmation" />
</card-form>
</template>
<script>
import { validationMixin } from 'vuelidate'
import { required, minLength, sameAs } from '@/helpers/validators'
export default {
name: 'PasswordForm',
props: {
title: { type: String, required: true },
submitText: { type: String, default: null },
serverError: { type: String, default: '' },
extra: { type: Object, default: () => ({ form: {}, fields: {}, validations: {} }) }
},
data () {
return {
form: {
password: '',
confirmation: '',
...this.extra.form
},
fields: {
password: {
label: this.$i18n.t('password'),
props: { id: 'password', type: 'password', placeholder: '••••••••' }
},
confirmation: {
label: this.$i18n.t('password_confirmation'),
props: { id: 'confirmation', type: 'password', placeholder: '••••••••' }
},
...this.extra.fields
}
}
},
validations () {
return {
form: {
password: { required, passwordLenght: minLength(8) },
confirmation: { required, passwordMatch: sameAs('password') },
...this.extra.validations
}
}
},
methods: {
onSubmit () {
this.$emit('submit', this.form)
}
},
mixins: [validationMixin]
}
</script>

View file

@ -7,4 +7,3 @@ export { default as HistoryConsole } from './HistoryConsole'
export { default as ViewLockOverlay } from './ViewLockOverlay' export { default as ViewLockOverlay } from './ViewLockOverlay'
export { default as DomainForm } from './DomainForm' export { default as DomainForm } from './DomainForm'
export { default as PasswordForm } from './PasswordForm'