Merge pull request #33781 from nextcloud/port/login-form/vue-password-components

Use new vue components in login form
This commit is contained in:
Carl Schwan 2022-09-06 16:54:33 +02:00 committed by GitHub
commit 36b2d3dc2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 174 additions and 169 deletions

Binary file not shown.

View File

@ -1,7 +0,0 @@
#reset-password p {
position: relative;
}
.text-center {
text-align: center;
}

View File

@ -62,3 +62,9 @@ export default {
}, },
} }
</script> </script>
<style lang="scss" scoped>
.button-vue {
margin-top: .5rem;
}
</style>

View File

@ -21,28 +21,28 @@
<template> <template>
<form ref="loginForm" <form ref="loginForm"
class="login-form"
method="post" method="post"
name="login" name="login"
:action="loginActionUrl" :action="loginActionUrl"
@submit="submit"> @submit="submit">
<fieldset> <fieldset class="login-form__fieldset">
<div v-if="apacheAuthFailed" <NcNoteCard v-if="apacheAuthFailed"
class="warning"> :title="t('core', 'Server side authentication failed!')"
{{ t('core', 'Server side authentication failed!') }}<br> type="warning">
<small>{{ t('core', 'Please contact your administrator.') }} {{ t('core', 'Please contact your administrator.') }}
</small> </NcNoteCard>
</div> <NcNoteCard v-if="messages.length > 0">
<div v-for="(message, index) in messages" <div v-for="(message, index) in messages"
:key="index" :key="index">
class="warning"> {{ message }}<br>
{{ message }}<br> </div>
</div> </NcNoteCard>
<div v-if="internalException" <NcNoteCard v-if="internalException"
class="warning"> :class="t('core', 'An internal error occurred.')"
{{ t('core', 'An internal error occurred.') }}<br> type="warning">
<small>{{ t('core', 'Please try again or contact your administrator.') }} {{ t('core', 'Please try again or contact your administrator.') }}
</small> </NcNoteCard>
</div>
<div id="message" <div id="message"
class="hidden"> class="hidden">
<img class="float-spinner" <img class="float-spinner"
@ -52,65 +52,35 @@
<!-- the following div ensures that the spinner is always inside the #message div --> <!-- the following div ensures that the spinner is always inside the #message div -->
<div style="clear: both;" /> <div style="clear: both;" />
</div> </div>
<p class="grouptop" <NcTextField id="user"
:class="{shake: invalidPassword}"> ref="user"
<input id="user" :label="t('core', 'Account name or email')"
ref="user" :label-visible="true"
v-model="user" name="user"
type="text" :value.sync="user"
name="user" :class="{shake: invalidPassword}"
autocapitalize="none" autocapitalize="none"
autocorrect="off" :spellchecking="false"
:autocomplete="autoCompleteAllowed ? 'on' : 'off'" :autocomplete="autoCompleteAllowed ? 'username' : 'off'"
:placeholder="t('core', 'Username or email')" required
:aria-label="t('core', 'Username or email')" @change="updateUsername" />
required
@change="updateUsername">
<label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
</p>
<p class="groupbottom" <NcPasswordField id="password"
:class="{shake: invalidPassword}"> ref="password"
<input id="password" name="password"
ref="password" :label-visible="true"
:type="passwordInputType" :class="{shake: invalidPassword}"
class="password-with-toggle" :value="password"
name="password" :spellchecking="false"
autocorrect="off" autocapitalize="none"
autocapitalize="none" :autocomplete="autoCompleteAllowed ? 'current-password' : 'off'"
:autocomplete="autoCompleteAllowed ? 'current-password' : 'off'" :label="t('core', 'Password')"
:placeholder="t('core', 'Password')" :helper-text="errorLabel"
:aria-label="t('core', 'Password')" :error="isError"
required> required />
<label for="password"
class="infield">{{ t('core', 'Password') }}</label>
<NcButton class="toggle-password"
type="tertiary-no-background"
:aria-label="isPasswordHidden ? t('core', 'Show password') : t('core', 'Hide password')"
@click.stop.prevent="togglePassword">
<template #icon>
<Eye v-if="isPasswordHidden" :size="20" />
<EyeOff v-else :size="20" />
</template>
</NcButton>
</p>
<LoginButton :loading="loading" /> <LoginButton :loading="loading" />
<p v-if="invalidPassword"
class="warning wrongPasswordMsg">
{{ t('core', 'Wrong username or password.') }}
</p>
<p v-else-if="userDisabled"
class="warning userDisabledMsg">
{{ t('core', 'User disabled') }}
</p>
<p v-if="throttleDelay && throttleDelay > 5000"
class="warning throttledMsg">
{{ t('core', 'We have detected multiple invalid login attempts from your IP. Therefore your next login is throttled up to 30 seconds.') }}
</p>
<input v-if="redirectUrl" <input v-if="redirectUrl"
type="hidden" type="hidden"
name="redirect_url" name="redirect_url"
@ -136,20 +106,20 @@
import jstz from 'jstimezonedetect' import jstz from 'jstimezonedetect'
import { generateUrl, imagePath } from '@nextcloud/router' import { generateUrl, imagePath } from '@nextcloud/router'
import NcButton from '@nextcloud/vue/dist/Components/NcButton' import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
import Eye from 'vue-material-design-icons/Eye' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import EyeOff from 'vue-material-design-icons/EyeOff' import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import LoginButton from './LoginButton' import LoginButton from './LoginButton.vue'
export default { export default {
name: 'LoginForm', name: 'LoginForm',
components: { components: {
NcButton,
Eye,
EyeOff,
LoginButton, LoginButton,
NcPasswordField,
NcTextField,
NcNoteCard,
}, },
props: { props: {
@ -188,13 +158,28 @@ export default {
loading: false, loading: false,
timezone: jstz.determine().name(), timezone: jstz.determine().name(),
timezoneOffset: (-new Date().getTimezoneOffset() / 60), timezoneOffset: (-new Date().getTimezoneOffset() / 60),
user: this.username, user: '',
password: '', password: '',
passwordInputType: 'password',
} }
}, },
computed: { computed: {
isError() {
return this.invalidPassword || this.userDisabled
|| (this.throttleDelay && this.throttleDelay > 5000)
},
errorLabel() {
if (this.invalidPassword) {
return t('core', 'Wrong username or password.')
}
if (this.userDisabled) {
return t('core', 'User disabled')
}
if (this.throttleDelay && this.throttleDelay > 5000) {
return t('core', 'We have detected multiple invalid login attempts from your IP. Therefore your next login is throttled up to 30 seconds.')
}
return undefined
},
apacheAuthFailed() { apacheAuthFailed() {
return this.errors.indexOf('apacheAuthFailed') !== -1 return this.errors.indexOf('apacheAuthFailed') !== -1
}, },
@ -213,27 +198,18 @@ export default {
loginActionUrl() { loginActionUrl() {
return generateUrl('login') return generateUrl('login')
}, },
isPasswordHidden() {
return this.passwordInputType === 'password'
},
}, },
mounted() { mounted() {
if (this.username === '') { if (this.username === '') {
this.$refs.user.focus() this.$refs.user.focus()
} else { } else {
this.user = this.username
this.$refs.password.focus() this.$refs.password.focus()
} }
}, },
methods: { methods: {
togglePassword() {
if (this.passwordInputType === 'password') {
this.passwordInputType = 'text'
} else {
this.passwordInputType = 'password'
}
},
updateUsername() { updateUsername() {
this.$emit('update:username', this.user) this.$emit('update:username', this.user)
}, },
@ -246,10 +222,15 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.toggle-password { .login-form {
position: absolute; text-align: left;
top: 2px; font-size: 1rem;
right: 10px;
color: var(--color-text-lighter); &__fieldset {
width: 100%;
display: flex;
flex-direction: column;
gap: .5rem;
}
} }
</style> </style>

View File

@ -20,41 +20,37 @@
--> -->
<template> <template>
<form @submit.prevent="submit"> <form class="login-form" @submit.prevent="submit">
<fieldset> <fieldset class="login-form__fieldset">
<p> <NcTextField id="user"
<input id="user" :value.sync="user"
v-model="user" name="user"
type="text" autocapitalize="off"
name="user" :label="t('core', 'Account name or email')"
autocapitalize="off" :label-visible="true"
:placeholder="t('core', 'Username or email')" required
:aria-label="t('core', 'Username or email')" @change="updateUsername" />
required <!--<?php p($_['user_autofocus'] ? 'autofocus' : ''); ?>
@change="updateUsername">
<!--<?php p($_['user_autofocus'] ? 'autofocus' : ''); ?>
autocomplete="<?php p($_['login_form_autocomplete']); ?>" autocapitalize="none" autocorrect="off"--> autocomplete="<?php p($_['login_form_autocomplete']); ?>" autocapitalize="none" autocorrect="off"-->
<label for="user" class="infield">{{ t('core', 'Username or email') }}</label> <LoginButton :value="t('core', 'Reset password')" />
</p>
<div id="reset-password-wrapper"> <NcNoteCard v-if="message === 'send-success'"
<LoginButton :value="t('core', 'Reset password')" /> type="success">
</div>
<p v-if="message === 'send-success'"
class="notecard success">
{{ t('core', 'A password reset message has been sent to the email address of this account. If you do not receive it, check your spam/junk folders or ask your local administrator for help.') }} {{ t('core', 'A password reset message has been sent to the email address of this account. If you do not receive it, check your spam/junk folders or ask your local administrator for help.') }}
<br> <br>
{{ t('core', 'If it is not there ask your local administrator.') }} {{ t('core', 'If it is not there ask your local administrator.') }}
</p> </NcNoteCard>
<p v-else-if="message === 'send-error'" <NcNoteCard v-else-if="message === 'send-error'"
class="notecard error"> type="error">
{{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }} {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }}
</p> </NcNoteCard>
<p v-else-if="message === 'reset-error'" <NcNoteCard v-else-if="message === 'reset-error'"
class="notecard error"> type="error">
{{ t('core', 'Password cannot be changed. Please contact your administrator.') }} {{ t('core', 'Password cannot be changed. Please contact your administrator.') }}
</p> </NcNoteCard>
<a href="#" <a class="login-form__link"
href="#"
@click.prevent="$emit('abort')"> @click.prevent="$emit('abort')">
{{ t('core', 'Back to login') }} {{ t('core', 'Back to login') }}
</a> </a>
@ -66,11 +62,15 @@
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import LoginButton from './LoginButton.vue' import LoginButton from './LoginButton.vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
export default { export default {
name: 'ResetPassword', name: 'ResetPassword',
components: { components: {
LoginButton, LoginButton,
NcNoteCard,
NcTextField,
}, },
props: { props: {
username: { username: {
@ -130,8 +130,26 @@ export default {
} }
</script> </script>
<style scoped> <style lang="scss" scoped>
.update { .login-form {
width: auto; text-align: left;
font-size: 1rem;
&__fieldset {
width: 100%;
display: flex;
flex-direction: column;
gap: .5rem;
} }
&__link {
display: block;
font-weight: normal !important;
padding-bottom: 1rem;
cursor: pointer;
font-size: var(--default-font-size);
text-align: center;
padding: .5rem 1rem 1rem 1rem;
}
}
</style> </style>

View File

@ -20,7 +20,7 @@
--> -->
<template> <template>
<div id="login" class="guest-box"> <div class="guest-box login-box">
<div v-if="!hideLoginForm || directLogin"> <div v-if="!hideLoginForm || directLogin">
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''"> <div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''">
@ -34,16 +34,17 @@
@submit="loading = true" /> @submit="loading = true" />
<a v-if="canResetPassword && resetPasswordLink !== ''" <a v-if="canResetPassword && resetPasswordLink !== ''"
id="lost-password" id="lost-password"
class="login-box__link"
:href="resetPasswordLink"> :href="resetPasswordLink">
{{ t('core', 'Forgot password?') }} {{ t('core', 'Forgot password?') }}
</a> </a>
<a v-else-if="canResetPassword && !resetPassword" <a v-else-if="canResetPassword && !resetPassword"
id="lost-password" id="lost-password"
class="login-box__link"
:href="resetPasswordLink" :href="resetPasswordLink"
@click.prevent="resetPassword = true"> @click.prevent="resetPassword = true">
{{ t('core', 'Forgot password?') }} {{ t('core', 'Forgot password?') }}
</a> </a>
<br>
<template v-if="hasPasswordless"> <template v-if="hasPasswordless">
<div v-if="countAlternativeLogins" <div v-if="countAlternativeLogins"
class="alternative-logins"> class="alternative-logins">
@ -72,7 +73,7 @@
:is-localhost="isLocalhost" :is-localhost="isLocalhost"
:has-public-key-credential="hasPublicKeyCredential" :has-public-key-credential="hasPublicKeyCredential"
@submit="loading = true" /> @submit="loading = true" />
<a href="#" @click.prevent="passwordlessLogin = false"> <a href="#" class="login-box__link" @click.prevent="passwordlessLogin = false">
{{ t('core', 'Back') }} {{ t('core', 'Back') }}
</a> </a>
</div> </div>
@ -95,19 +96,16 @@
</div> </div>
<div v-else> <div v-else>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="warning"> <NcNoteCard type="warning" :title="t('core', 'Login form is disabled.')">
{{ t('core', 'Login form is disabled.') }}<br> {{ t('core', 'Please contact your administrator.') }}
<small> </NcNoteCard>
{{ t('core', 'Please contact your administrator.') }}
</small>
</div>
</transition> </transition>
</div> </div>
<div id="alternative-logins" class="alternative-logins"> <div id="alternative-logins" class="alternative-logins">
<NcButton v-for="(alternativeLogin, index) in alternativeLogins" <NcButton v-for="(alternativeLogin, index) in alternativeLogins"
:key="index" :key="index"
type="primary" type="secondary"
:wide="true" :wide="true"
:class="[alternativeLogin.class]" :class="[alternativeLogin.class]"
role="link" role="link"
@ -127,7 +125,8 @@ import LoginForm from '../components/login/LoginForm.vue'
import PasswordLessLoginForm from '../components/login/PasswordLessLoginForm.vue' import PasswordLessLoginForm from '../components/login/PasswordLessLoginForm.vue'
import ResetPassword from '../components/login/ResetPassword.vue' import ResetPassword from '../components/login/ResetPassword.vue'
import UpdatePassword from '../components/login/UpdatePassword.vue' import UpdatePassword from '../components/login/UpdatePassword.vue'
import NcButton from '@nextcloud/vue/dist/Components/NcButton' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
const query = queryString.parse(location.search) const query = queryString.parse(location.search)
if (query.clear === '1') { if (query.clear === '1') {
@ -149,6 +148,7 @@ export default {
ResetPassword, ResetPassword,
UpdatePassword, UpdatePassword,
NcButton, NcButton,
NcNoteCard,
}, },
data() { data() {
@ -192,28 +192,35 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.fade-enter-active, .fade-leave-active { body {
transition: opacity .3s; font-size: var(--default-font-size);
} }
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
#lost-password { .login-box {
padding: 4px; width: 300px;
margin: 8px;
border-radius: var(--border-radius);
}
.alternative-logins button { &__link {
margin-top: 12px; display: block;
margin-bottom: 12px; padding: 1rem;
&:first-child { font-size: var(--default-font-size);
margin-top: 0; text-align: center;
} font-weight: normal !important;
&:last-child {
margin-bottom: 0;
}
} }
}
.fade-enter-active, .fade-leave-active {
transition: opacity .3s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
.alternative-logins {
display: flex;
flex-direction: column;
gap: 0.75rem;
.button-vue {
box-sizing: border-box;
}
}
</style> </style>

BIN
dist/core-common.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
dist/core-login.js vendored

Binary file not shown.

BIN
dist/core-login.js.map vendored

Binary file not shown.

View File

@ -54,7 +54,7 @@ class LoginPageContext implements Context, ActorAwareInterface {
} }
public static function wrongPasswordMessage(): Locator { public static function wrongPasswordMessage(): Locator {
return Locator::forThe()->xpath("//*[@class = 'warning wrongPasswordMsg' and normalize-space() = 'Wrong username or password.']")-> return Locator::forThe()->xpath("//*[@class = 'input-field__helper-text-message input-field__helper-text-message--error' and normalize-space() = 'Wrong username or password.']")->
describedAs("Wrong password message in Login page"); describedAs("Wrong password message in Login page");
} }
@ -62,7 +62,7 @@ class LoginPageContext implements Context, ActorAwareInterface {
* @return Locator * @return Locator
*/ */
public static function userDisabledMessage() { public static function userDisabledMessage() {
return Locator::forThe()->xpath("//*[@class = 'warning userDisabledMsg' and normalize-space() = 'User disabled']")-> return Locator::forThe()->xpath("//*[@class = 'input-field__helper-text-message input-field__helper-text-message--error' and normalize-space() = 'User disabled']")->
describedAs('User disabled message on login page'); describedAs('User disabled message on login page');
} }