diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2025-03-13 08:55:37 +0100 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2025-03-13 16:00:18 +0100 |
commit | cc12719df509174f54d4479745902da633cd85a8 (patch) | |
tree | 03c95c0ed63462715f451fca92f17d1f0c9558fb /core/src | |
parent | 9dea6185ada1f9f891f2c64d256551c6ad171d29 (diff) | |
download | nextcloud-server-cc12719df509174f54d4479745902da633cd85a8.tar.gz nextcloud-server-cc12719df509174f54d4479745902da633cd85a8.zip |
feat(core): migrate setup to vue
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Diffstat (limited to 'core/src')
-rw-r--r-- | core/src/install.js | 156 | ||||
-rw-r--r-- | core/src/install.ts | 49 | ||||
-rw-r--r-- | core/src/views/Setup.vue | 420 |
3 files changed, 469 insertions, 156 deletions
diff --git a/core/src/install.js b/core/src/install.js deleted file mode 100644 index ea2e2996a2a..00000000000 --- a/core/src/install.js +++ /dev/null @@ -1,156 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import $ from 'jquery' -import { translate as t } from '@nextcloud/l10n' -import { linkTo } from '@nextcloud/router' - -import { getToken } from './OC/requesttoken.js' -import getURLParameter from './Util/get-url-parameter.js' - -import './jquery/showpassword.js' - -import 'jquery-ui/ui/widgets/button.js' -import 'jquery-ui/themes/base/theme.css' -import 'jquery-ui/themes/base/button.css' - -import 'strengthify' -import 'strengthify/strengthify.css' - -window.addEventListener('DOMContentLoaded', function() { - const dbtypes = { - sqlite: !!$('#hasSQLite').val(), - mysql: !!$('#hasMySQL').val(), - postgresql: !!$('#hasPostgreSQL').val(), - oracle: !!$('#hasOracle').val(), - } - - $('#selectDbType').buttonset() - // change links inside an info box back to their default appearance - $('#selectDbType p.info a').button('destroy') - - if ($('#hasSQLite').val()) { - $('#use_other_db').hide() - $('#use_oracle_db').hide() - } else { - $('#sqliteInformation').hide() - } - $('#adminlogin').change(function() { - $('#adminlogin').val($.trim($('#adminlogin').val())) - }) - $('#sqlite').click(function() { - $('#use_other_db').slideUp(250) - $('#use_oracle_db').slideUp(250) - $('#sqliteInformation').show() - $('#dbname').attr('pattern', '[0-9a-zA-Z$_-]+') - }) - - $('#mysql,#pgsql').click(function() { - $('#use_other_db').slideDown(250) - $('#use_oracle_db').slideUp(250) - $('#sqliteInformation').hide() - $('#dbname').attr('pattern', '[0-9a-zA-Z$_-]+') - }) - - $('#oci').click(function() { - $('#use_other_db').slideDown(250) - $('#use_oracle_db').show(250) - $('#sqliteInformation').hide() - $('#dbname').attr('pattern', '[0-9a-zA-Z$_-.]+') - }) - - $('#showAdvanced').click(function(e) { - e.preventDefault() - $('#datadirContent').slideToggle(250) - $('#databaseBackend').slideToggle(250) - $('#databaseField').slideToggle(250) - }) - $('form').submit(function() { - // Save form parameters - const post = $(this).serializeArray() - - // Show spinner while finishing setup - $('.float-spinner').show(250) - - // Disable inputs - $('input[type="submit"]').attr('disabled', 'disabled').val($('input[type="submit"]').data('finishing')) - $('input', this).addClass('ui-state-disabled').attr('disabled', 'disabled') - // only disable buttons if they are present - if ($('#selectDbType').find('.ui-button').length > 0) { - $('#selectDbType').buttonset('disable') - } - $('.strengthify-wrapper, .tipsy') - .css('filter', 'alpha(opacity=30)') - .css('opacity', 0.3) - - // Create the form - const form = $('<form>') - form.attr('action', $(this).attr('action')) - form.attr('method', 'POST') - - for (let i = 0; i < post.length; i++) { - const input = $('<input type="hidden">') - input.attr(post[i]) - form.append(input) - } - - // Add redirect_url - const redirectURL = getURLParameter('redirect_url') - if (redirectURL) { - const redirectURLInput = $('<input type="hidden">') - redirectURLInput.attr({ - name: 'redirect_url', - value: redirectURL, - }) - form.append(redirectURLInput) - } - - // Submit the form - form.appendTo(document.body) - form.submit() - return false - }) - - // Expand latest db settings if page was reloaded on error - const currentDbType = $('input[type="radio"]:checked').val() - - if (currentDbType === undefined) { - $('input[type="radio"]').first().click() - } - - if ( - currentDbType === 'sqlite' - || (dbtypes.sqlite && currentDbType === undefined) - ) { - $('#datadirContent').hide(250) - $('#databaseBackend').hide(250) - $('#databaseField').hide(250) - $('.float-spinner').hide(250) - } - - $('#adminpass').strengthify({ - zxcvbn: linkTo('core', 'vendor/zxcvbn/dist/zxcvbn.js'), - titles: [ - t('core', 'Very weak password'), - t('core', 'Weak password'), - t('core', 'So-so password'), - t('core', 'Good password'), - t('core', 'Strong password'), - ], - drawTitles: true, - nonce: btoa(getToken()), - }) - - $('#dbpass').showPassword().keyup() - $('.toggle-password').click(function(event) { - event.preventDefault() - const currentValue = $(this).parent().children('input').attr('type') - if (currentValue === 'password') { - $(this).parent().children('input').attr('type', 'text') - } else { - $(this).parent().children('input').attr('type', 'password') - } - }) -}) diff --git a/core/src/install.ts b/core/src/install.ts new file mode 100644 index 00000000000..61c3747d02a --- /dev/null +++ b/core/src/install.ts @@ -0,0 +1,49 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import Vue from 'vue' +import Setup from './views/Setup.vue' + +type Error = { + error: string + hint: string +} + +export type DbType = 'sqlite' | 'mysql' | 'pgsql' | 'oci' + +export type SetupConfig = { + adminlogin: string + adminpass: string + dbuser: string + dbpass: string + dbname: string + dbtablespace: string + dbhost: string + dbtype: DbType | '' + + hasSQLite: boolean + hasMySQL: boolean + hasPostgreSQL: boolean + hasOracle: boolean + databases: Record<DbType, string> + + dbIsSet: boolean + directory: string + directoryIsSet: boolean + hasAutoconfig: boolean + htaccessWorking: boolean + serverRoot: string + + errors: string[]|Error[] +} + +export type SetupLinks = { + adminInstall: string + adminSourceInstall: string + adminDBConfiguration: string +} + +const SetupVue = Vue.extend(Setup) +new SetupVue().$mount('#content') diff --git a/core/src/views/Setup.vue b/core/src/views/Setup.vue new file mode 100644 index 00000000000..149645307f5 --- /dev/null +++ b/core/src/views/Setup.vue @@ -0,0 +1,420 @@ +<!-- + - 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="" + method="POST" + @submit="onSubmit"> + <!-- Autoconfig info --> + <NcNoteCard v-if="config.hasAutoconfig" + :heading="t('core', 'Autoconfig file detected')" + 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')" + type="warning"> + <p v-html="htaccessWarning" /> + </NcNoteCard> + + <!-- Various errors --> + <NcNoteCard v-for="(error, index) in errors" + :key="index" + :heading="error.heading" + 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')" + name="adminlogin" + required /> + + <!-- Password --> + <NcPasswordField v-model="config.adminpass" + :label="t('core', 'Administration account password')" + name="adminpass" + required /> + + <!-- Password entropy --> + <NcNoteCard v-show="config.adminpass !== ''" :type="passwordHelperType"> + {{ passwordHelperText }} + </NcNoteCard> + </fieldset> + + <!-- Autoconfig toggle --> + <details :open="!isValidAutoconfig"> + <summary>{{ t('core', 'Advanced settings') }}</summary> + + <!-- Data folder --> + <fieldset class="setup-form__data-folder"> + <legend>{{ t('core', 'Data folder') }}</legend> + <NcTextField v-model="config.directory" + :label="t('core', 'Data folder')" + :placeholder="config.serverRoot + '/data'" + required + autocomplete="off" + autocapitalize="none" + 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>{{ t('core', 'Database type') }}</legend> + <p v-if="Object.keys(config.databases).length > 1" class="setup-form__database-type-select"> + <NcCheckboxRadioSwitch v-for="(name, db) in config.databases" + :key="db" + v-model="config.dbtype" + :button-variant="true" + :value="db" + name="dbtype" + button-variant-grouped="horizontal" + type="radio"> + {{ name }} + </NcCheckboxRadioSwitch> + </p> + + <NcNoteCard v-else type="warning"> + {{ t('core', 'Only {db} is available.', { db: Object.values(config.databases).at(0) }) }}<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')" + 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'"> + <NcTextField v-model="config.dbuser" + :label="t('core', 'Database user')" + autocapitalize="none" + autocomplete="off" + name="dbuser" + spellcheck="false" + required /> + + <NcPasswordField v-model="config.dbpass" + :label="t('core', 'Database password')" + autocapitalize="none" + autocomplete="off" + name="dbpass" + spellcheck="false" + required /> + + <NcTextField v-model="config.dbname" + :label="t('core', 'Database name')" + autocapitalize="none" + autocomplete="off" + 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" + 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" + 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" + native-type="submit" + type="primary"> + <template #icon> + <NcLoadingIcon v-if="loading" /> + <IconArrowRight v-else /> + </template> + {{ loading ? t('core', 'Installing …') : t('core', 'Install') }} + </NcButton> + + <!-- Help note --> + <NcNoteCard 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' + +import { defineComponent } from 'vue' +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' +import DomPurify from 'dompurify' + +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' + +const config = loadState<SetupConfig>('core', 'config') +const links = loadState<SetupLinks>('core', 'links') + +enum PasswordStrength { + VeryWeak, + Weak, + Moderate, + Strong, + VeryStrong, + ExtremelyStrong, +} + +export default defineComponent({ + name: 'Setup', + + components: { + IconArrowRight, + NcButton, + NcCheckboxRadioSwitch, + NcLoadingIcon, + NcNoteCard, + NcPasswordField, + NcTextField, + }, + + setup() { + return { + links, + t, + } + }, + + data() { + return { + config, + isValidAutoconfig: false, + loading: false, + } + }, + + computed: { + passwordHelperText(): string { + if (this.config.adminpass === '') { + return '' + } + + const passwordStrength = this.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 (this.checkPasswordEntropy(this.config.adminpass) < PasswordStrength.Moderate) { + return 'error' + } + if (this.checkPasswordEntropy(this.config.adminpass) < PasswordStrength.Strong) { + return 'warning' + } + return 'success' + }, + + 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="' + 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, + } + }) + }, + }, + + mounted() { + 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 + }, + + 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 + }, + }, +}) +</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; + } + +} + +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> |