Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
* @psalm-type LibresignSignerSummary = array{
* signRequestId: int,
* displayName: string,
* email: string,
* email?: ?string,
* identifyMethods?: LibresignIdentifyMethod[],
* signed: ?string,
* status: int,
Expand Down Expand Up @@ -394,7 +394,7 @@
* statusText: string,
* nodeId: non-negative-int,
* nodeType: 'file'|'envelope',
* signatureFlow: int,
* signatureFlow: 0|1|2,
* docmdpLevel: int,
* filesCount: int<0, max>,
* files: list<LibresignValidatedChildFile>,
Expand Down
11 changes: 8 additions & 3 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -2436,7 +2436,6 @@
"required": [
"signRequestId",
"displayName",
"email",
"signed",
"status",
"statusText"
Expand All @@ -2450,7 +2449,8 @@
"type": "string"
},
"email": {
"type": "string"
"type": "string",
"nullable": true
},
"identifyMethods": {
"type": "array",
Expand Down Expand Up @@ -2767,7 +2767,12 @@
},
"signatureFlow": {
"type": "integer",
"format": "int64"
"format": "int64",
"enum": [
0,
1,
2
]
},
"docmdpLevel": {
"type": "integer",
Expand Down
11 changes: 8 additions & 3 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1839,7 +1839,6 @@
"required": [
"signRequestId",
"displayName",
"email",
"signed",
"status",
"statusText"
Expand All @@ -1853,7 +1852,8 @@
"type": "string"
},
"email": {
"type": "string"
"type": "string",
"nullable": true
},
"identifyMethods": {
"type": "array",
Expand Down Expand Up @@ -2156,7 +2156,12 @@
},
"signatureFlow": {
"type": "integer",
"format": "int64"
"format": "int64",
"enum": [
0,
1,
2
]
},
"docmdpLevel": {
"type": "integer",
Expand Down
25 changes: 25 additions & 0 deletions playwright/e2e/sign-email-token-unauthenticated.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ test('sign document with email token as unauthenticated signer', async ({ page }
const email = await waitForEmailTo(mailpit, '[email protected]', 'LibreSign: There is a file for you to sign')
const signLink = extractSignLink(email.Text)
if (!signLink) throw new Error('Sign link not found in email')

// Regression guard: validation payload can contain signer without email.
// Reuse this existing E2E flow and force `email = null` in the validate response.
await page.route('**/ocs/v2.php/apps/libresign/api/v1/file/validate/uuid/**', async (route) => {
const response = await route.fetch()
const payload = await response.json() as Record<string, unknown>
const ocs = payload.ocs as Record<string, unknown> | undefined
const data = ocs?.data as Record<string, unknown> | undefined

if (data && Array.isArray(data.signers) && data.signers.length > 0) {
const firstSigner = data.signers[0] as Record<string, unknown>
firstSigner.email = null
}

await route.fulfill({
status: response.status(),
headers: {
...response.headers(),
'content-type': 'application/json',
},
body: JSON.stringify(payload),
})
})

await page.goto(signLink);
await page.getByRole('button', { name: 'Sign the document.' }).click();
await page.getByRole('textbox', { name: 'Email' }).click();
Expand All @@ -84,6 +108,7 @@ test('sign document with email token as unauthenticated signer', async ({ page }
await page.getByRole('button', { name: 'Sign document' }).click();
await page.waitForURL('**/validation/**');
await expect(page.getByText('This document is valid')).toBeVisible();
await expect(page.getByText('Failed to validate document')).not.toBeVisible();
await expect(page.getByText('Congratulations you have')).toBeVisible();
await expect(page.getByRole('button', { name: 'Sign the document.' })).not.toBeVisible();
});
2 changes: 1 addition & 1 deletion src/components/Request/SignDetail/partials/SignerRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<template #icon>
<NcAvatar is-no-user
:size="44"
:user="signer.email"
:user="signer.email ?? undefined"
:display-name="displayName" />
</template>
<template #subname>
Expand Down
2 changes: 1 addition & 1 deletion src/components/validation/SignerDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ type SignerModifications = {

type SignerModel = {
displayName?: string
email?: string
email?: string | null
name?: string
remote_address?: string
user_agent?: string
Expand Down
157 changes: 124 additions & 33 deletions src/services/validationDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export type ValidationModificationInfo = {
valid?: boolean
}

type ValidationSignatureFlow = ValidationFileRecord['signatureFlow']

type ValidationMetadataDimension = {
w: number
h: number
Expand Down Expand Up @@ -64,15 +66,12 @@ function isOptionalField(record: UnknownRecord, key: string, guard: (value: unkn
}

function toNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}

if (typeof value === 'string' && /^-?\d+$/.test(value)) {
return Number.parseInt(value, 10)
}
return typeof value === 'number' && Number.isFinite(value) ? value : null
}

return null
function toInteger(value: unknown): number | null {
const normalized = toNumber(value)
return normalized !== null && Number.isInteger(normalized) ? normalized : null
}

function isString(value: unknown): value is string {
Expand All @@ -84,7 +83,7 @@ function isNullableString(value: unknown): value is string | null {
}

function isValidationStatus(value: unknown): value is ValidationStatus {
const normalizedValue = toNumber(value)
const normalizedValue = toInteger(value)
return normalizedValue === FILE_STATUS.DRAFT
|| normalizedValue === FILE_STATUS.ABLE_TO_SIGN
|| normalizedValue === FILE_STATUS.PARTIAL_SIGNED
Expand All @@ -93,27 +92,30 @@ function isValidationStatus(value: unknown): value is ValidationStatus {
}

function isSignerStatus(value: unknown): value is SignerDetailRecord['status'] {
const normalizedValue = toNumber(value)
const normalizedValue = toInteger(value)
return normalizedValue === SIGN_REQUEST_STATUS.DRAFT
|| normalizedValue === SIGN_REQUEST_STATUS.ABLE_TO_SIGN
|| normalizedValue === SIGN_REQUEST_STATUS.SIGNED
}

function isValidationSignatureFlow(value: unknown): boolean {
if (value === 'none' || value === 'parallel' || value === 'ordered_numeric') {
return true
function isValidationSignatureFlow(value: unknown): value is ValidationSignatureFlow {
return value === 0 || value === 1 || value === 2
}

function normalizeValidationSignatureFlow(value: unknown): ValidationSignatureFlow | null {
if (isValidationSignatureFlow(value)) {
return value
}

const normalizedValue = toNumber(value)
return normalizedValue === 0 || normalizedValue === 1 || normalizedValue === 2
return null
}

function isValidationStatusInfo(value: unknown): value is ValidationStatusInfo {
if (!isRecord(value)) {
return false
}

return isOptionalField(value, 'id', fieldValue => typeof fieldValue === 'number')
return isOptionalField(value, 'id', fieldValue => toInteger(fieldValue) !== null)
&& isOptionalField(value, 'label', isString)
}

Expand Down Expand Up @@ -153,7 +155,7 @@ function isValidationMetadata(value: unknown): value is NonNullable<ValidationFi
return false
}

if (!isString(value.extension) || typeof value.p !== 'number') {
if (!isString(value.extension) || toInteger(value.p) === null) {
return false
}

Expand Down Expand Up @@ -181,9 +183,9 @@ function isSignerDetailRecord(value: unknown): value is SignerDetailRecord {
return false
}

return typeof value.signRequestId === 'number'
return toInteger(value.signRequestId) !== null
&& isString(value.displayName)
&& isString(value.email)
&& isOptionalField(value, 'email', isNullableString)
&& isNullableString(value.signed)
&& isSignerStatus(value.status)
&& isString(value.statusText)
Expand All @@ -203,13 +205,13 @@ function isValidatedChildFileRecord(value: unknown): value is ValidatedChildFile
return false
}

return typeof value.id === 'number'
return toInteger(value.id) !== null
&& isString(value.uuid)
&& isString(value.name)
&& isValidationStatus(value.status)
&& isString(value.statusText)
&& typeof value.nodeId === 'number'
&& typeof value.size === 'number'
&& toInteger(value.nodeId) !== null
&& toInteger(value.size) !== null
&& Array.isArray(value.signers)
&& isString(value.file)
&& isValidationMetadata(value.metadata)
Expand All @@ -220,19 +222,19 @@ function isValidationDocumentRecord(data: unknown): data is ValidationFileRecord
return false
}
if (
typeof data.id !== 'number'
toInteger(data.id) === null
|| !isString(data.uuid)
|| !isString(data.name)
|| !isValidationStatus(data.status)
|| !isString(data.statusText)
|| typeof data.nodeId !== 'number'
|| toInteger(data.nodeId) === null
|| (data.nodeType !== 'file' && data.nodeType !== 'envelope')
|| !isValidationSignatureFlow(data.signatureFlow)
|| toNumber(data.docmdpLevel) === null
|| toNumber(data.filesCount) === null
|| normalizeValidationSignatureFlow(data.signatureFlow) === null
|| toInteger(data.docmdpLevel) === null
|| toInteger(data.filesCount) === null
|| !Array.isArray(data.files)
|| toNumber(data.totalPages) === null
|| toNumber(data.size) === null
|| toInteger(data.totalPages) === null
|| toInteger(data.size) === null
|| !isString(data.pdfVersion)
|| !isString(data.created_at)
|| !isRequestedBy(data.requested_by)
Expand Down Expand Up @@ -278,24 +280,113 @@ export function toValidationDocument(data: unknown): ValidationDocumentState | n
return null
}

const id = toInteger(data.id)
const nodeId = toInteger(data.nodeId)
const docmdpLevel = toInteger(data.docmdpLevel)
const filesCount = toInteger(data.filesCount)
const totalPages = toInteger(data.totalPages)
const size = toInteger(data.size)
const signatureFlow = normalizeValidationSignatureFlow(data.signatureFlow)

if (
id === null
|| nodeId === null
|| docmdpLevel === null
|| filesCount === null
|| totalPages === null
|| size === null
|| signatureFlow === null
) {
return null
}

const files = data.files.map((file) => {
const childId = toInteger(file.id)
const childNodeId = toInteger(file.nodeId)
const childSize = toInteger(file.size)
const childStatus = toInteger(file.status)
const metadataPages = toInteger(file.metadata.p)

if (
childId === null
|| childNodeId === null
|| childSize === null
|| childStatus === null
|| metadataPages === null
) {
return null
}

return {
...file,
id: childId,
nodeId: childNodeId,
size: childSize,
status: childStatus,
metadata: {
...file.metadata,
p: metadataPages,
},
}
})

if (files.some(file => file === null)) {
return null
}
const normalizedFiles = files.filter((file): file is ValidatedChildFileRecord => file !== null)

const metadata = isValidationMetadata(data.metadata)
? data.metadata
: {
...DEFAULT_VALIDATION_METADATA,
p: data.totalPages,
p: totalPages,
}

const metadataPages = toInteger(metadata.p)
if (metadataPages === null) {
return null
}

const settings = isValidationSettings(data.settings)
? data.settings
: DEFAULT_VALIDATION_SETTINGS

const signers = Array.isArray(data.signers) ? data.signers : []
const signers = (Array.isArray(data.signers) ? data.signers : []).map((signer) => {
const signRequestId = toInteger(signer.signRequestId)
const status = toInteger(signer.status)

if (signRequestId === null || status === null) {
return null
}

return {
...signer,
signRequestId,
status,
}
})

if (signers.some(signer => signer === null)) {
return null
}
const normalizedSigners = signers.filter((signer): signer is SignerDetailRecord => signer !== null)

return {
...data,
metadata,
id,
nodeId,
signatureFlow,
docmdpLevel,
filesCount,
totalPages,
size,
files: normalizedFiles,
metadata: {
...metadata,
p: metadataPages,
},
settings,
signers,
signers: normalizedSigners,
}
}

Expand Down
Loading
Loading