Skip to content

Commit f8d6ba4

Browse files
authored
Merge pull request #7603 from LibreSign/backport/7602/stable33
[stable33] fix: align signer email contract with API runtime behavior
2 parents fea53ab + 8265d20 commit f8d6ba4

11 files changed

Lines changed: 264 additions & 56 deletions

File tree

lib/ResponseDefinitions.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
* @psalm-type LibresignSignerSummary = array{
143143
* signRequestId: int,
144144
* displayName: string,
145-
* email: string,
145+
* email?: ?string,
146146
* identifyMethods?: LibresignIdentifyMethod[],
147147
* signed: ?string,
148148
* status: int,
@@ -394,7 +394,7 @@
394394
* statusText: string,
395395
* nodeId: non-negative-int,
396396
* nodeType: 'file'|'envelope',
397-
* signatureFlow: int,
397+
* signatureFlow: 0|1|2,
398398
* docmdpLevel: int,
399399
* filesCount: int<0, max>,
400400
* files: list<LibresignValidatedChildFile>,

openapi-full.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2437,7 +2437,6 @@
24372437
"required": [
24382438
"signRequestId",
24392439
"displayName",
2440-
"email",
24412440
"signed",
24422441
"status",
24432442
"statusText"
@@ -2451,7 +2450,8 @@
24512450
"type": "string"
24522451
},
24532452
"email": {
2454-
"type": "string"
2453+
"type": "string",
2454+
"nullable": true
24552455
},
24562456
"identifyMethods": {
24572457
"type": "array",
@@ -2768,7 +2768,12 @@
27682768
},
27692769
"signatureFlow": {
27702770
"type": "integer",
2771-
"format": "int64"
2771+
"format": "int64",
2772+
"enum": [
2773+
0,
2774+
1,
2775+
2
2776+
]
27722777
},
27732778
"docmdpLevel": {
27742779
"type": "integer",

openapi.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,7 +1839,6 @@
18391839
"required": [
18401840
"signRequestId",
18411841
"displayName",
1842-
"email",
18431842
"signed",
18441843
"status",
18451844
"statusText"
@@ -1853,7 +1852,8 @@
18531852
"type": "string"
18541853
},
18551854
"email": {
1856-
"type": "string"
1855+
"type": "string",
1856+
"nullable": true
18571857
},
18581858
"identifyMethods": {
18591859
"type": "array",
@@ -2156,7 +2156,12 @@
21562156
},
21572157
"signatureFlow": {
21582158
"type": "integer",
2159-
"format": "int64"
2159+
"format": "int64",
2160+
"enum": [
2161+
0,
2162+
1,
2163+
2
2164+
]
21602165
},
21612166
"docmdpLevel": {
21622167
"type": "integer",

playwright/e2e/sign-email-token-unauthenticated.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,30 @@ test('sign document with email token as unauthenticated signer', async ({ page }
6464
const email = await waitForEmailTo(mailpit, 'signer01@libresign.coop', 'LibreSign: There is a file for you to sign')
6565
const signLink = extractSignLink(email.Text)
6666
if (!signLink) throw new Error('Sign link not found in email')
67+
68+
// Regression guard: validation payload can contain signer without email.
69+
// Reuse this existing E2E flow and force `email = null` in the validate response.
70+
await page.route('**/ocs/v2.php/apps/libresign/api/v1/file/validate/uuid/**', async (route) => {
71+
const response = await route.fetch()
72+
const payload = await response.json() as Record<string, unknown>
73+
const ocs = payload.ocs as Record<string, unknown> | undefined
74+
const data = ocs?.data as Record<string, unknown> | undefined
75+
76+
if (data && Array.isArray(data.signers) && data.signers.length > 0) {
77+
const firstSigner = data.signers[0] as Record<string, unknown>
78+
firstSigner.email = null
79+
}
80+
81+
await route.fulfill({
82+
status: response.status(),
83+
headers: {
84+
...response.headers(),
85+
'content-type': 'application/json',
86+
},
87+
body: JSON.stringify(payload),
88+
})
89+
})
90+
6791
await page.goto(signLink);
6892
await page.getByRole('button', { name: 'Sign the document.' }).click();
6993
await page.getByRole('textbox', { name: 'Email' }).click();
@@ -84,6 +108,7 @@ test('sign document with email token as unauthenticated signer', async ({ page }
84108
await page.getByRole('button', { name: 'Sign document' }).click();
85109
await page.waitForURL('**/validation/**');
86110
await expect(page.getByText('This document is valid')).toBeVisible();
111+
await expect(page.getByText('Failed to validate document')).not.toBeVisible();
87112
await expect(page.getByText('Congratulations you have')).toBeVisible();
88113
await expect(page.getByRole('button', { name: 'Sign the document.' })).not.toBeVisible();
89114
});

src/components/Request/SignDetail/partials/SignerRow.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<template #icon>
1212
<NcAvatar is-no-user
1313
:size="44"
14-
:user="signer.email"
14+
:user="signer.email ?? undefined"
1515
:display-name="displayName" />
1616
</template>
1717
<template #subname>

src/components/validation/SignerDetails.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ type SignerModifications = {
284284
285285
type SignerModel = {
286286
displayName?: string
287-
email?: string
287+
email?: string | null
288288
name?: string
289289
remote_address?: string
290290
user_agent?: string

src/services/validationDocument.ts

Lines changed: 124 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export type ValidationModificationInfo = {
3232
valid?: boolean
3333
}
3434

35+
type ValidationSignatureFlow = ValidationFileRecord['signatureFlow']
36+
3537
type ValidationMetadataDimension = {
3638
w: number
3739
h: number
@@ -64,15 +66,12 @@ function isOptionalField(record: UnknownRecord, key: string, guard: (value: unkn
6466
}
6567

6668
function toNumber(value: unknown): number | null {
67-
if (typeof value === 'number' && Number.isFinite(value)) {
68-
return value
69-
}
70-
71-
if (typeof value === 'string' && /^-?\d+$/.test(value)) {
72-
return Number.parseInt(value, 10)
73-
}
69+
return typeof value === 'number' && Number.isFinite(value) ? value : null
70+
}
7471

75-
return null
72+
function toInteger(value: unknown): number | null {
73+
const normalized = toNumber(value)
74+
return normalized !== null && Number.isInteger(normalized) ? normalized : null
7675
}
7776

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

8685
function isValidationStatus(value: unknown): value is ValidationStatus {
87-
const normalizedValue = toNumber(value)
86+
const normalizedValue = toInteger(value)
8887
return normalizedValue === FILE_STATUS.DRAFT
8988
|| normalizedValue === FILE_STATUS.ABLE_TO_SIGN
9089
|| normalizedValue === FILE_STATUS.PARTIAL_SIGNED
@@ -93,27 +92,30 @@ function isValidationStatus(value: unknown): value is ValidationStatus {
9392
}
9493

9594
function isSignerStatus(value: unknown): value is SignerDetailRecord['status'] {
96-
const normalizedValue = toNumber(value)
95+
const normalizedValue = toInteger(value)
9796
return normalizedValue === SIGN_REQUEST_STATUS.DRAFT
9897
|| normalizedValue === SIGN_REQUEST_STATUS.ABLE_TO_SIGN
9998
|| normalizedValue === SIGN_REQUEST_STATUS.SIGNED
10099
}
101100

102-
function isValidationSignatureFlow(value: unknown): boolean {
103-
if (value === 'none' || value === 'parallel' || value === 'ordered_numeric') {
104-
return true
101+
function isValidationSignatureFlow(value: unknown): value is ValidationSignatureFlow {
102+
return value === 0 || value === 1 || value === 2
103+
}
104+
105+
function normalizeValidationSignatureFlow(value: unknown): ValidationSignatureFlow | null {
106+
if (isValidationSignatureFlow(value)) {
107+
return value
105108
}
106109

107-
const normalizedValue = toNumber(value)
108-
return normalizedValue === 0 || normalizedValue === 1 || normalizedValue === 2
110+
return null
109111
}
110112

111113
function isValidationStatusInfo(value: unknown): value is ValidationStatusInfo {
112114
if (!isRecord(value)) {
113115
return false
114116
}
115117

116-
return isOptionalField(value, 'id', fieldValue => typeof fieldValue === 'number')
118+
return isOptionalField(value, 'id', fieldValue => toInteger(fieldValue) !== null)
117119
&& isOptionalField(value, 'label', isString)
118120
}
119121

@@ -153,7 +155,7 @@ function isValidationMetadata(value: unknown): value is NonNullable<ValidationFi
153155
return false
154156
}
155157

156-
if (!isString(value.extension) || typeof value.p !== 'number') {
158+
if (!isString(value.extension) || toInteger(value.p) === null) {
157159
return false
158160
}
159161

@@ -181,9 +183,9 @@ function isSignerDetailRecord(value: unknown): value is SignerDetailRecord {
181183
return false
182184
}
183185

184-
return typeof value.signRequestId === 'number'
186+
return toInteger(value.signRequestId) !== null
185187
&& isString(value.displayName)
186-
&& isString(value.email)
188+
&& isOptionalField(value, 'email', isNullableString)
187189
&& isNullableString(value.signed)
188190
&& isSignerStatus(value.status)
189191
&& isString(value.statusText)
@@ -203,13 +205,13 @@ function isValidatedChildFileRecord(value: unknown): value is ValidatedChildFile
203205
return false
204206
}
205207

206-
return typeof value.id === 'number'
208+
return toInteger(value.id) !== null
207209
&& isString(value.uuid)
208210
&& isString(value.name)
209211
&& isValidationStatus(value.status)
210212
&& isString(value.statusText)
211-
&& typeof value.nodeId === 'number'
212-
&& typeof value.size === 'number'
213+
&& toInteger(value.nodeId) !== null
214+
&& toInteger(value.size) !== null
213215
&& Array.isArray(value.signers)
214216
&& isString(value.file)
215217
&& isValidationMetadata(value.metadata)
@@ -220,19 +222,19 @@ function isValidationDocumentRecord(data: unknown): data is ValidationFileRecord
220222
return false
221223
}
222224
if (
223-
typeof data.id !== 'number'
225+
toInteger(data.id) === null
224226
|| !isString(data.uuid)
225227
|| !isString(data.name)
226228
|| !isValidationStatus(data.status)
227229
|| !isString(data.statusText)
228-
|| typeof data.nodeId !== 'number'
230+
|| toInteger(data.nodeId) === null
229231
|| (data.nodeType !== 'file' && data.nodeType !== 'envelope')
230-
|| !isValidationSignatureFlow(data.signatureFlow)
231-
|| toNumber(data.docmdpLevel) === null
232-
|| toNumber(data.filesCount) === null
232+
|| normalizeValidationSignatureFlow(data.signatureFlow) === null
233+
|| toInteger(data.docmdpLevel) === null
234+
|| toInteger(data.filesCount) === null
233235
|| !Array.isArray(data.files)
234-
|| toNumber(data.totalPages) === null
235-
|| toNumber(data.size) === null
236+
|| toInteger(data.totalPages) === null
237+
|| toInteger(data.size) === null
236238
|| !isString(data.pdfVersion)
237239
|| !isString(data.created_at)
238240
|| !isRequestedBy(data.requested_by)
@@ -278,24 +280,113 @@ export function toValidationDocument(data: unknown): ValidationDocumentState | n
278280
return null
279281
}
280282

283+
const id = toInteger(data.id)
284+
const nodeId = toInteger(data.nodeId)
285+
const docmdpLevel = toInteger(data.docmdpLevel)
286+
const filesCount = toInteger(data.filesCount)
287+
const totalPages = toInteger(data.totalPages)
288+
const size = toInteger(data.size)
289+
const signatureFlow = normalizeValidationSignatureFlow(data.signatureFlow)
290+
291+
if (
292+
id === null
293+
|| nodeId === null
294+
|| docmdpLevel === null
295+
|| filesCount === null
296+
|| totalPages === null
297+
|| size === null
298+
|| signatureFlow === null
299+
) {
300+
return null
301+
}
302+
303+
const files = data.files.map((file) => {
304+
const childId = toInteger(file.id)
305+
const childNodeId = toInteger(file.nodeId)
306+
const childSize = toInteger(file.size)
307+
const childStatus = toInteger(file.status)
308+
const metadataPages = toInteger(file.metadata.p)
309+
310+
if (
311+
childId === null
312+
|| childNodeId === null
313+
|| childSize === null
314+
|| childStatus === null
315+
|| metadataPages === null
316+
) {
317+
return null
318+
}
319+
320+
return {
321+
...file,
322+
id: childId,
323+
nodeId: childNodeId,
324+
size: childSize,
325+
status: childStatus,
326+
metadata: {
327+
...file.metadata,
328+
p: metadataPages,
329+
},
330+
}
331+
})
332+
333+
if (files.some(file => file === null)) {
334+
return null
335+
}
336+
const normalizedFiles = files.filter((file): file is ValidatedChildFileRecord => file !== null)
337+
281338
const metadata = isValidationMetadata(data.metadata)
282339
? data.metadata
283340
: {
284341
...DEFAULT_VALIDATION_METADATA,
285-
p: data.totalPages,
342+
p: totalPages,
286343
}
287344

345+
const metadataPages = toInteger(metadata.p)
346+
if (metadataPages === null) {
347+
return null
348+
}
349+
288350
const settings = isValidationSettings(data.settings)
289351
? data.settings
290352
: DEFAULT_VALIDATION_SETTINGS
291353

292-
const signers = Array.isArray(data.signers) ? data.signers : []
354+
const signers = (Array.isArray(data.signers) ? data.signers : []).map((signer) => {
355+
const signRequestId = toInteger(signer.signRequestId)
356+
const status = toInteger(signer.status)
357+
358+
if (signRequestId === null || status === null) {
359+
return null
360+
}
361+
362+
return {
363+
...signer,
364+
signRequestId,
365+
status,
366+
}
367+
})
368+
369+
if (signers.some(signer => signer === null)) {
370+
return null
371+
}
372+
const normalizedSigners = signers.filter((signer): signer is SignerDetailRecord => signer !== null)
293373

294374
return {
295375
...data,
296-
metadata,
376+
id,
377+
nodeId,
378+
signatureFlow,
379+
docmdpLevel,
380+
filesCount,
381+
totalPages,
382+
size,
383+
files: normalizedFiles,
384+
metadata: {
385+
...metadata,
386+
p: metadataPages,
387+
},
297388
settings,
298-
signers,
389+
signers: normalizedSigners,
299390
}
300391
}
301392

0 commit comments

Comments
 (0)