Merge pull request #6216 from LibreSign/feat/custom-message-for-signers

feat: custom message for signers
This commit is contained in:
Vitor Mattos 2025-12-16 21:48:53 -03:00 committed by GitHub
commit 9a9ca2bba7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 367 additions and 21 deletions

View file

@ -19,8 +19,10 @@ use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\Collaboration\Collaborators\ISearch;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IShare;
@ -36,6 +38,8 @@ class IdentifyAccountController extends AEnvironmentAwareController {
private IURLGenerator $urlGenerator,
private Email $identifyEmailMethod,
private Account $identifyAccountMethod,
private IUserManager $userManager,
private IConfig $config,
) {
parent::__construct(Application::APP_ID, $request);
}
@ -76,6 +80,7 @@ class IdentifyAccountController extends AEnvironmentAwareController {
$return = $this->addHerselfAccount($return, $search);
$return = $this->addHerselfEmail($return, $search);
$return = $this->replaceShareTypeByMethod($return);
$return = $this->addEmailNotificationPreference($return);
$return = $this->excludeNotAllowed($return);
return new DataResponse($return);
@ -239,4 +244,62 @@ class IdentifyAccountController extends AEnvironmentAwareController {
}
return $list;
}
private function addEmailNotificationPreference(array $list): array {
foreach ($list as $key => $item) {
if ($item['method'] !== 'account') {
continue;
}
$user = $this->userManager->get($item['id']);
if ($user === null) {
$list[$key]['acceptsEmailNotifications'] = false;
continue;
}
$email = $user->getEMailAddress();
if (empty($email)) {
$list[$key]['acceptsEmailNotifications'] = false;
continue;
}
if ($this->isNotificationDisabledAtActivity($user->getUID(), 'libresign_file_to_sign')) {
$list[$key]['acceptsEmailNotifications'] = false;
continue;
}
$list[$key]['acceptsEmailNotifications'] = true;
}
return $list;
}
private function isNotificationDisabledAtActivity(string $userId, string $type): bool {
if (!class_exists(\OCA\Activity\UserSettings::class)) {
return false;
}
$activityUserSettings = \OCP\Server::get(\OCA\Activity\UserSettings::class);
if ($activityUserSettings) {
$manager = \OCP\Server::get(\OCP\Activity\IManager::class);
try {
$manager->getSettingById($type);
} catch (\Exception $e) {
return false;
}
$adminSetting = $activityUserSettings->getAdminSetting('email', $type);
if (!$adminSetting) {
return true;
}
$notificationSetting = $activityUserSettings->getUserSetting(
$userId,
'email',
$type
);
if (!$notificationSetting) {
return true;
}
}
return false;
}
}

View file

