diff --git a/l10n/messages.pot b/l10n/messages.pot index 58522f57..3edd97a7 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -21,8 +21,5 @@ msgstr "" msgid "This action needs authentication, please confirm it by entering your password." msgstr "" -msgid "Unknown error while checking password" -msgstr "" - msgid "Wrong password" msgstr "" 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 @@ -->