From 3455a5c6d2c3c41fa43af56a0f9ace6ad0986eb9 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 17 Mar 2026 17:55:05 +0100 Subject: [PATCH 1/2] feat: use status header on recent Nextcloud versions to detect confirmation error Signed-off-by: Ferdinand Thiessen --- src/apiError.spec.ts | 93 +++++++++++++++++++++++++++++++ src/apiError.ts | 30 ++++++++++ src/components/PasswordDialog.vue | 36 +++++------- src/env.d.ts | 4 ++ src/password-confirmation.ts | 17 +++--- 5 files changed, 152 insertions(+), 28 deletions(-) create mode 100644 src/apiError.spec.ts create mode 100644 src/apiError.ts diff --git a/src/apiError.spec.ts b/src/apiError.spec.ts new file mode 100644 index 00000000..4059b26b --- /dev/null +++ b/src/apiError.spec.ts @@ -0,0 +1,93 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +import { beforeEach, describe, expect, test, vi } from 'vitest' + +const isAxiosErrorMock = vi.fn() +const loggerDebugMock = vi.fn() + +vi.mock('@nextcloud/axios', () => ({ + isAxiosError: isAxiosErrorMock, +})) + +vi.mock('./utils/logger.ts', () => ({ + logger: { + debug: loggerDebugMock, + }, +})) + +describe('isConfirmationError', () => { + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + window._oc_config = undefined + }) + + async function importSubject(version?: string) { + window._oc_config = version ? { version } : undefined + return import('./apiError.ts') + } + + test('returns false for non axios errors', async () => { + const { isConfirmationError } = await importSubject('33.0.0') + isAxiosErrorMock.mockReturnValue(false) + + expect(isConfirmationError(new Error('nope'))).toBe(false) + expect(loggerDebugMock).not.toBeCalled() + }) + + test('returns false when axios error has no response', async () => { + const { isConfirmationError } = await importSubject('33.0.0') + isAxiosErrorMock.mockReturnValue(true) + + expect(isConfirmationError({ response: undefined })).toBe(false) + expect(loggerDebugMock).not.toBeCalled() + }) + + test('uses header based detection on Nextcloud 32', async () => { + const { isConfirmationError } = await importSubject('32.0.7') + isAxiosErrorMock.mockReturnValue(true) + + const error = { + response: { + headers: { + 'x-nextcloud-password-confirmation': 'true', + }, + status: 403, + }, + } + + expect(isConfirmationError(error)).toBe(true) + expect(loggerDebugMock).toBeCalledWith('Handle modern confirmation error based on header', { hasConfirmationHeader: true }) + }) + + test('returns false if header is not present on Nextcloud 32', async () => { + const { isConfirmationError } = await importSubject('32.0.7') + isAxiosErrorMock.mockReturnValue(true) + + const error = { + response: { + status: 403, + }, + } + + expect(isConfirmationError(error)).toBe(false) + expect(loggerDebugMock).toBeCalledWith('Handle modern confirmation error based on header', { hasConfirmationHeader: false }) + }) + + test('uses status based detection on Nextcloud 31', async () => { + const { isConfirmationError } = await importSubject('31.0.8') + isAxiosErrorMock.mockReturnValue(true) + + const error = { + response: { + status: 403, + }, + } + + expect(isConfirmationError(error)).toBe(true) + expect(loggerDebugMock).toBeCalledWith('Handle legacy confirmation error based on status code', { status: 403 }) + }) +}) diff --git a/src/apiError.ts b/src/apiError.ts new file mode 100644 index 00000000..6062357d --- /dev/null +++ b/src/apiError.ts @@ -0,0 +1,30 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +import { isAxiosError } from '@nextcloud/axios' +import { logger } from './utils/logger.ts' + +const [NC_MAJOR_VERSION] = window._oc_config?.version.split('.').map(Number) ?? [] + +/** + * Check if the given error is a confirmation error, + * which means that the password was incorrect. + * + * @param error - The error to check + */ +export function isConfirmationError(error: unknown): boolean { + if (!isAxiosError(error) || !error.response) { + return false + } + + const hasConfirmationHeader = error.response.headers?.['x-nextcloud-password-confirmation'] === 'true' + if (NC_MAJOR_VERSION < 32) { + logger.debug('Handle legacy confirmation error based on status code', { status: error.response.status }) + return error.response.status === 403 + } + + logger.debug('Handle modern confirmation error based on header', { hasConfirmationHeader }) + return hasConfirmationHeader +} diff --git a/src/components/PasswordDialog.vue b/src/components/PasswordDialog.vue index 090acb3e..0f66a781 100644 --- a/src/components/PasswordDialog.vue +++ b/src/components/PasswordDialog.vue @@ -4,10 +4,10 @@ -->