diff --git a/lib/Service/IdentifyMethod/Email.php b/lib/Service/IdentifyMethod/Email.php index 3019a6df26..aab26b52cc 100644 --- a/lib/Service/IdentifyMethod/Email.php +++ b/lib/Service/IdentifyMethod/Email.php @@ -138,7 +138,19 @@ private function throwIfIsAuthenticatedWithDifferentAccount(): void { } throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_DO_NOTHING, - 'errors' => [['message' => $this->identifyService->getL10n()->t('This document is not yours. Log out and use the sign link again.')]], + 'page_state' => 'authentication_required', + 'page_state_data' => [ + // TRANSLATORS Title shown when a signing link is opened while a different authenticated Nextcloud session is active. This is an expected security check, not a system failure. + 'title' => $this->identifyService->getL10n()->t('Authentication required'), + // TRANSLATORS Description shown when the current authenticated Nextcloud session cannot be used to sign a document because it does not match the authorized signer. + 'description' => $this->identifyService->getL10n()->t('The current authenticated session cannot be used to sign this document.'), + 'noteType' => 'info', + 'icon' => 'info', + ], + 'errors' => [[ + // TRANSLATORS Guidance shown when the current authenticated Nextcloud session cannot be used to sign a document because it does not match the authorized signer. Keep it generic and do not mention emails or delivery channels. + 'message' => $this->identifyService->getL10n()->t('To continue, sign out from the current account and open the signing link again, or open the signing link in a browser session where this account is not active.'), + ]], ])); } diff --git a/playwright/e2e/sign-wrong-session.spec.ts b/playwright/e2e/sign-wrong-session.spec.ts index 4a34a410a6..5088b19f02 100644 --- a/playwright/e2e/sign-wrong-session.spec.ts +++ b/playwright/e2e/sign-wrong-session.spec.ts @@ -10,14 +10,17 @@ import { createMailpitClient, waitForEmailTo, extractSignLink } from '../support /** * When an authenticated Nextcloud user visits a sign link that belongs to a - * different email address, LibreSign must block the attempt with a clear error - * message instead of silently failing or allowing the wrong user to sign. + * different email address, LibreSign must block the attempt with clear + * authentication guidance instead of silently failing or allowing the wrong + * user to sign. * * The admin is logged in as admin@email.tld but the email sign request is for - * signer01@libresign.coop — they do NOT match, so the backend throws: - * "This document is not yours. Log out and use the sign link again." + * signer01@libresign.coop — they do NOT match, so the backend must render the + * neutral authentication-required state and keep the signing UI unavailable. */ -test('authenticated user sees error when accessing another signer\'s email link', async ({ page }) => { +test('authenticated user sees authentication guidance when accessing another signer\'s email link', async ({ page }) => { + test.setTimeout(90_000) + await login( page.request, process.env.NEXTCLOUD_ADMIN_USER ?? 'admin', @@ -50,10 +53,9 @@ test('authenticated user sees error when accessing another signer\'s email link' await page.getByRole('textbox', { name: 'URL of a PDF file' }).fill('https://raw.githubusercontent.com/LibreSign/libresign/main/tests/php/fixtures/pdfs/small_valid.pdf') await page.getByRole('button', { name: 'Send' }).click() - // Email signer — only the email method is active so there are no tabs in the Add signer dialog + // Email signer — only the email method is active so there are no tabs in the Add signer dialog. await page.getByRole('button', { name: 'Add signer' }).click() - await page.getByPlaceholder('Email').click() - await page.getByPlaceholder('Email').pressSequentially('signer01@libresign.coop', { delay: 50 }) + await page.getByPlaceholder('Email').fill('signer01@libresign.coop') await page.getByRole('option', { name: 'signer01@libresign.coop' }).click() await page.getByRole('textbox', { name: 'Signer name' }).fill('Signer 01') await page.getByRole('button', { name: 'Save' }).click() @@ -63,16 +65,22 @@ test('authenticated user sees error when accessing another signer\'s email link' // Retrieve the sign link from the notification email sent to the signer. // The admin is intentionally NOT logged out — this simulates the wrong-session scenario. - const email = await waitForEmailTo(mailpit, 'signer01@libresign.coop', 'LibreSign: There is a file for you to sign') + const email = await waitForEmailTo(mailpit, 'signer01@libresign.coop', 'LibreSign: There is a file for you to sign', { + timeout: 60_000, + }) const signLink = extractSignLink(email.Text) if (!signLink) throw new Error('Sign link not found in email') // Admin is still logged in (admin@email.tld) but navigates to a link // that belongs to signer01@libresign.coop — the emails do NOT match. // The identity check runs on page load; the "Sign the document." button is - // never rendered — the error is shown directly in the signing status panel. + // never rendered — the authentication-required guidance is shown directly. await page.goto(signLink) - // Backend must return the "wrong session" error via ACTION_DO_NOTHING. - await expect(page.getByText('This document is not yours. Log out and use the sign link again.')).toBeVisible() + // Backend must keep blocking the flow via ACTION_DO_NOTHING while explaining + // how to continue safely without exposing either account identity. + await expect(page.getByText('Authentication required')).toBeVisible() + await expect(page.getByText('The current authenticated session cannot be used to sign this document.')).toBeVisible() + await expect(page.getByText('To continue, sign out from the current account and open the signing link again, or open the signing link in a browser session where this account is not active.')).toBeVisible() + await expect(page.getByRole('button', { name: 'Sign the document.' })).toHaveCount(0) }) diff --git a/src/tests/views/DefaultPageError.spec.ts b/src/tests/views/DefaultPageError.spec.ts index db03de1873..6dd5bb2ddf 100644 --- a/src/tests/views/DefaultPageError.spec.ts +++ b/src/tests/views/DefaultPageError.spec.ts @@ -21,7 +21,7 @@ vi.mock('@nextcloud/vue/components/NcIconSvgWrapper', () => ({ default: { name: 'NcIconSvgWrapper', template: '', props: ['path', 'size'] }, })) vi.mock('@nextcloud/vue/components/NcNoteCard', () => ({ - default: { name: 'NcNoteCard', template: '
', props: ['type'] }, + default: { name: 'NcNoteCard', template: '
', props: ['type'] }, })) import DefaultPageError from '../../views/DefaultPageError.vue' @@ -38,7 +38,7 @@ describe('DefaultPageError', () => { it('shows "An error occurred" title and error message when errors are provided via initial state', () => { loadState.mockImplementation((app, key, defaultValue) => { if (app === 'libresign' && key === 'errors') { - return [{ message: 'This document is not yours. Log out and use the sign link again.' }] + return [{ message: 'Something went wrong' }] } return defaultValue }) @@ -47,7 +47,38 @@ describe('DefaultPageError', () => { expect(wrapper.find('.title').text()).toBe('An error occurred') expect(wrapper.find('.description').text()).toBe('') - expect(wrapper.find('.nc-note-card').text()).toContain('This document is not yours. Log out and use the sign link again.') + expect(wrapper.find('.nc-note-card').text()).toContain('Something went wrong') + expect(wrapper.find('.nc-note-card').attributes('data-type')).toBe('error') + }) + + it('shows authentication-required guidance with non-error styling for wrong authenticated session state', () => { + loadState.mockImplementation((app, key, defaultValue) => { + if (app !== 'libresign') { + return defaultValue + } + + if (key === 'page_state_data') { + return { + title: 'Authentication required', + description: 'The current authenticated session cannot be used to sign this document.', + noteType: 'info', + icon: 'info', + } + } + + if (key === 'errors') { + return [{ message: 'To continue, sign out from the current account and open the signing link again, or open the signing link in a browser session where this account is not active.' }] + } + + return defaultValue + }) + + const wrapper = mount(DefaultPageError) + + expect(wrapper.find('.title').text()).toBe('Authentication required') + expect(wrapper.find('.description').text()).toBe('The current authenticated session cannot be used to sign this document.') + expect(wrapper.find('.nc-note-card').text()).toContain('To continue, sign out from the current account and open the signing link again, or open the signing link in a browser session where this account is not active.') + expect(wrapper.find('.nc-note-card').attributes('data-type')).not.toBe('error') }) it('shows "Page not found" title and description when no errors are present', () => { diff --git a/src/views/DefaultPageError.vue b/src/views/DefaultPageError.vue index cb3fbd71ef..329fbac682 100644 --- a/src/views/DefaultPageError.vue +++ b/src/views/DefaultPageError.vue @@ -15,13 +15,13 @@