parent
9dea6185ad
commit
cc12719df5
@ -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') |
||||
} |
||||
}) |
||||
}) |
@ -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') |
@ -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> |
Loading…
Reference in new issue