diff --git a/EnvelopeGenerator.Service/Scheduler.vb b/EnvelopeGenerator.Service/Scheduler.vb index 0ff579fc..232a9e51 100644 --- a/EnvelopeGenerator.Service/Scheduler.vb +++ b/EnvelopeGenerator.Service/Scheduler.vb @@ -9,8 +9,8 @@ Public Class Scheduler Inherits BaseClass Private Scheduler As IScheduler - Private ConnectionString As String - Private LicenseKey As String + Private ReadOnly ConnectionString As String + Private ReadOnly LicenseKey As String Private Const JobName = "CertificateDocumentJob" diff --git a/EnvelopeGenerator.Web/Controllers/DocumentController.cs b/EnvelopeGenerator.Web/Controllers/DocumentController.cs index 95ebb7b1..98f7b17e 100644 --- a/EnvelopeGenerator.Web/Controllers/DocumentController.cs +++ b/EnvelopeGenerator.Web/Controllers/DocumentController.cs @@ -58,7 +58,7 @@ namespace EnvelopeGenerator.Web.Controllers actionService.OpenEnvelope(response.Envelope, response.Receiver); - return Ok(); + return Ok(new object()); } catch (Exception e) { diff --git a/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs b/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs index 18d0f856..ce12611f 100644 --- a/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs +++ b/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs @@ -26,6 +26,11 @@ namespace EnvelopeGenerator.Web.Controllers // Validate Envelope Key and load envelope envelopeService.EnsureValidEnvelopeKey(envelopeKey); EnvelopeResponse response = envelopeService.LoadEnvelope(envelopeKey); + + if (envelopeService.ReceiverAlreadySigned(response.Envelope, response.Receiver.Id) == true) + { + return Problem(statusCode: 403); + } logger.Debug("Loaded envelope [{0}] for receiver [{1}]", response.Envelope.Id, response.Envelope.Id); return Json(response); @@ -63,7 +68,7 @@ namespace EnvelopeGenerator.Web.Controllers var signResult = actionService?.SignEnvelope(response.Envelope, response.Receiver); - return Ok(); + return Ok(new object()); } catch (Exception e) { diff --git a/EnvelopeGenerator.Web/Services/EnvelopeService.cs b/EnvelopeGenerator.Web/Services/EnvelopeService.cs index 731ab7ec..5e15a3b6 100644 --- a/EnvelopeGenerator.Web/Services/EnvelopeService.cs +++ b/EnvelopeGenerator.Web/Services/EnvelopeService.cs @@ -10,6 +10,8 @@ namespace EnvelopeGenerator.Web.Services { private readonly ReceiverModel receiverModel; private readonly EnvelopeModel envelopeModel; + private readonly HistoryModel historyModel; + private readonly DocumentStatusModel documentStatusModel; public EnvelopeService(IConfiguration Config, LoggingService Logging, DatabaseService database) : base(Config, Logging) @@ -23,6 +25,7 @@ namespace EnvelopeGenerator.Web.Services receiverModel = database.Models.receiverModel; envelopeModel = database.Models.envelopeModel; + historyModel = database.Models.historyModel; documentStatusModel = database.Models.documentStatusModel; } @@ -110,6 +113,11 @@ namespace EnvelopeGenerator.Web.Services return (List)envelopeModel.List(pReceiverId); } + public bool ReceiverAlreadySigned(Envelope envelope, int receiverId) + { + return historyModel.HasReceiverSigned(envelope.Id, receiverId); + } + public async Task EnsureValidAnnotationData(HttpRequest request) { try diff --git a/EnvelopeGenerator.Web/wwwroot/js/annotation.js b/EnvelopeGenerator.Web/wwwroot/js/annotation.js index a0322782..96c0387b 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/annotation.js +++ b/EnvelopeGenerator.Web/wwwroot/js/annotation.js @@ -3,7 +3,7 @@ const annotations = [] document.elements.forEach((element) => { - console.log('Creating annotation for element', element.id) + console.debug('Creating annotation for element', element.id) const [annotation, formField] = this.createAnnotationFromElement(element) annotations.push(annotation) @@ -13,17 +13,20 @@ return annotations } - async deleteAnnotations(instance) { - let pageAnnotations = ( - await Promise.all( - Array.from({ length: instance.totalPageCount }).map((_, pageIndex) => - instance.getAnnotations(pageIndex) - ) + async getAnnotations(instance) { + const array = await Promise.all( + Array.from({ length: instance.totalPageCount }).map((_, pageIndex) => + instance.getAnnotations(pageIndex) ) ) - .flatMap((annotations) => - annotations.reduce((acc, annotation) => acc.concat(annotation), []) - ) + + return array.flatMap((annotations) => + annotations.reduce((acc, annotation) => acc.concat(annotation), []) + ) + } + + async deleteAnnotations(instance) { + let pageAnnotations = this.getAnnotations(instance) .filter( (annotation) => !!annotation.isSignature || annotation.description == 'FRAME' @@ -33,16 +36,7 @@ } async validateAnnotations(instance) { - let pageAnnotations = ( - await Promise.all( - Array.from({ length: instance.totalPageCount }).map((_, pageIndex) => - instance.getAnnotations(pageIndex) - ) - ) - ) - .flatMap((annotations) => - annotations.reduce((acc, annotation) => acc.concat(annotation), []) - ) + let pageAnnotations = this.getAnnotations(instance) .map((annotation) => { console.log(annotation.toJS()) return annotation diff --git a/EnvelopeGenerator.Web/wwwroot/js/app.js b/EnvelopeGenerator.Web/wwwroot/js/app.js index 9c6a1f92..ad98cb10 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/app.js +++ b/EnvelopeGenerator.Web/wwwroot/js/app.js @@ -23,17 +23,17 @@ class App { this.Instance = null this.currentDocument = null this.currentReceiver = null + this.signatureCount = 0 } - // This function will be called in the ShowEnvelope.razor page + // This function will be called from the ShowEnvelope.razor page // and will trigger loading of the Editor Interface async init() { // Load the envelope from the database console.debug('Loading envelope from database..') const envelopeResponse = await this.Network.getEnvelope(this.envelopeKey) - const envelopeError = !!envelopeResponse.error - if (envelopeError) { + if (envelopeResponse.fatal) { return Swal.fire({ title: 'Fehler', text: 'Umschlag konnte nicht geladen werden!', @@ -41,6 +41,14 @@ class App { }) } + if (envelopeResponse.error) { + return Swal.fire({ + title: 'Warnung', + text: 'Umschlag ist nicht mehr verfügbar.', + icon: 'warning', + }) + } + this.currentDocument = envelopeResponse.data.envelope.documents[0] this.currentReceiver = envelopeResponse.data.receiver @@ -50,9 +58,8 @@ class App { this.envelopeKey, this.currentDocument.id ) - const documentError = !!documentResponse.error - if (documentError) { + if (documentResponse.fatal || documentResponse.error) { console.error(documentResponse.error) return Swal.fire({ title: 'Fehler', @@ -85,12 +92,21 @@ class App { console.debug('Loading annotations..') try { + this.signatureCount = this.currentDocument.elements.length const annotations = this.Annotation.createAnnotations( this.currentDocument ) - const createdAnnotations = await this.Instance.create(annotations) + await this.Instance.create(annotations) - await this.Network.openDocument(this.envelopeKey) + const openResponse = await this.Network.openDocument(this.envelopeKey) + + if (openResponse.fatal || openResponse.error) { + return Swal.fire({ + title: 'Fehler', + text: 'Umschlag konnte nicht geöffnet werden!', + icon: 'error', + }) + } } catch (e) { console.error(e) } @@ -169,8 +185,6 @@ class App { if (result == true) { // Redirect to success page after saving to database window.location.href = `/EnvelopeKey/${this.envelopeKey}/Success` - } else { - alert('Fehler beim Abschließen des Dokuments!') } break @@ -181,11 +195,28 @@ class App { } async handleFinish(event) { + + const validationResult = await this.validateAnnotations(this.signatureCount) + if (validationResult === false) { + Swal.fire({ + title: 'Warnung', + text: 'Es wurden nicht alle Signaturfelder ausgefüllt!', + icon: 'warning', + }) + return false + + } + // Save changes before doing anything try { await this.Instance.save() } catch (e) { console.error(e) + Swal.fire({ + title: 'Fehler', + text: 'Umschlag konnte nicht signiert werden!', + icon: 'error', + }) return false } @@ -195,20 +226,64 @@ class App { const postEnvelopeResult = await this.Network.postEnvelope( this.envelopeKey, this.currentDocument.id, - JSON.stringify(json) + json ) - console.log(postEnvelopeResult) - - if (postEnvelopeResult === false) { + if (postEnvelopeResult.fatal) { + Swal.fire({ + title: 'Fehler', + text: 'Umschlag konnte nicht signiert werden!', + icon: 'error', + }) return false } + + if (postEnvelopeResult.error) { + Swal.fire({ + title: 'Warnung', + text: 'Umschlag ist nicht mehr verfügbar.', + icon: 'warning', + }) + return false + } + + return true } catch (e) { console.error(e) return false } + } - return true + + async validateAnnotations(totalSignatures) { + const annotations = await this.Annotation.getAnnotations(this.Instance) + const filtered = annotations + .map(a => a.toJS()) + .filter(a => a.isSignature) + + console.log(filtered.length, "Signatures signed!") + + if (totalSignatures > filtered.length) { + return false + } else { + return true + } + + /*this.Instance.getFormFields().then(formFields => { + formFields.forEach(formField => { + console.log(formField.name, formField.toJS()); + }); + + // Filter form fields by type + formFields.filter(formField => ( + formField instanceof PSPDFKit.FormFields.TextFormField + )); + + // Get the total number of form fields + const totalFormFields = formFields.size; + + console.log(totalFormFields) + })*/ } async handleReset(event) { diff --git a/EnvelopeGenerator.Web/wwwroot/js/network.js b/EnvelopeGenerator.Web/wwwroot/js/network.js index d83e6668..fccdff38 100644 --- a/EnvelopeGenerator.Web/wwwroot/js/network.js +++ b/EnvelopeGenerator.Web/wwwroot/js/network.js @@ -1,107 +1,167 @@ class Network { - getEnvelope(envelopeKey) { - return fetch( - `/api/envelope/${envelopeKey}`, - this.withCSRFToken({ credentials: 'include' }) - ).then(this.wrapJsonResponse.bind(this)) - } - getDocument(envelopeKey, documentId) { - return fetch( - `/api/document/${envelopeKey}?index=${documentId}`, - this.withCSRFToken({ credentials: 'include' }) - ).then(this.wrapBinaryResponse.bind(this)) - } - - postEnvelope(envelopeKey, documentId, jsonString) { - const url = `/api/envelope/${envelopeKey}?index=${documentId}` - const options = { - credentials: 'include', - method: 'POST', - body: jsonString, + /** + * Load envelope json data + * @param {any} envelopeKey + */ + async getEnvelope(envelopeKey) { + console.log("getEnvelope") + return this.getRequest(`/api/envelope/${envelopeKey}`) + .then(this.wrapJsonResponse.bind(this)) } - console.debug('PostEnvelope/Calling url: ' + url) - return fetch(url, this.withCSRFToken(options)) - .then(this.handleResponse) - .then((res) => { - if (!res.ok) { - return false + /** + * Save signature data to server + * @param {any} envelopeKey + * @param {any} documentId + * @param {any} json + */ + async postEnvelope(envelopeKey, documentId, json) { + console.log("postEnvelope") + return this.postRequest(`/api/envelope/${envelopeKey}?index=${documentId}`, json) + .then(this.wrapJsonResponse.bind(this)) + } + + /** + * Load document binary data + * @param {any} envelopeKey + * @param {any} documentId + */ + async getDocument(envelopeKey, documentId) { + console.log("getDocument") + return this.getRequest(`/api/document/${envelopeKey}?index=${documentId}`) + .then(this.wrapBinaryResponse.bind(this)) + } + + /** + * Tell the server that document has been seen + * @param {any} envelopeKey + */ + async openDocument(envelopeKey) { + console.log("openDocument") + return this.postRequest(`/api/document/${envelopeKey}`, {}) + .then(this.wrapJsonResponse.bind(this)) + } + + /** + * Add CSRF Token to request headers + * @param {any} options + * @returns + */ + withCSRFToken(options) { + const token = getCSRFToken + let headers = options.headers + + options.headers = { + ...headers, + ...token } - return true - }) - } - openDocument(envelopeKey) { - const url = `/api/document/${envelopeKey}` - - const options = { - credentials: 'include', - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({}), + return options } - console.debug('OpenDocument/Calling url: ' + url) - return fetch(url, this.withCSRFToken(options)) - .then(this.handleResponse) - .then((res) => { - if (!res.ok) { - return false + /** + * Fetches CSRF Token from page + * @returns + */ + getCSRFToken() { + const token = document.getElementsByName('__RequestVerificationToken')[0].value + return { 'X-XSRF-TOKEN': token } + } + + /** + * Creates a GET HTTP request to `url` + * @param {any} url + */ + getRequest(url) { + const token = this.getCSRFToken() + const options = { + credentials: 'include', + method: 'GET', + headers: { + ...token + } } - return true - }) - } - withCSRFToken(options) { - const token = document.getElementsByName('__RequestVerificationToken')[0] - .value - let headers = options.headers - options.headers = { ...headers, 'X-XSRF-TOKEN': token } - - return options - } - - async wrapJsonResponse(response) { - return await this.wrapResponse(response, async (res) => await res.json()) - } - - async wrapBinaryResponse(response) { - return await this.wrapResponse( - response, - async (res) => await res.arrayBuffer() - ) - } - - async wrapResponse(response, responseHandler) { - let wrappedResponse - - if (response.ok) { - const data = await responseHandler(response) - wrappedResponse = new WrappedResponse(data, null) - } else { - const error = await response.json() - wrappedResponse = new WrappedResponse(null, error) + return fetch(url, options) } - return wrappedResponse - } + /** + * Creates a POST HTTP request for url + * @param {any} url + * @param {any} json + * @returns + */ + postRequest(url, json) { + const token = this.getCSRFToken() + const options = { + credentials: 'include', + method: 'POST', + headers: { + ...token, + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify(json) + } - handleResponse(res) { - if (!res.ok) { - console.log(`Request failed with status ${res.status}`) - return res - } else { - return res + return fetch(url, options) + } + + /** + * Reads and wraps a json response + * @param {any} response + * @returns + */ + async wrapJsonResponse(response) { + return await this.wrapResponse( + response, + async (res) => await res.json()) + } + + /** + * Reads and wraps a binary response + * @param {any} response + * @returns + */ + async wrapBinaryResponse(response) { + return await this.wrapResponse( + response, + async (res) => await res.arrayBuffer()) + } + + /** + * Wraps a fetch response depending on status code + * @param {any} response + * @param {any} responseHandler + * @returns + */ + async wrapResponse(response, responseHandler) { + let wrappedResponse + + console.log("Handling response from", response.url) + console.log("Status", response.status) + console.log(response) + + if (response.status === 200) { + const data = await responseHandler(response) + wrappedResponse = new WrappedResponse(data, null) + } else if (response.status === 403) { + const error = await response.json() + wrappedResponse = new WrappedResponse(null, error) + } else { + wrappedResponse = new WrappedResponse(null, null) + } + + console.log("Wrapped response", wrappedResponse) + + return wrappedResponse } - } } class WrappedResponse { - constructor(data, error) { - this.data = data - this.error = error - } + constructor(data, error) { + this.data = data + this.error = error + this.fatal = (data === null && error === null) + } }