nextcloud-server/core/src/views/Setup.vue

489 lines
13 KiB
Vue

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

<!--
- SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<form
ref="form"
class="setup-form"
:class="{ 'setup-form--loading': loading }"
action=""
data-cy-setup-form
method="POST"
@submit="onSubmit">
<!-- Autoconfig info -->
<NcNoteCard
v-if="config.hasAutoconfig"
:heading="t('core', 'Autoconfig file detected')"
data-cy-setup-form-note="autoconfig"
type="success">
{{ t('core', 'The setup form below is pre-filled with the values from the config file.') }}
</NcNoteCard>
<!-- Htaccess warning -->
<NcNoteCard
v-if="config.htaccessWorking === false"
:heading="t('core', 'Security warning')"
data-cy-setup-form-note="htaccess"
type="warning">
<p v-html="htaccessWarning" />
</NcNoteCard>
<!-- Various errors -->
<NcNoteCard
v-for="(error, index) in errors"
:key="index"
:heading="error.heading"
data-cy-setup-form-note="error"
type="error">
{{ error.message }}
</NcNoteCard>
<!-- Admin creation -->
<fieldset class="setup-form__administration">
<legend>{{ t('core', 'Create administration account') }}</legend>
<!-- Username -->
<NcTextField
v-model="config.adminlogin"
:label="t('core', 'Administration account name')"
data-cy-setup-form-field="adminlogin"
name="adminlogin"
required />
<!-- Password -->
<NcPasswordField
v-model="config.adminpass"
:label="t('core', 'Administration account password')"
data-cy-setup-form-field="adminpass"
name="adminpass"
required />
<!-- Password entropy -->
<NcNoteCard v-show="config.adminpass !== ''" :type="passwordHelperType">
{{ passwordHelperText }}
</NcNoteCard>
</fieldset>
<!-- Autoconfig toggle -->
<details :open="!isValidAutoconfig" data-cy-setup-form-advanced-config>
<summary>{{ t('core', 'Storage & database') }}</summary>
<!-- Data folder -->
<fieldset class="setup-form__data-folder">
<NcTextField
v-model="config.directory"
:label="t('core', 'Data folder')"
:placeholder="config.serverRoot + '/data'"
required
autocomplete="off"
autocapitalize="none"
data-cy-setup-form-field="directory"
name="directory"
spellcheck="false" />
</fieldset>
<!-- Database -->
<fieldset class="setup-form__database">
<legend>{{ t('core', 'Database configuration') }}</legend>
<!-- Database type select -->
<fieldset class="setup-form__database-type">
<legend class="hidden-visually">
{{ t('core', 'Database type') }}
</legend>
<!-- Using v-show instead of v-if ensures that the input dbtype remains set even when only one database engine is available -->
<p v-show="!firstAndOnlyDatabase" :class="`setup-form__database-type-select--${DBTypeGroupDirection}`" class="setup-form__database-type-select">
<NcCheckboxRadioSwitch
v-for="(name, db) in config.databases"
:key="db"
v-model="config.dbtype"
:button-variant="true"
:data-cy-setup-form-field="`dbtype-${db}`"
:value="db"
:button-variant-grouped="DBTypeGroupDirection"
name="dbtype"
type="radio">
{{ name }}
</NcCheckboxRadioSwitch>
</p>
<NcNoteCard v-if="firstAndOnlyDatabase" data-cy-setup-form-db-note="single-db" type="warning">
{{ t('core', 'Only {firstAndOnlyDatabase} is available.', { firstAndOnlyDatabase }) }}<br>
{{ t('core', 'Install and activate additional PHP modules to choose other database types.') }}<br>
<a :href="links.adminSourceInstall" target="_blank" rel="noreferrer noopener">
{{ t('core', 'For more details check out the documentation.') }} ↗
</a>
</NcNoteCard>
<NcNoteCard
v-if="config.dbtype === 'sqlite'"
:heading="t('core', 'Performance warning')"
data-cy-setup-form-db-note="sqlite"
type="warning">
{{ t('core', 'You chose SQLite as database.') }}<br>
{{ t('core', 'SQLite should only be used for minimal and development instances. For production we recommend a different database backend.') }}<br>
{{ t('core', 'If you use clients for file syncing, the use of SQLite is highly discouraged.') }}
</NcNoteCard>
</fieldset>
<!-- Database configuration -->
<fieldset v-if="config.dbtype !== 'sqlite'">
<legend class="hidden-visually">
{{ t('core', 'Database connection') }}
</legend>
<NcTextField
v-model="config.dbuser"
:label="t('core', 'Database user')"
autocapitalize="none"
autocomplete="off"
data-cy-setup-form-field="dbuser"
name="dbuser"
spellcheck="false"
required />
<NcPasswordField
v-model="config.dbpass"
:label="t('core', 'Database password')"
autocapitalize="none"
autocomplete="off"
data-cy-setup-form-field="dbpass"
name="dbpass"
spellcheck="false"
required />
<NcTextField
v-model="config.dbname"
:label="t('core', 'Database name')"
autocapitalize="none"
autocomplete="off"
data-cy-setup-form-field="dbname"
name="dbname"
pattern="[0-9a-zA-Z\$_\-]+"
spellcheck="false"
required />
<NcTextField
v-if="config.dbtype === 'oci'"
v-model="config.dbtablespace"
:label="t('core', 'Database tablespace')"
autocapitalize="none"
autocomplete="off"
data-cy-setup-form-field="dbtablespace"
name="dbtablespace"
spellcheck="false" />
<NcTextField
v-model="config.dbhost"
:helper-text="t('core', 'Please specify the port number along with the host name (e.g., localhost:5432).')"
:label="t('core', 'Database host')"
:placeholder="t('core', 'localhost')"
autocapitalize="none"
autocomplete="off"
data-cy-setup-form-field="dbhost"
name="dbhost"
spellcheck="false" />
</fieldset>
</fieldset>
</details>
<!-- Submit -->
<NcButton
class="setup-form__button"
:class="{ 'setup-form__button--loading': loading }"
:disabled="loading"
:loading="loading"
:wide="true"
alignment="center-reverse"
data-cy-setup-form-submit
type="submit"
variant="primary">
<template #icon>
<NcLoadingIcon v-if="loading" />
<IconArrowRight v-else />
</template>
{{ loading ? t('core', 'Installing …') : t('core', 'Install') }}
</NcButton>
<!-- Help note -->
<NcNoteCard data-cy-setup-form-note="help" type="info">
{{ t('core', 'Need help?') }}
<a target="_blank" rel="noreferrer noopener" :href="links.adminInstall">{{ t('core', 'See the documentation') }} ↗</a>
</NcNoteCard>
</form>
</template>
<script lang="ts">
import type { DbType, SetupConfig, SetupLinks } from '../install.ts'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import DomPurify from 'dompurify'
import { defineComponent } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
enum PasswordStrength {
VeryWeak,
Weak,
Moderate,
Strong,
VeryStrong,
ExtremelyStrong,
}
/**
*
* @param password
*/
function checkPasswordEntropy(password: string = ''): PasswordStrength {
const uniqueCharacters = new Set(password)
const entropy = parseInt(Math.log2(Math.pow(parseInt(uniqueCharacters.size.toString()), password.length)).toFixed(2))
if (entropy < 16) {
return PasswordStrength.VeryWeak
} else if (entropy < 31) {
return PasswordStrength.Weak
} else if (entropy < 46) {
return PasswordStrength.Moderate
} else if (entropy < 61) {
return PasswordStrength.Strong
} else if (entropy < 76) {
return PasswordStrength.VeryStrong
}
return PasswordStrength.ExtremelyStrong
}
export default defineComponent({
name: 'Setup',
components: {
IconArrowRight,
NcButton,
NcCheckboxRadioSwitch,
NcLoadingIcon,
NcNoteCard,
NcPasswordField,
NcTextField,
},
setup() {
return {
t,
}
},
data() {
return {
config: {} as SetupConfig,
links: {} as SetupLinks,
isValidAutoconfig: false,
loading: false,
}
},
computed: {
passwordHelperText(): string {
if (this.config?.adminpass === '') {
return ''
}
const passwordStrength = checkPasswordEntropy(this.config?.adminpass)
switch (passwordStrength) {
case PasswordStrength.VeryWeak:
return t('core', 'Password is too weak')
case PasswordStrength.Weak:
return t('core', 'Password is weak')
case PasswordStrength.Moderate:
return t('core', 'Password is average')
case PasswordStrength.Strong:
return t('core', 'Password is strong')
case PasswordStrength.VeryStrong:
return t('core', 'Password is very strong')
case PasswordStrength.ExtremelyStrong:
return t('core', 'Password is extremely strong')
}
return t('core', 'Unknown password strength')
},
passwordHelperType() {
if (checkPasswordEntropy(this.config?.adminpass) < PasswordStrength.Moderate) {
return 'error'
}
if (checkPasswordEntropy(this.config?.adminpass) < PasswordStrength.Strong) {
return 'warning'
}
return 'success'
},
firstAndOnlyDatabase(): string | null {
const dbNames = Object.values(this.config?.databases || {})
if (dbNames.length === 1) {
return dbNames[0]
}
return null
},
DBTypeGroupDirection() {
const databases = Object.keys(this.config?.databases || {})
// If we have more than 3 databases, we want to display them vertically
if (databases.length > 3) {
return 'vertical'
}
return 'horizontal'
},
htaccessWarning(): string {
// We use v-html, let's make sure we're safe
const message = [
t('core', 'Your data directory and files are probably accessible from the internet because the <code>.htaccess</code> file does not work.'),
t('core', 'For information how to properly configure your server, please {linkStart}see the documentation{linkEnd}', {
linkStart: '<a href="' + this.links.adminInstall + '" target="_blank" rel="noreferrer noopener">',
linkEnd: '</a>',
}, { escape: false }),
].join('<br>')
return DomPurify.sanitize(message)
},
errors() {
return (this.config?.errors || []).map((error) => {
if (typeof error === 'string') {
return {
heading: '',
message: error,
}
}
// f no hint is set, we don't want to show a heading
if (error.hint === '') {
return {
heading: '',
message: error.error,
}
}
return {
heading: error.error,
message: error.hint,
}
})
},
},
beforeMount() {
// Needs to only read the state once we're mounted
// for Cypress to be properly initialized.
this.config = loadState<SetupConfig>('core', 'config')
this.links = loadState<SetupLinks>('core', 'links')
},
mounted() {
// Set the first database type as default if none is set
if (this.config.dbtype === '') {
this.config.dbtype = Object.keys(this.config.databases).at(0) as DbType
}
// Validate the legitimacy of the autoconfig
if (this.config.hasAutoconfig) {
const form = this.$refs.form as HTMLFormElement
// Check the form without the administration account fields
form.querySelectorAll('input[name="adminlogin"], input[name="adminpass"]').forEach((input) => {
input.removeAttribute('required')
})
if (form.checkValidity() && this.config.errors.length === 0) {
this.isValidAutoconfig = true
} else {
this.isValidAutoconfig = false
}
// Restore the required attribute
// Check the form without the administration account fields
form.querySelectorAll('input[name="adminlogin"], input[name="adminpass"]').forEach((input) => {
input.setAttribute('required', 'true')
})
}
},
methods: {
async onSubmit() {
this.loading = true
},
},
})
</script>
<style lang="scss">
form {
padding: calc(3 * var(--default-grid-baseline));
color: var(--color-main-text);
border-radius: var(--border-radius-container);
background-color: var(--color-main-background-blur);
box-shadow: 0 0 10px var(--color-box-shadow);
-webkit-backdrop-filter: var(--filter-background-blur);
backdrop-filter: var(--filter-background-blur);
max-width: 300px;
margin-bottom: 30px;
> fieldset:first-child,
> .notecard:first-child {
margin-top: 0;
}
> .notecard:last-child {
margin-bottom: 0;
}
fieldset,
details {
margin-block: 1rem;
}
.setup-form__button:not(.setup-form__button--loading) {
.material-design-icon {
transition: all linear var(--animation-quick);
}
&:hover .material-design-icon {
transform: translateX(0.2em);
}
}
// Db select required styling
.setup-form__database-type-select {
display: flex;
&--vertical {
flex-direction: column;
}
}
}
code {
background-color: var(--color-background-dark);
margin-top: 1rem;
padding: 0 0.3em;
border-radius: var(--border-radius);
}
// Various overrides
.input-field {
margin-block-start: 1rem !important;
}
.notecard__heading {
font-size: inherit !important;
}
</style>