summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetr Vobornik <pvoborni@redhat.com>2013-11-13 16:02:48 +0100
committerPetr Vobornik <pvoborni@redhat.com>2014-04-15 12:41:53 +0200
commitefc9e66f4ddff7c0aaae5d460ab41f6026fbd32d (patch)
tree65f1150568db08c2fd26d1d745ae8dd8a8c1af09
parent0c8b04699b35ea48b897bcd3419a94dd90a07c5d (diff)
downloadfreeipa-efc9e66f4ddff7c0aaae5d460ab41f6026fbd32d.tar.gz
freeipa-efc9e66f4ddff7c0aaae5d460ab41f6026fbd32d.tar.xz
freeipa-efc9e66f4ddff7c0aaae5d460ab41f6026fbd32d.zip
webui: login screen widget
Reimplementation of unauthorized dialog into separate widget. It uses RCUE design. New features compared to unauthorized dialog: - reflects auth methods from `auth` module - validation summary - differentiates Kerberos auth failure with session expiration - Caps Lock warning - form based method doesn't allow password only submission https://fedorahosted.org/freeipa/ticket/4017 https://fedorahosted.org/freeipa/ticket/3903 Reviewed-By: Adam Misnyovszki <amisnyov@redhat.com>
-rw-r--r--freeipa.spec.in1
-rw-r--r--install/ui/images/Makefile.am3
-rw-r--r--install/ui/images/login-screen-background.jpgbin0 -> 69168 bytes
-rw-r--r--install/ui/images/login-screen-logo.pngbin0 -> 5802 bytes
-rw-r--r--install/ui/images/product-name.pngbin0 -> 13622 bytes
-rw-r--r--install/ui/less/login.less167
-rw-r--r--install/ui/less/rcue.less1
-rw-r--r--install/ui/src/freeipa/dialog.js4
-rw-r--r--install/ui/src/freeipa/widgets/LoginScreen.js549
9 files changed, 723 insertions, 2 deletions
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 8ae5e2e08..52817cd4d 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -707,6 +707,7 @@ fi
%{_usr}/share/ipa/ui/js/freeipa/app.js
%dir %{_usr}/share/ipa/ui/js/plugins
%dir %{_usr}/share/ipa/ui/images
+%{_usr}/share/ipa/ui/images/*.jpg
%{_usr}/share/ipa/ui/images/*.png
%{_usr}/share/ipa/ui/images/*.gif
%dir %{_usr}/share/ipa/wsgi
diff --git a/install/ui/images/Makefile.am b/install/ui/images/Makefile.am
index 0a92fdf9a..e63af6912 100644
--- a/install/ui/images/Makefile.am
+++ b/install/ui/images/Makefile.am
@@ -17,12 +17,15 @@ app_DATA = \
ie-icon.png \
ipa-banner.png \
ipa-logo.png \
+ login-screen-background.jpg \
+ login-screen-logo.png \
mainnav-tab-off.png \
mainnav-tab-on.png \
modal-background.png \
nav-arrow.png \
outer-background.png \
panel-background.png \
+ product-name.png \
remove-icon.png \
reset-icon.png \
search-background.png \
diff --git a/install/ui/images/login-screen-background.jpg b/install/ui/images/login-screen-background.jpg
new file mode 100644
index 000000000..b2ffc1ac5
--- /dev/null
+++ b/install/ui/images/login-screen-background.jpg
Binary files differ
diff --git a/install/ui/images/login-screen-logo.png b/install/ui/images/login-screen-logo.png
new file mode 100644
index 000000000..38f948efd
--- /dev/null
+++ b/install/ui/images/login-screen-logo.png
Binary files differ
diff --git a/install/ui/images/product-name.png b/install/ui/images/product-name.png
new file mode 100644
index 000000000..79551f3ac
--- /dev/null
+++ b/install/ui/images/product-name.png
Binary files differ
diff --git a/install/ui/less/login.less b/install/ui/less/login.less
new file mode 100644
index 000000000..de9d651b0
--- /dev/null
+++ b/install/ui/less/login.less
@@ -0,0 +1,167 @@
+/**
+ * Authors:
+ * UXD team
+ * Petr Vobornik <pvoborni@redhat.com>
+ *
+ * Copyright (C) 2013 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.facet[name=login] {
+ height: 100%;
+}
+
+.rcue-login-screen {
+ width: 100%;
+
+ height: 100%;
+ background-color: #1D2226;
+ background-image: url("../images/login-screen-background.jpg");
+ background-position: top left;
+ background-size: auto;
+ background-repeat: no-repeat;
+
+ img.logo {
+ // position: absolute;
+ // top: 50px;
+ // right: 64px;
+ display: none;
+ }
+
+ .login-form {
+ position: absolute;
+ bottom: 15%;
+ left: 0;
+ right: 0;
+ border-top: 1px rgba(255, 255, 255, 0.05) solid;
+ border-bottom: 1px rgba(255, 255, 255, 0.05) solid;
+ background-color: rgba(0, 0, 0, 0.3);
+ color: #fff;
+
+ aside {
+ padding: 30px;
+ p {
+ margin-bottom: 8px;
+ }
+ }
+
+ fieldset {
+ float: left;
+ margin: 30px 30px 30px 0;
+ padding: 10px;
+ padding-right: 34px;
+ width: 426px;
+ border-right: 1px rgba(255, 255, 255, 0.15) solid;
+
+ legend {
+ position: absolute;
+ top: -100px;
+ padding-left: 100px;
+ width: auto;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ font-weight: 200;
+ font-size: 22px;
+ border-bottom: none;
+
+ strong {
+ margin-right: 4px;
+ font-weight: 700;
+ }
+ }
+
+ input[type="text"],
+ input[type="password"] {
+ width: 282px;
+ color: black;
+ }
+
+ .buttons {
+ float: right;
+ }
+
+ label, button {
+ font-size: 13px;
+ }
+
+ small {
+ line-height: 22px;
+ margin-left: 87px;
+ margin-top: 10px;
+ display: block;
+ float: left;
+ }
+ }
+ }
+}
+
+@media (min-width: 1280px) {
+ .rcue-login-screen {
+ background-size: 100% auto;
+ }
+}
+
+
+.login_small() {
+ .rcue-login-screen .login-form {
+ top: 100px;
+ bottom: inherit;
+ }
+}
+
+@media (max-width: 480px) {
+
+ .login_small;
+
+ .rcue-login-screen .login-form {
+ fieldset {
+ float: none;
+ width: auto;
+ .buttons {
+ float: none;
+ }
+ }
+ aside {
+ padding: 10px;
+ }
+ }
+}
+
+@media (max-height: 700px) {
+ .login_small;
+}
+
+@media (max-height: 480px) {
+ .login_small;
+ .rcue-login-screen .login-form legend {
+ position: relative;
+ }
+}
+
+@media (max-width: 740px) {
+ .rcue-login-screen .login-form {
+ fieldset {
+ border-right: none;
+ legend {
+ padding-left: 0;
+ width: 100%;
+ }
+ }
+ aside {
+ clear: both;
+ }
+ }
+}
diff --git a/install/ui/less/rcue.less b/install/ui/less/rcue.less
index 60e41d95f..2d41aa283 100644
--- a/install/ui/less/rcue.less
+++ b/install/ui/less/rcue.less
@@ -11,3 +11,4 @@
@import "forms-override";
@import "widgets";
@import "plugins/otp";
+@import "login.less";
diff --git a/install/ui/src/freeipa/dialog.js b/install/ui/src/freeipa/dialog.js
index 39c48efd3..4c6c37f88 100644
--- a/install/ui/src/freeipa/dialog.js
+++ b/install/ui/src/freeipa/dialog.js
@@ -1291,7 +1291,7 @@ IPA.confirm_mixin = function() {
var self = this;
this._on_key_up_listener = function(e) { self.on_key_up(e); };
this._on_key_down_listener = function(e) { self._on_key_down(e); };
- var dialog_container = this.dom_node;
+ var dialog_container = $(this.dom_node);
dialog_container.bind('keyup', this._on_key_up_listener);
dialog_container.bind('keydown', this._on_key_down_listener);
},
@@ -1300,7 +1300,7 @@ IPA.confirm_mixin = function() {
* Removal of registered event handlers
*/
remove_listeners: function() {
- var dialog_container = this.dom_node;
+ var dialog_container = $(this.dom_node);
dialog_container.unbind('keyup', this._on_key_up_listener);
dialog_container.unbind('keydown', this._on_key_down_listener);
},
diff --git a/install/ui/src/freeipa/widgets/LoginScreen.js b/install/ui/src/freeipa/widgets/LoginScreen.js
new file mode 100644
index 000000000..9d149af4c
--- /dev/null
+++ b/install/ui/src/freeipa/widgets/LoginScreen.js
@@ -0,0 +1,549 @@
+/* Authors:
+ * Petr Vobornik <pvoborni@redhat.com>
+ *
+ * Copyright (C) 2013 Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+define(['dojo/_base/declare',
+ 'dojo/_base/lang',
+ 'dojo/dom-construct',
+ 'dojo/dom-style',
+ 'dojo/query',
+ 'dojo/on',
+ 'dojo/Evented',
+ 'dojo/Stateful',
+ '../ipa',
+ '../auth',
+ '../reg',
+ '../FieldBinder',
+ '../FormMixin',
+ '../text',
+ '../util',
+ './ContainerMixin'
+ ],
+ function(declare, lang, construct, dom_style, query, on,
+ Evented, Stateful, IPA, auth, reg, FieldBinder, FormMixin, text,
+ util, ContainerMixin) {
+
+ var ConfirmMixin = declare(null, IPA.confirm_mixin().mixin);
+
+ /**
+ * Widget with login form.
+ *
+ * Supported operations:
+ *
+ * - login with password, kerberos
+ * - password change
+ *
+ * @class widgets.LoginScreen
+ */
+ var LoginScreen = declare([Stateful, Evented, FormMixin, ContainerMixin, ConfirmMixin], {
+
+ id: 'rcue-login-screen',
+
+ 'class': 'rcue-login-screen',
+
+ logo_src: 'images/login-screen-logo.png',
+
+ product_name_src: 'images/product-name.png',
+
+ expired_msg: "Your session has expired. Please re-login.",
+
+ form_auth_msg: "To login with username and password, enter them in the fields below, then click Login.",
+
+ kerberos_msg: " To login with Kerberos, please make sure you" +
+ " have valid tickets (obtainable via kinit) and " +
+ "<a href='http://${host}/ipa/config/unauthorized.html'>configured</a>" +
+ " the browser correctly, then click Login. ",
+
+ form_auth_failed: "The password or username you entered is incorrect. ",
+
+ krb_auth_failed: "Authentication with Kerberos failed",
+
+ password_expired: "Your password has expired. Please enter a new password.",
+
+ denied: "Sorry you are not allowed to access this service.",
+
+ caps_warning_msg: "Warning: CAPS LOCK key is on",
+
+ /**
+ * Details builder
+ * @property {IPA.details_builder}
+ * @protected
+ */
+ details_builder: null,
+
+ /**
+ * Aside text
+ * @property {string}
+ */
+ aside: "",
+
+ //nodes:
+ dom_node: null,
+ container_node: null,
+ content_node: null,
+ aside_node: null,
+ login_btn_node: null,
+ reset_btn_node: null,
+ buttons_node: null,
+
+ /**
+ * View this form is in.
+ *
+ * Possible views: ['login', 'reset']
+ * @property {string}
+ */
+ view: 'login',
+
+ /**
+ * Indicates that CAPS LOCK warning is on. Null indicates that we don't
+ * know the state.
+ * @property {boolean|null}
+ */
+ caps_warning: null,
+
+
+ _asideSetter: function(text) {
+ this.aside = text;
+ if (this.aside_node) {
+ this.aside_node.innerHTML = this.aside;
+ }
+ },
+
+ _viewSetter: function(view) {
+ this.view = view;
+ this.refresh();
+ },
+
+ render: function() {
+
+ this.dom_node = construct.create('div', {
+ id: this.id,
+ 'class': this['class']
+ });
+
+ if (this.container_node) {
+ construct.place(this.dom_node, this.container_node);
+ }
+
+ this.render_content();
+ this.register_listeners();
+
+ return this.dom_node;
+ },
+
+ render_content: function() {
+
+ construct.empty(this.dom_node);
+
+ construct.create('img', {
+ 'class': 'logo',
+ src: this.logo_src
+ }, this.dom_node);
+
+ this.render_form(this.dom_node);
+ },
+
+ render_form: function(container) {
+
+ var form_cont = construct.create('div', {
+ 'class': 'login-form'
+ }, container);
+
+ var fieldset = construct.create('fieldset', {}, form_cont);
+
+ var legend = construct.create('legend', {}, fieldset);
+ construct.create('img', {
+ src: this.product_name_src
+ }, legend);
+
+ var layout = IPA.fluid_layout({});
+ var form = layout.create(this.get_widgets());
+ construct.place(form[0], fieldset);
+ this.register_caps_check();
+
+ this.buttons_node = construct.create('div', {
+ 'class': 'buttons'
+ }, fieldset);
+
+ this.login_btn_node = IPA.button({
+ label: text.get('@i18n:login.login', "Login"),
+ 'class': 'btn-primary',
+ click: lang.hitch(this, this.on_confirm)
+ })[0];
+ construct.place(this.login_btn_node, this.buttons_node);
+
+ this.reset_btn_node = IPA.button({
+ label: text.get('@i18n:buttons.reset_password_and_login', "Reset Password and Login"),
+ 'class': 'btn-primary',
+ click: lang.hitch(this, this.on_confirm)
+ })[0];
+
+ this.render_aside(form_cont);
+ },
+
+ render_aside: function(container) {
+
+ this.aside_node = construct.create('aside', {
+ innerHTML: this.aside
+ }, container);
+ },
+
+ create_fields: function() {
+
+ var validation_summary = {
+ $type: 'validation_summary',
+ name: 'validation',
+ visible: false
+ };
+
+ var val_w = this.add_widget(validation_summary);
+ var fields = LoginScreen.field_specs;
+ for (var i=0, l=fields.length; i<l; i++) {
+ var f = this.add_field(fields[i]);
+ var w = this.add_widget(fields[i]);
+ new FieldBinder(f, w).bind(true);
+ this.bind_validation(val_w, f);
+ }
+
+ var u_f = this.get_field('username');
+ var p_f = this.get_field('password');
+
+ u_f.on('value-change', lang.hitch(this, this.on_form_change));
+ p_f.on('value-change', lang.hitch(this, this.on_form_change));
+ this.on_form_change();
+ },
+
+ on_form_change: function(event) {
+
+ var u_f = this.get_field('username');
+ var p_f = this.get_field('password');
+ var required = !util.is_empty(u_f.get_value()) ||
+ !util.is_empty(p_f.get_value()) || !this.kerberos_enabled();
+ u_f.set_required(required);
+ p_f.set_required(required);
+ },
+
+ register_caps_check: function() {
+
+ // this is not a nice solution. It breaks widget encapsulation over
+ // input field, but we need to listen to keydown events which are
+ // not exposed
+ var nodes = query("input[type=text],input[type=password]", this.dom_node);
+ nodes.on('keypress', lang.hitch(this, this.check_caps_lock));
+ nodes.on('keydown', lang.hitch(this, this.check_caps_lock_press));
+ },
+
+ /**
+ * Check if Caps Lock key is on.
+ *
+ * Works fine only with keypress events. Doesn't work with keydown or
+ * up since keyCode is always uppercase there.
+ * @param {Event} e Key press event
+ * @protected
+ */
+ check_caps_lock: function(e) {
+
+ var s = String.fromCharCode(e.keyCode || e.which);
+
+ if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey)||
+ (s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) {
+ this.displey_caps_warning(true);
+ } else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey)||
+ (s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) {
+ this.displey_caps_warning(false);
+ }
+ // in other cases it's most likely non alpha numeric character
+ },
+
+ /**
+ * Check if Caps Lock was press
+ * If current caps lock state is known, i.e. by `check_caps_lock` method,
+ * toogle it.
+ * @param {Event} e Key down or key up event
+ * @protected
+ */
+ check_caps_lock_press: function(e) {
+
+ if (this.caps_warning !== null && e.keyCode == 20) {
+ this.displey_caps_warning(!this.caps_warning);
+ }
+ },
+
+ /**
+ * Show or hide CAPS lock warning
+ * @param {boolean} display
+ * @protected
+ */
+ displey_caps_warning: function(display) {
+
+ this.caps_warning = display;
+ var val_summary = this.get_widget('validation');
+ if (display) {
+ val_summary.add_warning('caps', this.caps_warning_msg);
+ } else {
+ val_summary.remove_warning('caps');
+ }
+ },
+
+ bind_validation: function(summary, field) {
+
+ on(field, 'valid-change', function(e) {
+ if (e.valid) {
+ summary.remove_error(field.name);
+ } else {
+ summary.add_error(field.name, field.label + ': ' + e.result.message);
+ }
+ });
+ },
+
+ on_confirm: function() {
+ if (this.view == 'login') {
+ this.login();
+ } else {
+ this.login_and_reset();
+ }
+ },
+
+ login: function() {
+
+ var val_summary = this.get_widget('validation');
+ val_summary.remove_error('login');
+
+ if (!this.validate()) return;
+
+ var login = this.get_field('username').get_value()[0];
+ if (util.is_empty(login) && this.kerberos_enabled()) {
+ this.login_with_kerberos();
+ } else {
+ this.login_with_password();
+ }
+ },
+
+ login_with_kerberos: function() {
+
+ var status = IPA.get_credentials();
+
+ if (status === 200) {
+ this.emit('logged_in');
+ } else {
+ var val_summary = this.get_widget('validation');
+ val_summary.add_error('login', this.krb_auth_failed);
+ }
+ },
+
+ login_with_password: function() {
+
+ if(!this.password_enabled()) return;
+
+ var val_summary = this.get_widget('validation');
+ var login = this.get_field('username').get_value()[0];
+ var password_f = this.get_field('password');
+ var password = password_f.get_value()[0];
+
+ var result = IPA.login_password(login, password);
+
+ if (result === 'success') {
+ this.emit('logged_in');
+ password_f.set_value('');
+ } else if (result === 'password-expired') {
+ this.set('view', 'reset');
+ val_summary.add_error('login', this.password_expired);
+ } else {
+ val_summary.add_error('login', this.form_auth_failed);
+ password_f.set_value('');
+ }
+ },
+
+ login_and_reset: function() {
+
+ var val_summary = this.get_widget('validation');
+ val_summary.remove_error('login');
+
+ if (!this.validate()) return;
+
+ var psw_f = this.get_field('password');
+ var new_f = this.get_field('new_password');
+ var ver_f = this.get_field('verify_password');
+ var username_f = this.get_field('username');
+
+ var result = IPA.reset_password(
+ username_f.get_value()[0],
+ psw_f.get_value()[0],
+ new_f.get_value()[0]);
+
+ if (result.status === 'ok') {
+ psw_f.set_value(new_f.get_value());
+ this.login();
+ this.set('view', 'login');
+ } else {
+ val_summary.add_error('login', result.message);
+ }
+
+ new_f.set_value('');
+ ver_f.set_value('');
+ },
+
+ refresh: function() {
+ if (this.buttons_node) {
+ this.buttons_node.innerHTML = "";
+ }
+ if (this.view === 'reset') {
+ this.show_reset_view();
+ } else {
+ this.show_login_view();
+ }
+ },
+
+ show_login_view: function() {
+ this.set_login_aside_text();
+ if (this.buttons_node) {
+ construct.place(this.login_btn_node, this.buttons_node);
+ }
+ if (this.password_enabled()) {
+ this.use_fields(['username', 'password']);
+ this.get_widget('username').focus_input();
+ } else {
+ this.use_fields([]);
+ this.login_btn_node.focus();
+ }
+ },
+
+ show_reset_view: function() {
+
+ this.set_reset_aside_text();
+ if (this.buttons_node) {
+ construct.place(this.reset_btn_node, this.buttons_node);
+ }
+ this.use_fields(['username_r', 'new_password', 'verify_password']);
+
+ var val_summary = this.get_widget('validation');
+
+ var u_f = this.fields.get('username');
+ var u_r_f = this.fields.get('username_r');
+ u_r_f.set_value(u_f.get_value());
+ this.get_widget('new_password').focus_input();
+ },
+
+ use_fields: function(names) {
+
+ var fields = this.get_fields();
+ for (var i=0, l=fields.length; i<l; i++) {
+ var f = fields[i];
+ var w = this.get_widget(f.name);
+ var enable = names.indexOf(f.name) >-1;
+ f.set_enabled(enable);
+ w.set_visible(enable);
+ }
+ },
+
+ set_login_aside_text: function() {
+ var aside = "";
+ if (auth.current.expired) {
+ aside += "<p>"+this.expired_msg;+"<p/>";
+ }
+ if (this.password_enabled()) {
+ aside += "<p>"+this.form_auth_msg;+"<p/>";
+ }
+ if (this.kerberos_enabled()) {
+ aside += "<p>"+this.kerberos_msg;+"<p/>";
+ }
+ this.set('aside', aside);
+ },
+
+ set_reset_aside_text: function() {
+ this.set('aside', '');
+ },
+
+ kerberos_enabled: function() {
+ return auth.current.auth_methods.indexOf('kerberos') > -1;
+ },
+
+ password_enabled: function() {
+ return auth.current.auth_methods.indexOf('password') > -1;
+ },
+
+ postscript: function(args) {
+ this.create_fields();
+ },
+
+ constructor: function(spec) {
+ spec = spec || {};
+ declare.safeMixin(this, spec);
+
+ this.expired_msg = text.get(spec.expired_msg || '@i18n:ajax.401.message',
+ this.expired_msg);
+
+ this.form_auth_msg = text.get(spec.form_auth_msg || '@i18n:login.form_auth',
+ this.form_auth_msg);
+
+ this.kerberos_msg = text.get(spec.kerberos_msg || '@i18n:login.krb_auth_msg',
+ this.kerberos_msg);
+
+ this.kerberos_msg = this.kerberos_msg.replace('${host}', window.location.hostname);
+
+ this.krb_auth_failed = text.get(spec.krb_auth_failed, this.krb_auth_failed);
+ }
+ });
+
+ LoginScreen.field_specs = [
+ {
+ $type: 'text',
+ name: 'username',
+ label: text.get('@i18n:login.username', "Username"),
+ show_errors: false,
+ undo: false
+ },
+ {
+ $type: 'password',
+ name: 'password',
+ label: text.get('@i18n:login.password', "Password"),
+ show_errors: false,
+ undo: false
+ },
+ {
+ name: 'username_r',
+ read_only: true,
+ label: text.get('@i18n:login.username', "Username"),
+ show_errors: false,
+ undo: false
+ },
+ {
+ name: 'new_password',
+ $type: 'password',
+ required: true,
+ label: text.get('@i18n:password.new_password)', "New Password"),
+ show_errors: false,
+ undo: false
+ },
+ {
+ name: 'verify_password',
+ $type: 'password',
+ required: true,
+ label: text.get('@i18n:password.verify_password', "Verify Password"),
+ validators: [{
+ $type: 'same_password',
+ other_field: 'new_password'
+ }],
+ show_errors: false,
+ undo: false
+ }
+ ];
+
+ return LoginScreen;
+}); \ No newline at end of file