refactor(user_ldap): migrate jQuery UI of password renewal to Vue

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-01-12 18:22:53 +01:00
parent 5f33fee58f
commit 29e31ffdb1
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
13 changed files with 135 additions and 1009 deletions

View file

@ -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
}

View file

@ -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; }

View file

@ -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"]'),
})
})

View file

@ -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 = $('<button type="button"><span class="ui-icon ui-icon-triangle-2-n-s"></span></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 = $('<span />'))
.html(o.noneSelectedText)
.appendTo(button),
menu = (this.menu = $('<div />'))
.addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all')
.addClass(o.classes)
.appendTo(document.body),
header = (this.header = $('<div />'))
.addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix')
.appendTo(menu),
headerLinkContainer = (this.headerLinkContainer = $('<ul />'))
.addClass('ui-helper-reset')
.html(function() {
if (o.header === true) {
return '<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>' + o.checkAllText + '</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>' + o.uncheckAllText + '</span></a></li>'
} else if (typeof o.header === 'string') {
return '<li>' + o.header + '</li>'
} else {
return ''
}
})
.append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>')
.appendTo(header),
checkboxContainer = (this.checkboxContainer = $('<ul />'))
.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 += '<li class="ui-multiselect-optgroup-label ' + parent.className + '"><a href="#">' + optLabel + '</a></li>'
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 += '<li class="' + liClasses + '">'
// create the label
html += '<label for="' + inputID + '" title="' + description + '" class="' + labelClasses.join(' ') + '">'
html += '<input id="' + inputID + '" name="multiselect_' + id + '" type="' + (o.multiple ? 'checkbox' : 'radio') + '" value="' + value + '" title="' + title + '"'
// pre-selected?
if (isSelected) {
html += ' checked="checked"'
html += ' aria-selected="true"'
}
// disabled?
if (isDisabled) {
html += ' disabled="disabled"'
html += ' aria-disabled="true"'
}
// add the title and close everything off
html += ' /><span>' + title + '</span></label></li>'
})
// 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)

View file

@ -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),
);
});

View file

@ -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',
);
}

View file

@ -1,11 +0,0 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<Settings />
</template>
<script lang="ts" setup>
import Settings from './views/Settings.vue'
</script>

View file

@ -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')

View file

@ -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)

View file

