fix(core): prompt for password once when installing recommended apps

Wire the password-confirmation interceptors into the recommendedapps
entry point and switch the installer to a single bulk enable call so
the strict password confirmation on enableApps is satisfied.

Fixes #60068
-e
Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
This commit is contained in:
Peter Ringelmann 2026-05-05 14:30:01 +02:00
parent 9bc1a9245c
commit 66a8d4582c
4 changed files with 69 additions and 28 deletions

View file

@ -58,9 +58,9 @@
<script>
import { t } from '@nextcloud/l10n'
import { loadState } from '@nextcloud/initial-state'
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
import { generateUrl, imagePath } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import pLimit from 'p-limit'
import logger from '../../logger.js'
import NcButton from '@nextcloud/vue/components/NcButton'
@ -140,35 +140,41 @@ export default {
}
},
methods: {
installApps() {
this.installingApps = true
const limit = pLimit(1)
const installing = this.recommendedApps
async installApps() {
const apps = this.recommendedApps
.filter(app => !app.active && app.isCompatible && app.canInstall && app.isSelected)
.map(app => limit(async () => {
logger.info(`installing ${app.id}`)
app.loading = true
return axios.post(generateUrl('settings/apps/enable'), { appIds: [app.id], groups: [] })
.catch(error => {
logger.error(`could not install ${app.id}`, { error })
app.isSelected = false
app.installationError = true
})
.then(() => {
logger.info(`installed ${app.id}`)
app.loading = false
app.active = true
})
}))
logger.debug(`installing ${installing.length} recommended apps`)
Promise.all(installing)
.then(() => {
logger.info('all recommended apps installed, redirecting …')
if (apps.length === 0) {
return
}
window.location = this.defaultPageUrl
this.installingApps = true
apps.forEach(app => {
app.loading = true
})
const appIds = apps.map(app => app.id)
logger.debug(`installing ${apps.length} recommended apps`, { appIds })
try {
await axios.post(
generateUrl('settings/apps/enable'),
{ appIds, groups: [] },
{ confirmPassword: PwdConfirmationMode.Strict },
)
apps.forEach(app => {
app.loading = false
app.active = true
})
.catch(error => logger.error('could not install recommended apps', { error }))
logger.info('all recommended apps installed, redirecting …')
window.location = this.defaultPageUrl
} catch (error) {
logger.error('could not install recommended apps', { error })
apps.forEach(app => {
app.loading = false
app.isSelected = false
app.installationError = true
})
this.installingApps = false
}
},
customIcon(appId) {
if (!(appId in recommended) || !recommended[appId].icon) {

View file

@ -4,12 +4,16 @@
*/
import { getCSPNonce } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import { translate as t } from '@nextcloud/l10n'
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
import Vue from 'vue'
import logger from './logger.js'
import RecommendedApps from './components/setup/RecommendedApps.vue'
addPasswordConfirmationInterceptors(axios)
// eslint-disable-next-line camelcase
__webpack_nonce__ = getCSPNonce()

View file

@ -10,6 +10,33 @@ export function getUnifiedSearchModal() {
return cy.get('#unified-search')
}
/**
* Confirm the password-confirmation modal if it is visible.
* Used by flows that hit endpoints requiring strict re-authentication.
*
* @param adminPassword Password to type into the confirmation dialog.
*/
export function handlePasswordConfirmation(adminPassword = 'admin') {
const handleModal = (context: Cypress.Chainable) => {
return context.contains('.modal-container', 'Authentication required')
.if()
.within(() => {
cy.get('input[type="password"]')
.type(adminPassword)
cy.findByRole('button', { name: 'Confirm' })
.click()
})
}
return cy.get('body')
.if()
.then(() => handleModal(cy.get('body')))
.else()
// Handle if inside a cy.within
.root().closest('body')
.then(($body) => handleModal(cy.wrap($body)))
}
/**
* Open the unified search modal
*/

View file

@ -3,6 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { handlePasswordConfirmation } from '../core-utils.ts'
type RecommendedAppsMode = 'skip' | 'install-success' | 'install-failure'
/**
* DO NOT RENAME THIS FILE to .cy.ts
* This is not following the pattern of the other files in this folder
@ -110,7 +114,7 @@ describe('Can install Nextcloud', { testIsolation: true, retries: 0 }, () => {
/**
* Shared admin setup function for the Nextcloud setup
*/
function sharedSetup() {
function sharedSetup(mode: RecommendedAppsMode = 'skip') {
const randAdmin = 'admin-' + Math.random().toString(36).substring(2, 15)
// mock appstore