From 29e31ffdb1d9b3f59b56559fced437c26a04d33f Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 12 Jan 2026 18:22:53 +0100 Subject: [PATCH] refactor(user_ldap): migrate jQuery UI of password renewal to Vue Signed-off-by: Ferdinand Thiessen --- apps/user_ldap/css/renewPassword.css | 148 ---- .../ui-multiselect/jquery.multiselect.css | 27 - apps/user_ldap/js/renewPassword.js | 35 - .../ui-multiselect/src/jquery.multiselect.js | 707 ------------------ apps/user_ldap/lib/AppInfo/Application.php | 2 + .../Controller/RenewPasswordController.php | 37 +- apps/user_ldap/src/LDAPSettingsApp.vue | 11 - apps/user_ldap/src/renewPassword.ts | 10 + apps/user_ldap/src/settings-admin.ts | 3 +- .../{Settings.vue => LDAPSettingsApp.vue} | 5 +- apps/user_ldap/src/views/RenewPassword.vue | 97 +++ apps/user_ldap/templates/renewpassword.php | 61 +- build/frontend/vite.config.ts | 1 + 13 files changed, 135 insertions(+), 1009 deletions(-) delete mode 100644 apps/user_ldap/css/renewPassword.css delete mode 100644 apps/user_ldap/css/vendor/ui-multiselect/jquery.multiselect.css delete mode 100644 apps/user_ldap/js/renewPassword.js delete mode 100644 apps/user_ldap/js/vendor/ui-multiselect/src/jquery.multiselect.js delete mode 100644 apps/user_ldap/src/LDAPSettingsApp.vue create mode 100644 apps/user_ldap/src/renewPassword.ts rename apps/user_ldap/src/views/{Settings.vue => LDAPSettingsApp.vue} (98%) create mode 100644 apps/user_ldap/src/views/RenewPassword.vue diff --git a/apps/user_ldap/css/renewPassword.css b/apps/user_ldap/css/renewPassword.css deleted file mode 100644 index 8acd97254fa..00000000000 --- a/apps/user_ldap/css/renewPassword.css +++ /dev/null @@ -1,148 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -#personal-show + label { - inset-inline-start: 230px !important; - margin-top: 8px !important; - box-sizing: border-box; -} - -#renewpassword .strengthify-wrapper { - inset-inline-start: 10px; - margin-top: 65px; - position: absolute; - width: 219px; -} - -#cancel-container p.info { - margin-top: 10px; - text-align: center; -} - -#renewpassword .title { - background-color: transparent; -} - -.tooltip { - position:absolute; - display:block; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; - font-style:normal; - font-weight:400; - letter-spacing:normal; - line-break:auto; - line-height:1.6; - text-align:start; - text-decoration:none; - text-shadow:none; - text-transform:none; - white-space:normal; - word-break:normal; - word-spacing:normal; - word-wrap:normal; - font-size:12px; - opacity:0; - z-index:100000; - filter:drop-shadow(0 1px 10px rgba(77, 77, 77, 0.75)); -} - -.tooltip.in { - opacity:1 -} - -.tooltip.top { - margin-top:-3px; - padding:10px 0 -} - -.tooltip.bottom { - margin-top:3px; - padding:10px 0 -} - -.tooltip.right { - margin-inline-start:3px; - padding:0 10px -} - -.tooltip.right .tooltip-arrow { - top:50%; - inset-inline-start:0; - margin-top:-10px; - border-width:10px 10px 10px 0; - border-inline-end-color:#fff -} - -.tooltip.left { - margin-inline-start:-3px; - padding:0 5px -} - -.tooltip.left .tooltip-arrow { - top:50%; - inset-inline-end:0; - margin-top:-10px; - border-width:10px 0 10px 10px; - border-inline-start-color:#fff -} - -.tooltip.top .tooltip-arrow,.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow { - bottom:0; - border-width:10px 10px 0; - border-top-color:#fff -} - -.tooltip.top .tooltip-arrow { - inset-inline-start:50%; - margin-inline-start:-10px -} - -.tooltip.top-left .tooltip-arrow { - inset-inline-end:10px; - margin-bottom:-10px -} - -.tooltip.top-right .tooltip-arrow { - inset-inline-start:10px; - margin-bottom:-10px -} - -.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow { - top:0; - border-width:0 10px 10px; - border-bottom-color:#fff -} - -.tooltip.bottom .tooltip-arrow { - inset-inline-start:50%; - margin-inline-start:-10px -} - -.tooltip.bottom-left .tooltip-arrow { - inset-inline-end:10px; - margin-top:-10px -} - -.tooltip.bottom-right .tooltip-arrow { - inset-inline-start:10px; - margin-top:-10px -} - -.tooltip-inner { - max-width:350px; - padding:5px 8px !important; - background-color:#fff; - color:#000 !important; - text-align:center !important; - font-weight:normal !important; - border-radius:3px -} - -.tooltip-arrow { - position:absolute; - width:0; - height:0; - border-color:transparent; - border-style:solid -} diff --git a/apps/user_ldap/css/vendor/ui-multiselect/jquery.multiselect.css b/apps/user_ldap/css/vendor/ui-multiselect/jquery.multiselect.css deleted file mode 100644 index 533eb63762f..00000000000 --- a/apps/user_ldap/css/vendor/ui-multiselect/jquery.multiselect.css +++ /dev/null @@ -1,27 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2012 Eric Hynds - * SPDX-License-Identifier: MIT - */ -.ui-multiselect { padding:2px 0 2px 4px; text-align:left; } -.ui-multiselect span.ui-icon { float:right; } -.ui-multiselect-single .ui-multiselect-checkboxes input { position:absolute !important; top: auto !important; left:-9999px; } -.ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important; } - -.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px; } -.ui-multiselect-header ul { font-size:14px; } -.ui-multiselect-header ul li { float:left; padding:0 10px 0 0; } -.ui-multiselect-header a { text-decoration:none; } -.ui-multiselect-header a:hover { text-decoration:underline; } -.ui-multiselect-header span.ui-icon { float:left;} -.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0; } - -.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000; text-align: left; } -.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:scroll; } -.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px; } -.ui-multiselect-checkboxes label input { position:relative; top:1px; } -.ui-multiselect-checkboxes li { clear:both; font-size:14px; padding-right:3px; } -.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid; } -.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none; } - -/* remove label borders in IE6 because IE6 does not support transparency */ -* html .ui-multiselect-checkboxes label { border:none; } diff --git a/apps/user_ldap/js/renewPassword.js b/apps/user_ldap/js/renewPassword.js deleted file mode 100644 index 2db9b8e7cd2..00000000000 --- a/apps/user_ldap/js/renewPassword.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -OCA = OCA || {} -OCA.LDAP = _.extend(OC.LDAP || {}, { - onRenewPassword: function() { - $('#submit') - .removeClass('icon-confirm-white') - .addClass('icon-loading-small') - .attr('value', t('core', 'Renewing …')) - return true - }, -}) - -window.addEventListener('DOMContentLoaded', function() { - $('form[name=renewpassword]').submit(OCA.LDAP.onRenewPassword) - - if ($('#newPassword').length) { - $('#newPassword').showPassword().keyup() - } - $('#newPassword').strengthify({ - zxcvbn: OC.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, - $addAfter: $('input[name="newPassword-clone"]'), - }) -}) diff --git a/apps/user_ldap/js/vendor/ui-multiselect/src/jquery.multiselect.js b/apps/user_ldap/js/vendor/ui-multiselect/src/jquery.multiselect.js deleted file mode 100644 index 844e257ca29..00000000000 --- a/apps/user_ldap/js/vendor/ui-multiselect/src/jquery.multiselect.js +++ /dev/null @@ -1,707 +0,0 @@ -/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, boss:true, undef:true, curly:true, browser:true, jquery:true */ -/* - * jQuery MultiSelect UI Widget 1.13 - * Copyright (c) 2012 Eric Hynds - * - * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/ - * - * Depends: - * - jQuery 1.4.2+ - * - jQuery UI 1.8 widget factory - * - * Optional: - * - jQuery UI effects - * - jQuery UI position utility - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * SPDX-FileCopyrightText: 2012 Eric Hynds - * SPDX-License-Identifier: MIT - */ -(function($, undefined) { - let multiselectID = 0 - - $.widget('ech.multiselect', { - - // default options - options: { - header: true, - height: 175, - minWidth: 225, - classes: '', - checkAllText: 'Check all', - uncheckAllText: 'Uncheck all', - noneSelectedText: 'Select options', - selectedText: '# selected', - selectedList: 0, - show: null, - hide: null, - autoOpen: false, - multiple: true, - position: {}, - }, - - _create: function() { - const el = this.element.hide(), - o = this.options - - this.speed = $.fx.speeds._default // default speed for effects - this._isOpen = false // assume no - - const - button = (this.button = $('')) - .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all') - .addClass(o.classes) - .attr({ title: el.attr('title'), 'aria-haspopup': true, tabIndex: el.attr('tabIndex') }) - .insertAfter(el), - - buttonlabel = (this.buttonlabel = $('')) - .html(o.noneSelectedText) - .appendTo(button), - - menu = (this.menu = $('
')) - .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all') - .addClass(o.classes) - .appendTo(document.body), - - header = (this.header = $('
')) - .addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix') - .appendTo(menu), - - headerLinkContainer = (this.headerLinkContainer = $('
    ')) - .addClass('ui-helper-reset') - .html(function() { - if (o.header === true) { - return '
  • ' + o.checkAllText + '
  • ' + o.uncheckAllText + '
  • ' - } else if (typeof o.header === 'string') { - return '
  • ' + o.header + '
  • ' - } else { - return '' - } - }) - .append('
  • ') - .appendTo(header), - - checkboxContainer = (this.checkboxContainer = $('
      ')) - .addClass('ui-multiselect-checkboxes ui-helper-reset') - .appendTo(menu) - - // perform event bindings - this._bindEvents() - - // build menu - this.refresh(true) - - // some addl. logic for single selects - if (!o.multiple) { - menu.addClass('ui-multiselect-single') - } - }, - - _init: function() { - if (this.options.header === false) { - this.header.hide() - } - if (!this.options.multiple) { - this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide() - } - if (this.options.autoOpen) { - this.open() - } - if (this.element.is(':disabled')) { - this.disable() - } - }, - - refresh: function(init) { - let el = this.element, - o = this.options, - menu = this.menu, - checkboxContainer = this.checkboxContainer, - optgroups = [], - html = '', - id = el.attr('id') || multiselectID++ // unique ID for the label & option tags - - // build items - el.find('option').each(function(i) { - let $this = $(this), - parent = this.parentNode, - title = this.innerHTML, - description = this.title, - value = this.value, - inputID = 'ui-multiselect-' + (this.id || id + '-option-' + i), - isDisabled = this.disabled, - isSelected = this.selected, - labelClasses = ['ui-corner-all'], - liClasses = (isDisabled ? 'ui-multiselect-disabled ' : ' ') + this.className, - optLabel - - // is this an optgroup? - if (parent.tagName === 'OPTGROUP') { - optLabel = parent.getAttribute('label') - - // has this optgroup been added already? - if ($.inArray(optLabel, optgroups) === -1) { - html += '
    • ' + optLabel + '
    • ' - optgroups.push(optLabel) - } - } - - if (isDisabled) { - labelClasses.push('ui-state-disabled') - } - - // browsers automatically select the first option - // by default with single selects - if (isSelected && !o.multiple) { - labelClasses.push('ui-state-active') - } - - html += '
    • ' - - // create the label - html += '
    • ' - }) - - // insert into the DOM - checkboxContainer.html(html) - - // cache some moar useful elements - this.labels = menu.find('label') - this.inputs = this.labels.children('input') - - // set widths - this._setButtonWidth() - this._setMenuWidth() - - // remember default value - this.button[0].defaultValue = this.update() - - // broadcast refresh event; useful for widgets - if (!init) { - this._trigger('refresh') - } - }, - - // updates the button text. call refresh() to rebuild - update: function() { - let o = this.options, - $inputs = this.inputs, - $checked = $inputs.filter(':checked'), - numChecked = $checked.length, - value - - if (numChecked === 0) { - value = o.noneSelectedText - } else { - if ($.isFunction(o.selectedText)) { - value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get()) - } else if (/\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList) { - value = $checked.map(function() { return $(this).next().html() }).get().join(', ') - } else { - value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length) - } - } - - this.buttonlabel.html(value) - return value - }, - - // binds events - _bindEvents: function() { - const self = this, button = this.button - - /** - * - */ - function clickHandler() { - self[self._isOpen ? 'close' : 'open']() - return false - } - - // webkit doesn't like it when you click on the span :( - button - .find('span') - .bind('click.multiselect', clickHandler) - - // button events - button.bind({ - click: clickHandler, - keypress: function(e) { - switch (e.which) { - case 27: // esc - case 38: // up - case 37: // left - self.close() - break - case 39: // right - case 40: // down - self.open() - break - } - }, - mouseenter: function() { - if (!button.hasClass('ui-state-disabled')) { - $(this).addClass('ui-state-hover') - } - }, - mouseleave: function() { - $(this).removeClass('ui-state-hover') - }, - focus: function() { - if (!button.hasClass('ui-state-disabled')) { - $(this).addClass('ui-state-focus') - } - }, - blur: function() { - $(this).removeClass('ui-state-focus') - }, - }) - - // header links - this.header - .delegate('a', 'click.multiselect', function(e) { - // close link - if ($(this).hasClass('ui-multiselect-close')) { - self.close() - - // check all / uncheck all - } else { - self[$(this).hasClass('ui-multiselect-all') ? 'checkAll' : 'uncheckAll']() - } - - e.preventDefault() - }) - - // optgroup label toggle support - this.menu - .delegate('li.ui-multiselect-optgroup-label a', 'click.multiselect', function(e) { - e.preventDefault() - - const $this = $(this), - $inputs = $this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)'), - nodes = $inputs.get(), - label = $this.parent().text() - - // trigger event and bail if the return is false - if (self._trigger('beforeoptgrouptoggle', e, { inputs: nodes, label }) === false) { - return - } - - // toggle inputs - self._toggleChecked( - $inputs.filter(':checked').length !== $inputs.length, - $inputs, - ) - - self._trigger('optgrouptoggle', e, { - inputs: nodes, - label, - checked: nodes[0].checked, - }) - }) - .delegate('label', 'mouseenter.multiselect', function() { - if (!$(this).hasClass('ui-state-disabled')) { - self.labels.removeClass('ui-state-hover') - $(this).addClass('ui-state-hover').find('input').focus() - } - }) - .delegate('label', 'keydown.multiselect', function(e) { - e.preventDefault() - - switch (e.which) { - case 9: // tab - case 27: // esc - self.close() - break - case 38: // up - case 40: // down - case 37: // left - case 39: // right - self._traverse(e.which, this) - break - case 13: // enter - $(this).find('input')[0].click() - break - } - }) - .delegate('input[type="checkbox"], input[type="radio"]', 'click.multiselect', function(e) { - const $this = $(this), - val = this.value, - checked = this.checked, - tags = self.element.find('option') - - // bail if this input is disabled or the event is cancelled - if (this.disabled || self._trigger('click', e, { value: val, text: this.title, checked }) === false) { - e.preventDefault() - return - } - - // make sure the input has focus. otherwise, the esc key - // won't close the menu after clicking an item. - $this.focus() - - // toggle aria state - $this.attr('aria-selected', checked) - - // change state on the original option tags - tags.each(function() { - if (this.value === val) { - this.selected = checked - } else if (!self.options.multiple) { - this.selected = false - } - }) - - // some additional single select-specific logic - if (!self.options.multiple) { - self.labels.removeClass('ui-state-active') - $this.closest('label').toggleClass('ui-state-active', checked) - - // close menu - self.close() - } - - // fire change on the select box - self.element.trigger('change') - - // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827 - // http://bugs.jquery.com/ticket/3827 - setTimeout($.proxy(self.update, self), 10) - }) - - // close each widget when clicking on any other element/anywhere else on the page - $(document).bind('mousedown.multiselect', function(e) { - if (self._isOpen && !$.contains(self.menu[0], e.target) && !$.contains(self.button[0], e.target) && e.target !== self.button[0]) { - self.close() - } - }) - - // deal with form resets. the problem here is that buttons aren't - // restored to their defaultValue prop on form reset, and the reset - // handler fires before the form is actually reset. delaying it a bit - // gives the form inputs time to clear. - $(this.element[0].form).bind('reset.multiselect', function() { - setTimeout($.proxy(self.refresh, self), 10) - }) - }, - - // set button width - _setButtonWidth: function() { - let width = this.element.outerWidth(), - o = this.options - - if (/\d/.test(o.minWidth) && width < o.minWidth) { - width = o.minWidth - } - - // set widths - this.button.width(width) - }, - - // set menu width - _setMenuWidth: function() { - const m = this.menu, - width = this.button.outerWidth() - - parseInt(m.css('padding-left'), 10) - - parseInt(m.css('padding-right'), 10) - - parseInt(m.css('border-right-width'), 10) - - parseInt(m.css('border-left-width'), 10) - - m.width(width || this.button.outerWidth()) - }, - - // move up or down within the menu - _traverse: function(which, start) { - const $start = $(start), - moveToLast = which === 38 || which === 37, - - // select the first li that isn't an optgroup label / disabled - $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)')[moveToLast ? 'last' : 'first']() - - // if at the first/last element - if (!$next.length) { - const $container = this.menu.find('ul').last() - - // move to the first/last - this.menu.find('label')[moveToLast ? 'last' : 'first']().trigger('mouseover') - - // set scroll position - $container.scrollTop(moveToLast ? $container.height() : 0) - } else { - $next.find('label').trigger('mouseover') - } - }, - - // This is an internal function to toggle the checked property and - // other related attributes of a checkbox. - // - // The context of this function should be a checkbox; do not proxy it. - _toggleState: function(prop, flag) { - return function() { - if (!this.disabled) { - this[prop] = flag - } - - if (flag) { - this.setAttribute('aria-selected', true) - } else { - this.removeAttribute('aria-selected') - } - } - }, - - _toggleChecked: function(flag, group) { - const $inputs = (group && group.length) ? group : this.inputs, - self = this - - // toggle state on inputs - $inputs.each(this._toggleState('checked', flag)) - - // give the first input focus - $inputs.eq(0).focus() - - // update button text - this.update() - - // gather an array of the values that actually changed - const values = $inputs.map(function() { - return this.value - }).get() - - // toggle state on original option tags - this.element - .find('option') - .each(function() { - if (!this.disabled && $.inArray(this.value, values) > -1) { - self._toggleState('selected', flag).call(this) - } - }) - - // trigger the change event on the select - if ($inputs.length) { - this.element.trigger('change') - } - }, - - _toggleDisabled: function(flag) { - this.button - .attr({ disabled: flag, 'aria-disabled': flag })[flag ? 'addClass' : 'removeClass']('ui-state-disabled') - - let inputs = this.menu.find('input') - const key = 'ech-multiselect-disabled' - - if (flag) { - // remember which elements this widget disabled (not pre-disabled) - // elements, so that they can be restored if the widget is re-enabled. - inputs = inputs.filter(':enabled') - .data(key, true) - } else { - inputs = inputs.filter(function() { - return $.data(this, key) === true - }).removeData(key) - } - - inputs - .attr({ disabled: flag, 'arial-disabled': flag }) - .parent()[flag ? 'addClass' : 'removeClass']('ui-state-disabled') - - this.element - .attr({ disabled: flag, 'aria-disabled': flag }) - }, - - // open the menu - open: function(e) { - let self = this, - button = this.button, - menu = this.menu, - speed = this.speed, - o = this.options, - args = [] - - // bail if the multiselectopen event returns false, this widget is disabled, or is already open - if (this._trigger('beforeopen') === false || button.hasClass('ui-state-disabled') || this._isOpen) { - return - } - - let $container = menu.find('ul').last(), - effect = o.show, - pos = button.offset() - - // figure out opening effects/speeds - if ($.isArray(o.show)) { - effect = o.show[0] - speed = o.show[1] || self.speed - } - - // if there's an effect, assume jQuery UI is in use - // build the arguments to pass to show() - if (effect) { - args = [effect, speed] - } - - // set the scroll of the checkbox container - $container.scrollTop(0).height(o.height) - - // position and show menu - if ($.ui.position && !$.isEmptyObject(o.position)) { - o.position.of = o.position.of || button - - menu - .show() - .position(o.position) - .hide() - - // if position utility is not available... - } else { - menu.css({ - top: pos.top + button.outerHeight(), - 'inset-inline-start': pos.left, - }) - } - - // show the menu, maybe with a speed/effect combo - $.fn.show.apply(menu, args) - - // select the first option - // triggering both mouseover and mouseover because 1.4.2+ has a bug where triggering mouseover - // will actually trigger mouseenter. the mouseenter trigger is there for when it's eventually fixed - this.labels.eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus') - - button.addClass('ui-state-active') - this._isOpen = true - this._trigger('open') - }, - - // close the menu - close: function() { - if (this._trigger('beforeclose') === false) { - return - } - - let o = this.options, - effect = o.hide, - speed = this.speed, - args = [] - - // figure out opening effects/speeds - if ($.isArray(o.hide)) { - effect = o.hide[0] - speed = o.hide[1] || this.speed - } - - if (effect) { - args = [effect, speed] - } - - $.fn.hide.apply(this.menu, args) - this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave') - this._isOpen = false - this._trigger('close') - }, - - enable: function() { - this._toggleDisabled(false) - }, - - disable: function() { - this._toggleDisabled(true) - }, - - checkAll: function(e) { - this._toggleChecked(true) - this._trigger('checkAll') - }, - - uncheckAll: function() { - this._toggleChecked(false) - this._trigger('uncheckAll') - }, - - getChecked: function() { - return this.menu.find('input').filter(':checked') - }, - - destroy: function() { - // remove classes + data - $.Widget.prototype.destroy.call(this) - - this.button.remove() - this.menu.remove() - this.element.show() - - return this - }, - - isOpen: function() { - return this._isOpen - }, - - widget: function() { - return this.menu - }, - - getButton: function() { - return this.button - }, - - // react to option changes after initialization - _setOption: function(key, value) { - const menu = this.menu - - switch (key) { - case 'header': - menu.find('div.ui-multiselect-header')[value ? 'show' : 'hide']() - break - case 'checkAllText': - menu.find('a.ui-multiselect-all span').eq(-1).text(value) - break - case 'uncheckAllText': - menu.find('a.ui-multiselect-none span').eq(-1).text(value) - break - case 'height': - menu.find('ul').last().height(parseInt(value, 10)) - break - case 'minWidth': - this.options[key] = parseInt(value, 10) - this._setButtonWidth() - this._setMenuWidth() - break - case 'selectedText': - case 'selectedList': - case 'noneSelectedText': - this.options[key] = value // these all needs to update immediately for the update() call - this.update() - break - case 'classes': - menu.add(this.button).removeClass(this.options.classes).addClass(value) - break - case 'multiple': - menu.toggleClass('ui-multiselect-single', !value) - this.options.multiple = value - this.element[0].multiple = value - this.refresh() - } - - $.Widget.prototype._setOption.apply(this, arguments) - }, - }) -})(jQuery) diff --git a/apps/user_ldap/lib/AppInfo/Application.php b/apps/user_ldap/lib/AppInfo/Application.php index bc55f907732..76462698280 100644 --- a/apps/user_ldap/lib/AppInfo/Application.php +++ b/apps/user_ldap/lib/AppInfo/Application.php @@ -30,6 +30,7 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\IAppContainer; use OCP\AppFramework\Services\IAppConfig; +use OCP\AppFramework\Services\IInitialState; use OCP\Config\IUserConfig; use OCP\EventDispatcher\IEventDispatcher; use OCP\IAvatarManager; @@ -67,6 +68,7 @@ class Application extends App implements IBootstrap { $appContainer->get(IL10N::class), $appContainer->get('Session'), $appContainer->get(IURLGenerator::class), + $appContainer->get(IInitialState::class), ); }); diff --git a/apps/user_ldap/lib/Controller/RenewPasswordController.php b/apps/user_ldap/lib/Controller/RenewPasswordController.php index 04b919eeb67..d3e89a12d56 100644 --- a/apps/user_ldap/lib/Controller/RenewPasswordController.php +++ b/apps/user_ldap/lib/Controller/RenewPasswordController.php @@ -6,6 +6,7 @@ */ namespace OCA\User_LDAP\Controller; +use OCA\User_LDAP\AppInfo\Application; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; @@ -13,6 +14,7 @@ use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Config\IUserConfig; use OCP\HintException; use OCP\IConfig; @@ -20,8 +22,8 @@ use OCP\IL10N; use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; -use OCP\IUser; use OCP\IUserManager; +use OCP\Util; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class RenewPasswordController extends Controller { @@ -34,6 +36,7 @@ class RenewPasswordController extends Controller { protected IL10N $l10n, private ISession $session, private IURLGenerator $urlGenerator, + private IInitialState $initialState, ) { parent::__construct($appName, $request); } @@ -51,7 +54,7 @@ class RenewPasswordController extends Controller { if (!$this->userConfig->getValueBool($user, 'user_ldap', 'needsPasswordReset')) { return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm')); } - $parameters = []; + $renewPasswordMessages = $this->session->get('renewPasswordMessages'); $errors = []; $messages = []; @@ -59,25 +62,23 @@ class RenewPasswordController extends Controller { [$errors, $messages] = $renewPasswordMessages; } $this->session->remove('renewPasswordMessages'); - foreach ($errors as $value) { - $parameters[$value] = true; - } - $parameters['messages'] = $messages; - $parameters['user'] = $user; - - $parameters['canResetPassword'] = true; - $parameters['resetPasswordLink'] = $this->config->getSystemValue('lost_password_link', ''); - if (!$parameters['resetPasswordLink']) { - $userObj = $this->userManager->get($user); - if ($userObj instanceof IUser) { - $parameters['canResetPassword'] = $userObj->canChangePassword(); - } - } - $parameters['cancelLink'] = $this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm'); + $this->initialState->provideInitialState('renewPasswordParameters', + [ + 'user' => $user, + 'errors' => $errors, + 'messages' => $messages, + 'cancelRenewUrl' => $this->urlGenerator->linkToRouteAbsolute('core.login.showLoginForm'), + 'tryRenewPasswordUrl' => $this->urlGenerator->linkToRouteAbsolute('user_ldap.renewPassword.tryRenewPassword'), + ], + ); + Util::addStyle(Application::APP_ID, 'renewPassword'); + Util::addScript(Application::APP_ID, 'renewPassword'); return new TemplateResponse( - $this->appName, 'renewpassword', $parameters, 'guest' + Application::APP_ID, + 'renewpassword', + renderAs: 'guest', ); } diff --git a/apps/user_ldap/src/LDAPSettingsApp.vue b/apps/user_ldap/src/LDAPSettingsApp.vue deleted file mode 100644 index d2daeb62de6..00000000000 --- a/apps/user_ldap/src/LDAPSettingsApp.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/apps/user_ldap/src/renewPassword.ts b/apps/user_ldap/src/renewPassword.ts new file mode 100644 index 00000000000..f9429229a34 --- /dev/null +++ b/apps/user_ldap/src/renewPassword.ts @@ -0,0 +1,10 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createApp } from 'vue' +import RenewPasswordView from './views/RenewPassword.vue' + +const app = createApp(RenewPasswordView) +app.mount('#user_ldap-renewPassword') diff --git a/apps/user_ldap/src/settings-admin.ts b/apps/user_ldap/src/settings-admin.ts index f7c25e2344a..50a496a571a 100644 --- a/apps/user_ldap/src/settings-admin.ts +++ b/apps/user_ldap/src/settings-admin.ts @@ -2,8 +2,9 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + import { createApp } from 'vue' -import LDAPSettingsApp from './LDAPSettingsApp.vue' +import LDAPSettingsApp from './views/LDAPSettingsApp.vue' import { pinia } from './store/index.ts' const app = createApp(LDAPSettingsApp) diff --git a/apps/user_ldap/src/views/Settings.vue b/apps/user_ldap/src/views/LDAPSettingsApp.vue similarity index 98% rename from apps/user_ldap/src/views/Settings.vue rename to apps/user_ldap/src/views/LDAPSettingsApp.vue index f913a2e9c85..4d1a0ab1810 100644 --- a/apps/user_ldap/src/views/Settings.vue +++ b/apps/user_ldap/src/views/LDAPSettingsApp.vue @@ -87,8 +87,6 @@ + + + + diff --git a/apps/user_ldap/templates/renewpassword.php b/apps/user_ldap/templates/renewpassword.php index 3345be29c13..91821896add 100644 --- a/apps/user_ldap/templates/renewpassword.php +++ b/apps/user_ldap/templates/renewpassword.php @@ -1,66 +1,9 @@ -
      -
      -
      - t('Please renew your password.')); ?>
      -
      - -
      -
      -
      - - -
      - t('An internal error occurred.')); ?>
      - t('Please try again or contact your administrator.')); ?> -
      - - -

      - - -

      - -

      - - - -

      - - - - -

      - t('Wrong password.')); ?> -

      - -

      - - t('Cancel')); ?> - -

      - - -
      -
      +
      diff --git a/build/frontend/vite.config.ts b/build/frontend/vite.config.ts index 1e416b234bc..086f3c748ea 100644 --- a/build/frontend/vite.config.ts +++ b/build/frontend/vite.config.ts @@ -46,6 +46,7 @@ const modules = { 'settings-personal': resolve(import.meta.dirname, 'apps/twofactor_backupcodes/src', 'settings-personal.ts'), }, user_ldap: { + renewPassword: resolve(import.meta.dirname, 'apps/user_ldap/src', 'renewPassword.ts'), 'settings-admin': resolve(import.meta.dirname, 'apps/user_ldap/src', 'settings-admin.ts'), }, user_status: {