@ -87,8 +87,6 @@
</template>
<script lang="ts" setup>
/* eslint vue/multi-word-component-names: "warn" */
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { NcButton, NcCheckboxRadioSwitch, NcNoteCard, NcSelect } from '@nextcloud/vue'
@ -132,8 +130,9 @@ const selectedConfigHasServerInfo = computed(() => {
})
/**
* Request to clear the mapping.
*
* @param subject
* @param subject - The subject to clear
*/
async function requestClearMapping(subject: 'user' | 'group') {
try {

View file

@ -0,0 +1,97 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { getRequestToken } from '@nextcloud/auth'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcGuestContent from '@nextcloud/vue/components/NcGuestContent'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
const renewPasswordParameters = loadState<{
user: string
errors: string[]
messages: string[]
cancelRenewUrl: string
tryRenewPasswordUrl: string
}>('user_ldap', 'renewPasswordParameters')
const hasInvalidPassword = renewPasswordParameters.errors.includes('invalidpassword')
const requestToken = getRequestToken()
const isRenewing = ref(false)
/**
* Handle the form submission.
*/
function onSubmit() {
isRenewing.value = true
}
</script>
<template>
<NcGuestContent>
<h2>{{ t('user_ldap', 'Please renew your password') }}</h2>
<NcNoteCard v-if="renewPasswordParameters.messages.length" type="warning">
<p v-for="(message, index) in renewPasswordParameters.messages" :key="index">
{{ message }}
</p>
</NcNoteCard>
<NcNoteCard
v-if="renewPasswordParameters.errors.includes('internalexception')"
:heading="t('user_ldap', 'An internal error occurred.')"
:text="t('user_ldap', 'Please try again or contact your administrator.')"
type="warning" />
<form
method="post"
name="renewpassword"
:action="renewPasswordParameters.tryRenewPasswordUrl"
@submit="onSubmit">
<NcPasswordField
autofocus
autocomplete="off"
autocapitalize="off"
:error="hasInvalidPassword"
:helper-text="hasInvalidPassword ? t('user_ldap', 'Wrong password.') : ''"
:label="t('user_ldap', 'Current password')"
required
spellcheck="false"
name="oldPassword" />
<NcPasswordField
autofocus
autocomplete="off"
autocapitalize="off"
:label="t('user_ldap', 'New password')"
required
spellcheck="false"
name="newPassword" />
<div :class="$style.renewPassword__actions">
<NcButton :href="renewPasswordParameters.cancelRenewUrl" variant="error">
{{ t('user_ldap', 'Cancel') }}
</NcButton>
<NcButton :disabled="isRenewing" type="submit" variant="primary">
{{ isRenewing ? t('user_ldap', 'Renewing…') : t('user_ldap', 'Renew password') }}
</NcButton>
</div>
<input type="hidden" name="user" :value="renewPasswordParameters.user">
<input type="hidden" name="requesttoken" :value="requestToken">
</form>
</NcGuestContent>
</template>
<style module>
.renewPassword__actions {
display: flex;
justify-content: end;
gap: var(--default-grid-baseline);
margin-top: 1rem;
}
</style>

View file

@ -1,66 +1,9 @@
<?php
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/** @var \OCP\IL10N $l */
\OCP\Util::addScript('user_ldap', 'renewPassword', 'core');
style('user_ldap', 'renewPassword');
?>
<form method="post" name="renewpassword" id="renewpassword" action="<?php p(\OCP\Server::get(\OCP\IURLGenerator::class)->linkToRoute('user_ldap.renewPassword.tryRenewPassword')); ?>">
<fieldset>
<div class="warning title">
<?php p($l->t('Please renew your password.')); ?><br>
</div>
<?php foreach ($_['messages'] as $message): ?>
<div class="warning">
<?php p($message); ?><br>
</div>
<?php endforeach; ?>
<?php if (isset($_['internalexception']) && $_['internalexception']): ?>
<div class="warning">
<?php p($l->t('An internal error occurred.')); ?><br>
<small><?php p($l->t('Please try again or contact your administrator.')); ?></small>
</div>
<?php endif; ?>
<div id="message" class="hidden">
<img class="float-spinner" alt=""
src="<?php p(image_path('core', 'loading-dark.gif'));?>">
<span id="messageText"></span>
<!-- the following div ensures that the spinner is always inside the #message div -->
<div style="clear: both;"></div>
</div>
<p class="grouptop">
<input type="password" id="oldPassword" name="oldPassword"
placeholder="<?php echo $l->t('Current password');?>"
autofocus autocomplete="off" autocapitalize="off" spellcheck="false" required/>
<label for="oldPassword" class="infield"><?php p($l->t('Current password')); ?></label>
</p>
<p class="groupbottom">
<input type="checkbox" id="personal-show" name="show" class="hidden-visually" /><label for="personal-show"></label>
<label id="newPassword-label" for="newPassword" class="infield"><?php p($l->t('New password')); ?></label>
<input type="password" id="newPassword" name="newPassword"
placeholder="<?php echo $l->t('New password');?>"
data-typetoggle="#personal-show" autofocus autocomplete="off" autocapitalize="off" spellcheck="false" required/>
</p>
<input type="submit" id="submit" class="login primary icon-confirm-white" value="<?php p($l->t('Renew password')); ?>"/>
<?php if (!empty($_['invalidpassword'])) { ?>
<p class="warning">
<?php p($l->t('Wrong password.')); ?>
</p>
<?php } ?>
<p id="cancel-container" class="info">
<a id="cancel" href="<?php p($_['cancelLink']); ?>">
<?php p($l->t('Cancel')); ?>
</a>
</p>
<input type="hidden" name="user" id="user" value="<?php p($_['user']) ?>">
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>">
</fieldset>
</form>
<div id="user_ldap-renewPassword"></div>

View file

@ -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: {