|
|
<!--
|
|
|
- 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>
|