@ -125,6 +125,7 @@ class IdentifyMethodMapper extends QBMapper {
->join('im', 'libresign_sign_request', 'sr',
$qb->expr()->eq('sr.id', 'im.sign_request_id'),
)
->where($qb->expr()->neq('im.identifier_value', $qb->createNamedParameter('deleted_users')))
->setMaxResults($limit)
->setFirstResult($offset);

View file

@ -60,10 +60,17 @@ class SignRequestMapper extends QBMapper {
if (!isset($metadata['notify'])) {
$this->firstNotification = true;
}
$metadata['notify'][] = [
$notificationEntry = [
'method' => $method,
'date' => time(),
];
if (!empty($fromDatabase->getDescription())) {
$notificationEntry['description'] = $fromDatabase->getDescription();
}
$metadata['notify'][] = $notificationEntry;
$fromDatabase->setMetadata($metadata);
$this->update($fromDatabase);
$this->db->commit();

View file

@ -89,10 +89,10 @@ class MailNotifyListener implements IEventListener {
$isFirstNotification = $this->signRequestMapper->incrementNotificationCounter($signRequest, 'mail');
if ($isFirstNotification) {
$this->mail->notifyUnsignedUser($signRequest, $email);
$this->mail->notifyUnsignedUser($signRequest, $email, $signRequest->getDescription());
return;
}
$this->mail->notifySignDataUpdated($signRequest, $email);
$this->mail->notifySignDataUpdated($signRequest, $email, $signRequest->getDescription());
} catch (\InvalidArgumentException $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return;
@ -174,6 +174,17 @@ class MailNotifyListener implements IEventListener {
}
$activityUserSettings = \OCP\Server::get(\OCA\Activity\UserSettings::class);
if ($activityUserSettings) {
$manager = \OCP\Server::get(\OCP\Activity\IManager::class);
try {
$manager->getSettingById($type);
} catch (\Exception $e) {
return false;
}
$adminSetting = $activityUserSettings->getAdminSetting('email', $type);
if (!$adminSetting) {
return true;
}
$notificationSetting = $activityUserSettings->getUserSetting(
$userId,
'email',

View file

@ -210,13 +210,23 @@ class NotificationListener implements IEventListener {
}
$activityUserSettings = \OCP\Server::get(\OCA\Activity\UserSettings::class);
if ($activityUserSettings) {
$manager = \OCP\Server::get(\OCP\Activity\IManager::class);
try {
$manager->getSettingById($type);
} catch (\Exception $e) {
return false;
}
$adminSetting = $activityUserSettings->getAdminSetting('notification', $type);
if (!$adminSetting) {
return true;
}
$notificationSetting = $activityUserSettings->getUserSetting(
$userId,
'notification',
$type
);
// If setting is explicitly false, notifications are disabled
// If setting is null/not configured, notifications are enabled by default
if ($notificationSetting === false) {
return true;
}

View file

@ -81,10 +81,16 @@ class TwofactorGatewayListener implements IEventListener {
}
$isFirstNotification = $this->signRequestMapper->incrementNotificationCounter($signRequest, $entity->getIdentifierKey());
$message = '';
if (!empty($signRequest->getDescription())) {
$message = $signRequest->getDescription() . "\n\n";
}
if ($isFirstNotification) {
$message = $this->l10n->t('There is a document for you to sign. Access the link below:');
$message .= $this->l10n->t('There is a document for you to sign. Access the link below:');
} else {
$message = $this->l10n->t('Changes have been made in a file that you have to sign. Access the link below:');
$message .= $this->l10n->t('Changes have been made in a file that you have to sign. Access the link below:');
}
$message .= "\n";
$link = $this->urlGenerator->linkToRouteAbsolute('libresign.page.sign', ['uuid' => $signRequest->getUuid()]);

View file

@ -28,6 +28,9 @@ namespace OCA\Libresign;
* email?: string,
* account?: string,
* },
* displayName?: string,
* description?: string,
* notify?: non-negative-int,
* signingOrder?: non-negative-int,
* }
* @psalm-type LibresignNewFile = array{
@ -55,6 +58,7 @@ namespace OCA\Libresign;
* subname: string,
* shareType: 0|4,
* icon?: 'icon-mail'|'icon-user',
* acceptsEmailNotifications?: boolean,
* }
* @psalm-type LibresignPagination = array{
* total: non-negative-int,

View file

@ -45,12 +45,18 @@ class MailService {
/**
* @psalm-suppress MixedMethodCall
*/
public function notifySignDataUpdated(SignRequest $data, string $email): void {
public function notifySignDataUpdated(SignRequest $data, string $email, ?string $description = null): void {
$emailTemplate = $this->mailer->createEMailTemplate('settings.TestEmail');
// TRANSLATORS The subject of the email that is sent after changes are made to the signature request that may affect something for the signer who will sign the document. Some possible reasons: URL for signature changed (when the URL expires), the person who requested the signature sent a notification
$emailTemplate->setSubject($this->l10n->t('LibreSign: Changes into a file for you to sign'));
$emailTemplate->addHeader();
$emailTemplate->addHeading($this->l10n->t('File to sign'), false);
if (!empty($description)) {
$emailTemplate->addBodyText($description);
$emailTemplate->addBodyText('');
}
$emailTemplate->addBodyText($this->l10n->t('Changes have been made in a file that you have to sign. Access the link below:'));
$link = $this->urlGenerator->linkToRouteAbsolute('libresign.page.sign', ['uuid' => $data->getUuid()]);
$file = $this->getFileById($data->getFileId());
@ -76,11 +82,17 @@ class MailService {
/**
* @psalm-suppress MixedMethodCall
*/
public function notifyUnsignedUser(SignRequest $data, string $email): void {
public function notifyUnsignedUser(SignRequest $data, string $email, ?string $description = null): void {
$emailTemplate = $this->mailer->createEMailTemplate('settings.TestEmail');
$emailTemplate->setSubject($this->l10n->t('LibreSign: There is a file for you to sign'));
$emailTemplate->addHeader();
$emailTemplate->addHeading($this->l10n->t('File to sign'), false);
if (!empty($description)) {
$emailTemplate->addBodyText($description);
$emailTemplate->addBodyText('');
}
$emailTemplate->addBodyText($this->l10n->t('There is a document for you to sign. Access the link below:'));
$link = $this->urlGenerator->linkToRouteAbsolute('libresign.page.sign', ['uuid' => $data->getUuid()]);
$file = $this->getFileById($data->getFileId());

View file

@ -453,6 +453,9 @@
"icon-mail",
"icon-user"
]
},
"acceptsEmailNotifications": {
"type": "boolean"
}
}
},
@ -514,6 +517,17 @@
}
}
},
"displayName": {
"type": "string"
},
"description": {
"type": "string"
},
"notify": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"signingOrder": {
"type": "integer",
"format": "int64",

View file

@ -383,6 +383,9 @@
"icon-mail",
"icon-user"
]
},
"acceptsEmailNotifications": {
"type": "boolean"
}
}
},
@ -444,6 +447,17 @@
}
}
},
"displayName": {
"type": "string"
},
"description": {
"type": "string"
},
"notify": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"signingOrder": {
"type": "integer",
"format": "int64",

View file

@ -26,6 +26,23 @@
:error="nameHaveError"
:helper-text="nameHelperText"
@update:value="onNameChange" />
<div v-if="signerSelected && showCustomMessage" class="description-wrapper">
<NcCheckboxRadioSwitch v-model:checked="enableCustomMessage"
type="switch"
@update:checked="onToggleCustomMessage">
{{ t('libresign', 'Add custom message') }}
</NcCheckboxRadioSwitch>
<NcTextArea v-if="enableCustomMessage"
v-model="description"
aria-describedby="description-input"
maxlength="500"
:label="t('libresign', 'Custom message')"
:placeholder="t('libresign', 'Add a personal message for this signer')"
:rows="3"
resize="none" />
</div>
<div class="identifySigner__footer">
<div class="button-group">
<NcButton @click="filesStore.disableIdentifySigner()">
@ -48,8 +65,10 @@ import svgWhatsapp from '@mdi/svg/svg/whatsapp.svg?raw'
import svgXmpp from '@mdi/svg/svg/xmpp.svg?raw'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import SignerSelect from './SignerSelect.vue'
@ -72,8 +91,10 @@ export default {
name: 'IdentifySigner',
components: {
NcButton,
NcCheckboxRadioSwitch,
NcIconSvgWrapper,
NcNoteCard,
NcTextArea,
NcTextField,
SignerSelect,
},
@ -109,6 +130,8 @@ export default {
nameHelperText: '',
nameHaveError: false,
displayName: '',
description: '',
enableCustomMessage: false,
identify: '',
signer: {},
}
@ -136,9 +159,17 @@ export default {
}
return methodConfig.friendly_name
},
showCustomMessage() {
if (this.signer?.method === 'account') {
return this.signer?.acceptsEmailNotifications === true
}
return !!this.signer?.method
},
},
beforeMount() {
this.displayName = this.signerToEdit.displayName ?? ''
this.description = this.signerToEdit.description ?? ''
this.enableCustomMessage = !!(this.signerToEdit.description)
this.identify = this.signerToEdit.identify ?? this.signerToEdit.signRequestId ?? ''
if (Object.keys(this.signerToEdit).length > 0 && this.signerToEdit.identifyMethods?.length) {
const method = this.signerToEdit.identifyMethods[0]
@ -161,6 +192,11 @@ export default {
this.signer = signer ?? {}
this.displayName = signer?.displayName ?? ''
this.identify = signer?.id ?? ''
if (signer?.method === 'account' && signer?.acceptsEmailNotifications === false) {
this.enableCustomMessage = false
this.description = ''
}
},
async saveSigner() {
if (!this.signer?.method || !this.signer?.id) {
@ -168,6 +204,7 @@ export default {
}
this.filesStore.signerUpdate({
displayName: this.displayName,
description: this.description.trim() || undefined,
identify: this.identify,
identifyMethods: [
{
@ -184,6 +221,7 @@ export default {
}
this.displayName = ''
this.description = ''
this.identify = ''
this.signer = {}
this.filesStore.disableIdentifySigner()
@ -198,6 +236,11 @@ export default {
this.nameHelperText = t('libresign', 'Please enter signer name.')
this.nameHaveError = true
},
onToggleCustomMessage(checked) {
if (!checked) {
this.description = ''
}
},
},
}
</script>
@ -224,6 +267,14 @@ export default {
gap: 0.5em;
}
}
.description-wrapper {
width: 100%;
margin-bottom: 16px;
:deep(textarea) {
margin-top: 8px;
}
}
&__footer {
width: 100%;
@ -234,6 +285,7 @@ export default {
justify-content: space-between;
align-items: flex-start;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--color-main-background));
padding-top: 24px;
.button-group {
display: flex;

View file

@ -26,6 +26,14 @@
<OrderNumericAscending :size="20" />
</template>
</NcActionInput>
<NcActionButton v-if="canCustomizeMessage(signer)"
:close-after-click="true"
@click="customizeMessage(signer); closeActions()">
<template #icon>
<MessageText :size="20" />
</template>
{{ t('libresign', 'Customize message') }}
</NcActionButton>
<NcActionButton v-if="canDelete(signer)"
aria-label="Delete"
:close-after-click="true"
@ -44,9 +52,11 @@
{{ t('libresign', 'Request signature') }}
</NcActionButton>
<NcActionButton v-if="canSendReminder(signer)"
icon="icon-comment"
:close-after-click="true"
@click="sendNotify(signer)">
<template #icon>
<Bell :size="20" />
</template>
{{ t('libresign', 'Send reminder') }}
</NcActionButton>
</template>
@ -194,10 +204,12 @@ import svgSms from '@mdi/svg/svg/message-processing.svg?raw'
import svgWhatsapp from '@mdi/svg/svg/whatsapp.svg?raw'
import svgXmpp from '@mdi/svg/svg/xmpp.svg?raw'
import Bell from 'vue-material-design-icons/Bell.vue'
import Delete from 'vue-material-design-icons/Delete.vue'
import Draw from 'vue-material-design-icons/Draw.vue'
import FileDocument from 'vue-material-design-icons/FileDocument.vue'
import Information from 'vue-material-design-icons/Information.vue'
import MessageText from 'vue-material-design-icons/MessageText.vue'
import OrderNumericAscending from 'vue-material-design-icons/OrderNumericAscending.vue'
import Pencil from 'vue-material-design-icons/Pencil.vue'
import Send from 'vue-material-design-icons/Send.vue'
@ -261,10 +273,12 @@ export default {
NcModal,
NcNoteCard,
NcDialog,
Bell,
Delete,
Draw,
FileDocument,
Information,
MessageText,
OrderNumericAscending,
Pencil,
Send,
@ -323,6 +337,20 @@ export default {
return this.filesStore.canSave() && !signer.signed
}
},
canCustomizeMessage() {
return (signer) => {
if (signer.signed || !signer.signRequestId || signer.me) {
return false
}
const method = signer.identifyMethods?.[0]?.method
if (method === 'account' && !signer.acceptsEmailNotifications) {
return false
}
return !!method
}
},
canRequestSignature() {
return (signer) => {
if (!this.filesStore.canRequestSign
@ -530,6 +558,10 @@ export default {
}
this.filesStore.enableIdentifySigner()
},
customizeMessage(signer) {
this.signerToEdit = signer
this.filesStore.enableIdentifySigner()
},
onTabChange(tabId) {
if (this.activeTab !== tabId) {
this.activeTab = tabId

View file

@ -1584,6 +1584,7 @@ export type components = {
shareType: 0 | 4;
/** @enum {string} */
icon?: "icon-mail" | "icon-user";
acceptsEmailNotifications?: boolean;
};
IdentifyMethod: {
/** @enum {string} */
@ -1603,6 +1604,10 @@ export type components = {
email?: string;
account?: string;
};
displayName?: string;
description?: string;
/** Format: int64 */
notify?: number;
/** Format: int64 */
signingOrder?: number;
};

View file

@ -1128,6 +1128,7 @@ export type components = {
shareType: 0 | 4;
/** @enum {string} */
icon?: "icon-mail" | "icon-user";
acceptsEmailNotifications?: boolean;
};
IdentifyMethod: {
/** @enum {string} */
@ -1147,6 +1148,10 @@ export type components = {
email?: string;
account?: string;
};
displayName?: string;
description?: string;
/** Format: int64 */
notify?: number;
/** Format: int64 */
signingOrder?: number;
};

View file

@ -17,7 +17,7 @@ Feature: account/signature
| (jq).ocs.data.rootCert.names[4].id | OU |
| (jq).ocs.data.rootCert.names[4].value\|length | 2 |
| (jq).ocs.data.rootCert.names[4].value[0] | Organizational Unit |
| (jq).ocs.data.rootCert.names[4].value | (jq)any(.[]; test("^libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?$")) |
| (jq).ocs.data.rootCert.names[4].value | (jq)any(.[]; test("^libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?$")) |
| (jq).ocs.data.generated | true |
Scenario: Create root certificate with CFSSL engine using API
@ -40,7 +40,7 @@ Feature: account/signature
| (jq).ocs.data.rootCert.names[4].id | OU |
| (jq).ocs.data.rootCert.names[4].value\|length | 2 |
| (jq).ocs.data.rootCert.names[4].value[0] | Organizational Unit |
| (jq).ocs.data.rootCert.names[4].value | (jq)any(.[]; test("^libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?$")) |
| (jq).ocs.data.rootCert.names[4].value | (jq)any(.[]; test("^libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?$")) |
| (jq).ocs.data.generated | true |
Scenario: Create pfx with success using CFSSL
@ -77,7 +77,7 @@ Feature: account/signature
| (jq).ocs.data | (jq).name \|test("/ST=State of Company") |
| (jq).ocs.data | (jq).name \|test("/L=City Name") |
| (jq).ocs.data | (jq).name \|test("/O=Organization") |
| (jq).ocs.data | (jq).name \|test("/OU=Organization Unit, libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?") |
| (jq).ocs.data | (jq).name \|test("/OU=Organization Unit, libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?") |
| (jq).ocs.data | (jq).name \|test("/CN=account:signer1, signer1-displayname") |
| (jq).ocs.data.issuer\|length | 6 |
| (jq).ocs.data.issuer.CN | Common Name |
@ -87,7 +87,7 @@ Feature: account/signature
| (jq).ocs.data.issuer.O | Organization |
| (jq).ocs.data.issuer.OU\|length | 2 |
| (jq).ocs.data.issuer.OU | (jq) .[0] \|test("^Organization Unit$") |
| (jq).ocs.data.issuer.OU | (jq) .[1] \|test("^libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.issuer.OU | (jq) .[1] \|test("^libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.subject\|length | 6 |
| (jq).ocs.data.subject.CN | (jq) .[0] \|test("^account:signer1$") |
| (jq).ocs.data.subject.CN | (jq) .[1] \|test("^signer1-displayname$") |
@ -97,7 +97,7 @@ Feature: account/signature
| (jq).ocs.data.subject.O | Organization |
| (jq).ocs.data.issuer.OU\|length | 2 |
| (jq).ocs.data.issuer.OU | (jq) .[0] \|test("^Organization Unit$") |
| (jq).ocs.data.issuer.OU | (jq) .[1] \|test("^libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.issuer.OU | (jq) .[1] \|test("^libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.extensions.basicConstraints | CA:FALSE |
| (jq).ocs.data.extensions.subjectAltName | email:signer@domain.test |
| (jq).ocs.data.extensions.keyUsage | Digital Signature, Non Repudiation, Key Encipherment |
@ -138,7 +138,7 @@ Feature: account/signature
| (jq).ocs.data | (jq).name \|test("/ST=State of Company") |
| (jq).ocs.data | (jq).name \|test("/L=City Name") |
| (jq).ocs.data | (jq).name \|test("/O=Organization") |
| (jq).ocs.data | (jq).name \|test("/OU=Organization Unit, libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?") |
| (jq).ocs.data | (jq).name \|test("/OU=Organization Unit, libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?") |
| (jq).ocs.data | (jq).name \|test("/UID=account:signer1") |
| (jq).ocs.data | (jq).name \|test("/CN=signer1-displayname") |
| (jq).ocs.data.issuer\|length | 6 |
@ -149,7 +149,7 @@ Feature: account/signature
| (jq).ocs.data.issuer.O | Organization |
| (jq).ocs.data.issuer.OU\|length | 2 |
| (jq).ocs.data.issuer.OU | (jq) .[0] \|test("^Organization Unit$") |
| (jq).ocs.data.issuer.OU | (jq) .[1] \|test("^libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.issuer.OU | (jq) .[1] \|test("^libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.subject\|length | 7 |
| (jq).ocs.data.subject.CN | signer1-displayname |
| (jq).ocs.data.subject.C | BR |
@ -158,7 +158,7 @@ Feature: account/signature
| (jq).ocs.data.subject.O | Organization |
| (jq).ocs.data.subject.OU \|length | 2 |
| (jq).ocs.data.subject.OU | (jq) .[0] \|test("^Organization Unit$") |
| (jq).ocs.data.subject.OU | (jq) .[1] \|test("^libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.subject.OU | (jq) .[1] \|test("^libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.subject.UID | account:signer1 |
| (jq).ocs.data.extensions.basicConstraints | CA:FALSE |
| (jq).ocs.data.extensions.subjectAltName | email:signer@domain.test |

View file

@ -11,7 +11,7 @@ Feature: admin/certificate_openssl
| (jq).ocs.data.rootCert.commonName | Common Name |
| (jq).ocs.data.rootCert.names\|length | 1 |
| (jq).ocs.data.rootCert.names[0].id | OU |
| (jq).ocs.data.rootCert.names[0].value | (jq) .[] \|test("^libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.rootCert.names[0].value | (jq) .[] \|test("^libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.generated | true |
Scenario: Generate root cert with fail without CN
@ -46,7 +46,7 @@ Feature: admin/certificate_openssl
| (jq).ocs.data.rootCert.names[0].id | C |
| (jq).ocs.data.rootCert.names[0].value | BR |
| (jq).ocs.data.rootCert.names[1].id | OU |
| (jq).ocs.data.rootCert.names[1].value | (jq) .[] \|test("^libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.rootCert.names[1].value | (jq) .[] \|test("^libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?$") |
| (jq).ocs.data.generated | true |
Scenario: Generate root cert with fail when country have less then 2 characters

View file

@ -38,7 +38,7 @@ Feature: validate
| (jq).ocs.data.signers[0] | (jq).name \|test("/ST=State of Company") |
| (jq).ocs.data.signers[0] | (jq).name \|test("/L=City Name") |
| (jq).ocs.data.signers[0] | (jq).name \|test("/O=Organization") |
| (jq).ocs.data.signers[0] | (jq).name \|test("/OU=Organization Unit, libresign-ca-id:[a-z0-9]{10}_g:[0-9]+_e:[oc]?") |
| (jq).ocs.data.signers[0] | (jq).name \|test("/OU=Organization Unit, libresign-ca-id:[a-z0-9]+_g:[0-9]+_e:[oc]?") |
| (jq).ocs.data.signers[0] | (jq).name \|test("/CN=signer1-displayname") |
| (jq).ocs.data.signers[0].subject.CN | signer1-displayname |
| (jq).ocs.data.signers[0].subject.C | BR |

View file

@ -118,3 +118,54 @@ Feature: search
| (jq).ocs.data[0].subname | admin@email.tld |
| (jq).ocs.data[0].icon | icon-mail |
| (jq).ocs.data[0].method | email |
Scenario: Search account returns acceptsEmailNotifications true when user accepts email
Given as user "admin"
And user "notification-enabled" exists
And set the email of user "notification-enabled" to "enabled@test.com"
And run the command "config:app:set activity notify_email_libresign_file_to_sign --value=1" with result code 0
And run the command "user:setting notification-enabled activity notify_email_libresign_file_to_sign 1" with result code 0
And sending "post" to ocs "/apps/provisioning_api/api/v1/config/apps/libresign/identify_methods"
| value | (string)[{"name":"account","enabled":true}] |
When sending "get" to ocs "/apps/libresign/api/v1/identify-account/search?search=notification-enabled"
Then the response should have a status code 200
And the response should be a JSON array with the following mandatory values
| key | value |
| (jq).ocs.data\|length | 1 |
| (jq).ocs.data[0].id | notification-enabled |
| (jq).ocs.data[0].method | account |
| (jq).ocs.data[0].acceptsEmailNotifications | true |
Scenario: Search account returns acceptsEmailNotifications false when user disabled email
Given as user "admin"
And user "notification-disabled" exists
And set the email of user "notification-disabled" to "disabled@test.com"
And run the command "config:app:set activity notify_email_libresign_file_to_sign --value=1" with result code 0
And run the command "user:setting notification-disabled activity notify_email_libresign_file_to_sign 0" with result code 0
And sending "post" to ocs "/apps/provisioning_api/api/v1/config/apps/libresign/identify_methods"
| value | (string)[{"name":"account","enabled":true}] |
When sending "get" to ocs "/apps/libresign/api/v1/identify-account/search?search=notification-disabled"
Then the response should have a status code 200
And the response should be a JSON array with the following mandatory values
| key | value |
| (jq).ocs.data\|length | 1 |
| (jq).ocs.data[0].id | notification-disabled |
| (jq).ocs.data[0].method | account |
| (jq).ocs.data[0].acceptsEmailNotifications | false |
Scenario: Search account returns acceptsEmailNotifications false when global setting disabled
Given as user "admin"
And user "notification-global-off" exists
And set the email of user "notification-global-off" to "globaloff@test.com"
And run the command "config:app:set activity notify_email_libresign_file_to_sign --value=0" with result code 0
And run the command "user:setting notification-global-off activity notify_email_libresign_file_to_sign 1" with result code 0
And sending "post" to ocs "/apps/provisioning_api/api/v1/config/apps/libresign/identify_methods"
| value | (string)[{"name":"account","enabled":true}] |
When sending "get" to ocs "/apps/libresign/api/v1/identify-account/search?search=notification-global-off"
Then the response should have a status code 200
And the response should be a JSON array with the following mandatory values
| key | value |
| (jq).ocs.data\|length | 1 |
| (jq).ocs.data[0].id | notification-global-off |
| (jq).ocs.data[0].method | account |
| (jq).ocs.data[0].acceptsEmailNotifications | false |

View file

@ -0,0 +1,59 @@
Feature: Custom message for signers
In order to provide personalized instructions to signers
As a document owner
I want to send custom messages to signers via email
Background:
Given as user "admin"
And user "signer1" exists
And set the email of user "signer1" to "signer1@test.com"
And my inbox is empty
And reset notifications of user "signer1"
And run the command "libresign:configure:openssl --cn test" with result code 0
And run the command "config:app:set activity notify_email_libresign_file_to_sign --value=1" with result code 0
And run the command "user:setting signer1 activity notify_email_libresign_file_to_sign 1" with result code 0
Scenario: Account method - default message without custom description
When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
| file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
| name | Document without custom message |
| users | [{"identify":{"account":"signer1"}}] |
Then the response should have a status code 200
And there should be 1 emails in my inbox
When I open the latest email to "signer1@test.com" with subject "LibreSign: There is a file for you to sign"
Then I should see "There is a document for you to sign" in the opened email
Scenario: Account method - custom description in email
When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
| file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
| name | Document with custom message |
| users | [{"identify":{"account":"signer1"},"description":"Please review section 3 and the appendix before signing."}] |
Then the response should have a status code 200
And there should be 1 emails in my inbox
When I open the latest email to "signer1@test.com" with subject "LibreSign: There is a file for you to sign"
Then I should see "Please review section 3 and the appendix before signing" in the opened email
And I should see "There is a document for you to sign" in the opened email
Scenario: Email method - default notification
When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
| file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
| name | Document for email method |
| users | [{"identify":{"email":"external@domain.test"},"displayName":"External Signer"}] |
Then the response should have a status code 200
Scenario: Email method - custom description via reminder
Given sending "post" to ocs "/apps/libresign/api/v1/request-signature"
| file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
| name | Document for email with description |
| users | [{"identify":{"email":"external@domain.test"},"displayName":"External Signer","description":"Urgent: Please sign by end of day."}] |
And the response should have a status code 200
And fetch field "(FILE_ID)ocs.data.data.nodeId" from previous JSON response
And fetch field "(SIGN_REQUEST_ID)ocs.data.data.signers.0.signRequestId" from previous JSON response
And my inbox is empty
When sending "post" to ocs "/apps/libresign/api/v1/notify/signer"
| fileId | <FILE_ID> |
| signRequestId | <SIGN_REQUEST_ID> |
Then the response should have a status code 200
And there should be 1 emails in my inbox
When I open the latest email to "external@domain.test"
Then I should see "Urgent: Please sign by end of day" in the opened email