Compare commits

...

11 Commits

20 changed files with 143 additions and 2937 deletions

View File

@@ -1,5 +1,7 @@
using EnvelopeGenerator.Application.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Newtonsoft.Json;
using System.Dynamic;
namespace EnvelopeGenerator.Application.Notifications.DocSigned;
@@ -8,12 +10,24 @@ namespace EnvelopeGenerator.Application.Notifications.DocSigned;
///
/// </summary>
/// <param name="Original"></param>
public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeReceiverDto(Original), INotification
public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeReceiverDto(Original), INotification, ISendMailNotification
{
/// <summary>
///
/// </summary>
public required ExpandoObject Annotations { get; init; }
/// <summary>
///
/// </summary>
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
/// <summary>
///
/// </summary>
public string EmailAddress => Receiver?.EmailAddress
?? throw new InvalidOperationException($"Receiver is null." +
$"DocSignedNotification:\n{JsonConvert.SerializeObject(this, Format.Json.ForDiagnostics)}");
}
/// <summary>

View File

@@ -0,0 +1,50 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Configurations;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Notifications.DocSigned.Handlers;
/// <summary>
///
/// </summary>
public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
{
/// <summary>
///
/// </summary>
/// <param name="tempRepo"></param>
/// <param name="emailOutRepo"></param>
/// <param name="mailParamsOptions"></param>
/// <param name="dispatcherParamsOptions"></param>
public SendSignedMailHandler(IRepository<EmailTemplate> tempRepo, IRepository<EmailOut> emailOutRepo, IOptions<MailParams> mailParamsOptions, IOptions<DispatcherParams> dispatcherParamsOptions) : base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
{
}
/// <summary>
///
/// </summary>
/// <param name="notification"></param>
/// <param name="emailOut"></param>
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
{
emailOut.ReferenceString = notification.EmailAddress;
emailOut.ReferenceId = notification.ReceiverId;
}
/// <summary>
/// </summary>
/// <param name="notification"></param>
/// <returns></returns>
protected override Dictionary<string, string> CreatePlaceHolders(DocSignedNotification notification)
{
var placeHolders = new Dictionary<string, string>()
{
{ "[NAME_RECEIVER]", notification.Name ?? string.Empty },
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
};
return placeHolders;
}
}

View File

@@ -45,10 +45,17 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
/// <summary>
///
/// </summary>
protected virtual Dictionary<string, string> BodyPlaceHolders { get; } = new();
protected abstract Dictionary<string, string> CreatePlaceHolders(TNotification notification);
/// <summary>
///
///{ "[MESSAGE]", notification.Message },<br/>
///{ "[DOCUMENT_ACCESS_CODE]", notification.ReceiverAccessCode },<br/>
///{ "[REASON]", pReason }<br/>
///{ "[NAME_SENDER]", notification.Envelope.User?.FullName},<br/>
///{ "[NAME_PORTAL]", DispatcherParams. },<br/>
///{ "[SIGNATURE_TYPE]", "signieren" },<br/>
///{ "[LINK_TO_DOCUMENT]", notification.SignatureLink },<br/>
///{ "[LINK_TO_DOCUMENT_TEXT]", $"{notification.SignatureLink.Truncate(40)}.." },
/// </summary>
protected readonly MailParams MailParams;
@@ -60,14 +67,9 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
/// <summary>
///
/// </summary>
protected virtual Dictionary<string, string> SubjectPlaceHolders { get; } = new();
/// <summary>
/// ReferenceString = Envelope.Uuid
/// </summary>
/// <param name="notification"></param>
/// <param name="emailOut"></param>
/// <returns></returns>
protected abstract void ConfigEmailOut(EmailOut emailOut);
protected abstract void ConfigureEmailOut(TNotification notification, EmailOut emailOut);
/// <summary>
///
@@ -91,22 +93,24 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task Handle(TNotification notification, CancellationToken cancel)
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
{
var placeHolders = CreatePlaceHolders(notification);
var temp = await TempRepo
.ReadOnly()
.SingleOrDefaultAsync(x => x.Name == notification.TemplateType.ToString(), cancel)
?? throw new InvalidOperationException($"Receiver information is missing in the notification." +
$"{typeof(TNotification)}:\n {JsonConvert.SerializeObject(notification, Format.Json.ForDiagnostics)}");
temp.Subject = ReplacePlaceHolders(temp.Subject, SubjectPlaceHolders, MailParams.Placeholders);
temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, MailParams.Placeholders);
temp.Body = ReplacePlaceHolders(temp.Body, BodyPlaceHolders, MailParams.Placeholders);
temp.Body = ReplacePlaceHolders(temp.Body, placeHolders, MailParams.Placeholders);
var emailOut = new EmailOut
{
EmailAddress = notification.EmailAddress,
EmailBody = temp.Body,
EmailBody = TextToHtml(temp.Body),
EmailSubj = temp.Subject,
AddedWhen = DateTime.UtcNow,
AddedWho = DispatcherParams.AddedWho,
@@ -116,7 +120,7 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
WfId = (int)EnvelopeStatus.MessageConfirmationSent,
};
ConfigEmailOut(emailOut);
ConfigureEmailOut(notification, emailOut);
await EmailOutRepo.CreateAsync(emailOut, cancel);
}
@@ -145,4 +149,4 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
return encoded;
}
}
}

View File

@@ -27,7 +27,7 @@
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.1.1" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.2.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">

View File

@@ -1,17 +0,0 @@
using EnvelopeGenerator.Web.Services;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Web.Controllers;
public class BaseController : Controller
{
protected readonly DatabaseService database;
protected readonly ILogger _logger;
public BaseController(DatabaseService database, ILogger logger)
{
this.database = database;
_logger = logger;
}
}

View File

@@ -1,11 +1,8 @@
using EnvelopeGenerator.CommonServices;
using EnvelopeGenerator.Web.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using EnvelopeGenerator.Application.Extensions;
using EnvelopeGenerator.Application.Interfaces.Services;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Web.Extensions;
using MediatR;
@@ -19,11 +16,8 @@ namespace EnvelopeGenerator.Web.Controllers;
[Authorize(Roles = ReceiverRole.FullyAuth)]
[ApiController]
[Route("api/[controller]")]
public class EnvelopeController : BaseController
public class EnvelopeController : ControllerBase
{
private readonly EnvelopeOldService envelopeService;
private readonly ActionService? actionService;
[Obsolete("Use MediatR")]
private readonly IEnvelopeHistoryService _histService;
[Obsolete("Use MediatR")]
@@ -31,24 +25,24 @@ public class EnvelopeController : BaseController
private readonly IMediator _mediator;
private readonly ILogger<EnvelopeController> _logger;
[Obsolete("Use MediatR")]
public EnvelopeController(DatabaseService database,
EnvelopeOldService envelope,
public EnvelopeController(
ILogger<EnvelopeController> logger,
IEnvelopeHistoryService envelopeHistoryService,
IEnvelopeReceiverService envelopeReceiverService, IMediator mediator) : base(database, logger)
IEnvelopeReceiverService envelopeReceiverService,
IMediator mediator)
{
envelopeService = envelope;
actionService = database?.Services?.actionService;
_histService = envelopeHistoryService;
_envRcvService = envelopeReceiverService;
_mediator = mediator;
_logger = logger;
}
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("{envelopeKey}")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> CreateOrUpdate([FromRoute] string envelopeKey, int index, [FromBody] ExpandoObject annotations, CancellationToken cancel = default)
public async Task<IActionResult> CreateOrUpdate([FromRoute] string envelopeKey, [FromBody] ExpandoObject annotations, CancellationToken cancel = default)
{
// get claims
var signature = User.GetAuthReceiverSignature();
@@ -70,11 +64,7 @@ public class EnvelopeController : BaseController
await _mediator.Publish(notification, cancel);
EnvelopeReceiver response = await envelopeService.LoadEnvelope(envelopeKey);
var signResult = actionService?.SignEnvelope(response.Envelope, ReceiverVM.From(response));
return Ok(new object());
return Ok();
}
[Authorize(Roles = ReceiverRole.FullyAuth)]
@@ -106,7 +96,7 @@ public class EnvelopeController : BaseController
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happend in api/envelope/reject");
_logger.LogNotice(ntc);
return this.ViewInnerServiceError();
return StatusCode(500, mssg);
});
}
}

View File

@@ -1,55 +0,0 @@
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Web.Services;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Web.Controllers.Test;
[Route("api/test/[controller]")]
public class TestViewController : BaseController
{
private readonly EnvelopeOldService envelopeOldService;
private readonly IConfiguration _config;
public TestViewController(DatabaseService databaseService, EnvelopeOldService envelopeOldService, ILogger<TestViewController> logger, IConfiguration configuration) : base(databaseService, logger)
{
this.envelopeOldService = envelopeOldService;
_config = configuration;
}
[HttpGet]
public IActionResult Index()
{
return View("AnnotationIndex");
}
[HttpPost]
public IActionResult DebugEnvelopes([FromForm] string? password)
{
try
{
var passwordFromConfig = _config["AdminPassword"];
if (passwordFromConfig == null)
{
ViewData["error"] = "No admin password configured!";
return View("AnnotationIndex");
}
if (password != passwordFromConfig)
{
ViewData["error"] = "Wrong Password!";
return View("AnnotationIndex");
}
List<Envelope> envelopes = envelopeOldService.LoadEnvelopes();
return View("DebugEnvelopes", envelopes);
}
catch(Exception ex)
{
_logger.LogError(ex, "Unexpected error");
ViewData["error"] = "Unknown error!";
return View("AnnotationIndex");
}
}
}

View File

@@ -41,8 +41,6 @@
<ItemGroup>
<None Include="bundleconfig.json" />
<None Include="wwwroot\lib\bootstrap-icons\bootstrap-icons.svg" />
<None Include="wwwroot\lib\bootstrap-icons\font\bootstrap-icons.scss" />
<None Include="wwwroot\lib\bootstrap-icons\font\fonts\bootstrap-icons.woff2" />
<None Include="wwwroot\lib\bootstrap-icons\icons\0-circle-fill.svg" />
<None Include="wwwroot\lib\bootstrap-icons\icons\0-circle.svg" />
@@ -2094,8 +2092,6 @@
<None Include="wwwroot\lib\bootstrap-icons\icons\youtube.svg" />
<None Include="wwwroot\lib\bootstrap-icons\icons\zoom-in.svg" />
<None Include="wwwroot\lib\bootstrap-icons\icons\zoom-out.svg" />
<None Include="wwwroot\lib\bootstrap-icons\LICENSE" />
<None Include="wwwroot\lib\bootstrap-icons\README.md" />
</ItemGroup>
<ItemGroup>
@@ -2104,9 +2100,6 @@
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="DigitalData.Modules.Base" Version="1.3.8" />
<PackageReference Include="DigitalData.Modules.Config" Version="1.3.0" />
<PackageReference Include="DigitalData.Modules.Database" Version="2.3.5.4" />
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
@@ -2135,7 +2128,6 @@
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
</ItemGroup>

View File

@@ -1,5 +1,4 @@
using EnvelopeGenerator.Application.Services;
using EnvelopeGenerator.Web.Services;
using Microsoft.EntityFrameworkCore;
using NLog;
using Quartz;
@@ -50,12 +49,6 @@ try
});
});
// Add base services
builder.Services.AddScoped<DatabaseService>();
// Add higher order services
builder.Services.AddScoped<EnvelopeOldService>();
builder.Services.AddHttpContextAccessor();
builder.ConfigureBySection<TFARegParams>();

View File

@@ -1,93 +0,0 @@
using DigitalData.Modules.Database;
using DigitalData.Modules.Logging;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.CommonServices;
namespace EnvelopeGenerator.Web.Services
{
public class DatabaseService
{
public MSSQLServer MSSQL { get; set; }
ILogger<DatabaseService> _logger;
public State? State { get; set; }
public class ServiceContainer
{
public ActionService actionService;
public EmailService emailService;
public ServiceContainer(State state, MSSQLServer MSSQL)
{
actionService = new(state, MSSQL);
emailService = new(state);
}
}
public class ModelContainer
{
public EnvelopeModel envelopeModel;
public DocumentModel documentModel;
public ReceiverModel receiverModel;
public ElementModel elementModel;
public HistoryModel historyModel;
public DocumentStatusModel documentStatusModel;
public EmailModel emailModel;
public ConfigModel configModel;
public ModelContainer(State state)
{
envelopeModel = new(state);
documentModel = new(state);
receiverModel = new(state);
elementModel = new(state);
historyModel = new(state);
documentStatusModel = new(state);
emailModel = new(state);
configModel = new(state);
}
}
public readonly ModelContainer? Models;
public readonly ServiceContainer? Services;
public DatabaseService(ILogger<DatabaseService> logger, IConfiguration config)
{
LogConfig logConfig = new LogConfig(LogConfig.PathType.CustomPath, config["NLog:variables:logDirectory"], null, "Digital Data", "ECM.EnvelopeGenerator.Web");
_logger = logger;
_logger.LogInformation("Establishing MSSQL Database connection..");
MSSQL = new MSSQLServer(logConfig, config.GetConnectionString("Default"));
if (MSSQL.DBInitialized == true)
{
_logger.LogInformation("MSSQL Connection established: [{0}]", MSSQL.MaskedConnectionString);
/// <summary>
/// There is a circular dependency between state and models
/// All models need a state object, including the config Model
/// The state object needs to be filled with the DbConfig property,
/// which is obtained by the config Model.
/// So first, the config model is initialized with an incomplete state object,
/// then all the other models with the DbConfig property filled.
/// </summary>
State = new State
{
Database = MSSQL,
LogConfig = logConfig,
UserId = 0,
DbConfig = null
};
var configModel = new ConfigModel(State);
State.DbConfig = configModel.LoadConfiguration();
Models = new(State);
Services = new(State, MSSQL);
}
else
{
_logger.LogInformation("Connection could not be established!");
}
}
}
}

View File

@@ -1,167 +0,0 @@
using EnvelopeGenerator.Application.Interfaces.Services;
using EnvelopeGenerator.CommonServices;
using EnvelopeGenerator.Domain.Entities;
using System.Text;
namespace EnvelopeGenerator.Web.Services;
public class EnvelopeOldService
{
private readonly ReceiverModel receiverModel;
private readonly EnvelopeModel envelopeModel;
private readonly HistoryModel historyModel;
private readonly DocumentStatusModel documentStatusModel;
[Obsolete("Use MediatR")]
private readonly IConfigService _configService;
private readonly ILogger<EnvelopeOldService> _logger;
[Obsolete("Use MediatR")]
public EnvelopeOldService(DatabaseService database, IConfigService configService, ILogger<EnvelopeOldService> logger)
{
_logger = logger;
if (database.Models is null)
throw new ArgumentNullException("Models not loaded.");
receiverModel = database.Models.receiverModel;
envelopeModel = database.Models.envelopeModel;
historyModel = database.Models.historyModel;
documentStatusModel = database.Models.documentStatusModel;
_configService = configService;
}
[Obsolete("Use MediatR")]
public async Task<EnvelopeReceiver> LoadEnvelope(string pEnvelopeKey)
{
_logger.LogInformation("Loading Envelope by Key [{0}]", pEnvelopeKey);
Tuple<string, string> result = Helpers.DecodeEnvelopeReceiverId(pEnvelopeKey);
var envelopeUuid = result.Item1;
var receiverSignature = result.Item2;
var receiverId = receiverModel.GetReceiverIdBySignature(receiverSignature);
_logger.LogInformation("Resolved receiver signature to receiverId [{0}]", receiverId);
_logger.LogInformation("Loading envelope..");
Envelope? envelope = envelopeModel.GetByUuid(envelopeUuid);
if (envelope == null)
{
_logger.LogWarning("Envelope not found");
throw new NullReferenceException("Envelope not found");
}
_logger.LogInformation("Envelope loaded");
if (envelope.Receivers == null)
{
_logger.LogWarning("Receivers for envelope not loaded");
throw new NullReferenceException("Receivers for envelope not loaded");
}
_logger.LogInformation("Envelope receivers found: [{0}]", envelope.Receivers.Count);
Receiver? receiver = envelope.Receivers.Where(r => r.ReceiverId == receiverId).SingleOrDefault()?.Receiver;
if (receiver == null)
{
_logger.LogWarning("Receiver [{0}] not found", receiverId);
throw new NullReferenceException("Receiver not found");
}
_logger.LogInformation("Loading documents for receiver [{0}]", receiver.EmailAddress);
// filter elements by receiver
envelope.Documents = envelope.Documents.Select((document) =>
{
document.Elements = document.Elements.Where((e) => e.ReceiverId == receiverId).ToList();
return document;
}).ToList();
//if documenet_path_dmz is existing in config, replace the path with it
var config = await _configService.ReadDefaultAsync();
return new()
{
Receiver = receiver,
Envelope = envelope
};
}
public List<Envelope> LoadEnvelopes()
{
var receivers = receiverModel.ListReceivers();
List<Envelope> envelopes = new();
foreach (var receiver in receivers)
{
var envs = (List<Envelope>)envelopeModel.List(receiver.Id);
envelopes.AddRange(envs);
}
return envelopes;
}
public bool ReceiverAlreadySigned(Envelope envelope, int receiverId)
{
return historyModel.HasReceiverSigned(envelope.Id, receiverId);
}
public async Task<string?> EnsureValidAnnotationData(HttpRequest request)
{
try
{
_logger.LogInformation("Parsing annotation data from request..");
using MemoryStream ms = new();
await request.BodyReader.CopyToAsync(ms);
var bytes = ms.ToArray();
_logger.LogInformation("Annotation data parsed, size: [{0}]", bytes.Length);
return Encoding.UTF8.GetString(bytes);
}
catch (Exception e)
{
_logger.LogError(e, "Inner Service Error");
throw new ArgumentNullException("AnnotationData");
}
}
[Obsolete("Use MediatR")]
public async Task<EnvelopeDocument> GetDocument(int documentId, string envelopeKey)
{
EnvelopeReceiver response = await LoadEnvelope(envelopeKey);
_logger.LogInformation("Loading document for Id [{0}]", documentId);
var document = response.Envelope.Documents.
Where(d => d.Id == documentId).
FirstOrDefault();
if (document == null)
throw new ArgumentException("DocumentId");
_logger.LogInformation("Document [{0}] loaded!", documentId);
return document;
}
public bool InsertDocumentStatus(DocumentStatus documentStatus)
{
_logger.LogInformation("Saving annotation data..");
return documentStatusModel.InsertOrUpdate(documentStatus);
}
public async Task<byte[]> GetDocumentContents(EnvelopeDocument document)
{
_logger.LogInformation("Loading file [{0}]", document.Filepath);
var bytes = await File.ReadAllBytesAsync(document.Filepath);
_logger.LogInformation("File loaded, size: [{0}]", bytes.Length);
return bytes;
}
}

View File

@@ -1,6 +1,6 @@
@using EnvelopeGenerator.CommonServices;
@using EnvelopeGenerator.Domain.Entities;
@using EnvelopeGenerator.Domain.Entities;
@using EnvelopeGenerator.Domain.Constants;
@using EnvelopeGenerator.Application.Extensions;
@{
ViewData["Title"] = "Debug";
}
@@ -9,7 +9,7 @@
string encodeEnvelopeKey(Envelope envelope)
{
var receiver = envelope.Receivers!.First();
return Helpers.EncodeEnvelopeReceiverId(envelope.Uuid, receiver.Receiver!.Signature);
return (envelope.Uuid, receiver.Receiver!.Signature).ToEnvelopeKey();
}
IEnumerable<IGrouping<EnvelopeStatus, Envelope>> groupEnvelopes(List<Envelope> envelopes)

View File

@@ -1,74 +0,0 @@
# 📄 PSPDFKit-Integration Vanilla JavaScript
Dieses Projekt zeigt, wie die PDF-Anzeige- und Signaturbibliothek [PSPDFKit](https://www.nutrient.io/sdk/web/getting-started/other-frameworks/javascript/) mithilfe von **Vanilla JavaScript** integriert werden kann.
## 🚀 Verwendungszweck
PSPDFKit wurde in der Webanwendung verwendet, um PDF-Dokumente:
- anzuzeigen,
- zu signieren,
- mit Anmerkungen zu versehen,
- Formularfelder auszufüllen.
Benutzer können Dokumente **direkt über den Browser signieren und versenden**.
---
## 🔧 Wo und wie wurde PSPDFKit verwendet?
### 1. PSPDFKit laden
PSPDFKit wurde mit der Funktion `loadPSPDFKit` in `UI.js` gestartet:
```js
PSPDFKit.load({
container: #app,
document: arrayBuffer,
licenseKey: YOUR_LICENSE_KEY,
...
})
```
### 2. Anmerkungen und Formularfelder
In `annotation.js` werden die folgenden Felder dynamisch zu PDF-Dokumenten hinzugefügt:
- **Signaturfeld** (`SignatureFormField`)
- **Position** (`TextFormField`)
- **Stadt** (`TextFormField`)
- **Datum** (`TextFormField`, wird automatisch mit dem heutigen Datum ausgefüllt)
- **Bezeichnungen** (`Ort`, `Position`, `Date`) dies sind nur lesbare Textfelder.
- **Rahmen für Signaturbild** wird dynamisch erstellt und platziert, wenn der Benutzer unterschreibt.
Die Felder werden entsprechend ihrer Position auf dem PDF berechnet und für jedes Feld wird eine eindeutige ID erstellt.
---
### 3. Anpassungen der Symbolleiste
In `UI.js` ist die Standard-Symbolleiste (`toolbarItems`) von PSPDFKit konfiguriert:
- Es werden nur zulässige Elemente (`sidebar`, `zoom`, `pager`, `search` usw.) angezeigt.
- Je nach Benutzerberechtigung (z. B. schreibgeschützt oder beschreibbar) werden die folgenden speziellen Schaltflächen dynamisch hinzugefügt:
**Beschreibbarer Modus:**
- `Teilen` (SHARE)
- `Logout` (LOGOUT)
- `Zurücksetzen` (RESET)
- `Ablehnen` (REJECT)
- `Finalisieren` (FINISH)
**Nur-Lesemodus:**
- Die Schaltfläche `Teilen` kopiert nur den Link in die Zwischenablage (COPY_URL)
Für mobile Geräte werden zusätzlich vereinfachte Schaltflächen angezeigt.
---
### 📁 Wichtige Dateien
| Datei | Beschreibung |
|-------------------|-----------------------------------------------|
| `app.js` | Hauptanwendungsklasse, Dokumentladen und -steuerung |
| `ui.js` | PSPDFKit-UI-Einrichtung und Symbolleistenverwaltung |
| `annotation.js` | Erstellen und Löschen von Anmerkungsfeldern |

View File

@@ -13,9 +13,6 @@ class App {
constructor(envelopeKey, envelopeReceiver, documentBytes, licenseKey, locale, container) {
this.container = container ?? `#${this.constructor.name.toLowerCase()}`;
this.envelopeKey = envelopeKey
this.Network = new Network()
this.Instance = null
this.currentDocument = null
this.currentReceiver = null
@@ -275,35 +272,30 @@ class App {
// Export annotation data and save to database
try {
const json = await iJSON
const postEnvelopeResult = await this.Network.postEnvelope(
this.currentDocument.id,
json
)
const res = await postEnvelope(this.envelopeKey, await iJSON);
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
if (!res.ok) {
if (res.status === 403) {
Swal.fire({
title: 'Warnung',
text: 'Umschlag ist nicht mehr verfügbar.',
icon: 'warning',
})
return false
}
else {
throw new Error()
}
} else
return true
} catch (e) {
Swal.fire({
title: 'Fehler',
text: 'Umschlag konnte nicht signiert werden!',
icon: 'error',
})
return false
}
//---
}
else
return false;

View File

@@ -1,150 +1,30 @@
class Network {
/**
* Load envelope json data
* @param {any} envelopeKey
*/
async getEnvelope(envelopeKey) {
return this.getRequest(`/api/envelope/${envelopeKey}`)
.then(this.wrapJsonResponse.bind(this))
}
/**
* Save signature data to server
* @param {any} envelopeKey
* @param {any} documentId
* @param {any} json
*/
async postEnvelope(documentId, json) {
return this.postRequest(`/api/envelope?index=${documentId}`, json)
.then(this.wrapJsonResponse.bind(this))
}
/**
* Load document binary data
* @param {any} envelopeKey
* @param {any} documentId
*/
async getDocument(envelopeKey, documentId) {
return this.getRequest(`/api/document/${envelopeKey}?index=${documentId}`)
.then(this.wrapBinaryResponse.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 options
}
/**
* 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 fetch(url, options)
}
/**
* 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)
}
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
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)
}
return wrappedResponse
}
/**
* Fetches CSRF Token from page
* @returns
*/
function getCSRFToken() {
const token = document.getElementsByName('__RequestVerificationToken')[0].value
return { 'X-XSRF-TOKEN': token }
}
class WrappedResponse {
constructor(data, error) {
this.data = data
this.error = error
this.fatal = (data === null && error === null)
/**
* Save signature data to server
* @param {any} envelopeKey
* @param {any} annotations
*/
function postEnvelope(envelopeKey, annotations) {
const token = getCSRFToken()
const options = {
credentials: 'include',
method: 'POST',
headers: {
...token,
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify(annotations)
}
return fetch(`/api/envelope/${envelopeKey}`, options)
}
async function setLangAsync(language, flagCode) {

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2019-2024 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,100 +0,0 @@
<p align="center">
<a href="https://getbootstrap.com/">
<img src="https://getbootstrap.com/docs/5.2/assets/brand/bootstrap-logo-shadow.png" alt="Bootstrap logo" width="200" height="165">
</a>
</p>
<h3 align="center">Bootstrap Icons</h3>
<p align="center">
Official open source SVG icon library for Bootstrap with over 2,000 icons.
<br>
<a href="https://icons.getbootstrap.com/"><strong>Explore Bootstrap Icons »</strong></a>
<br>
<br>
<a href="https://getbootstrap.com/">Bootstrap</a>
·
<a href="https://themes.getbootstrap.com/">Themes</a>
·
<a href="https://blog.getbootstrap.com/">Blog</a>
<br>
</p>
[![Bootstrap Icons preview](https://github.com/twbs/icons/blob/main/.github/preview.png)](https://icons.getbootstrap.com/)
## Install
Bootstrap Icons are packaged up and published to npm. We only include the processed SVGs in this package—it's up to you and your team to implement. [Read our docs](https://icons.getbootstrap.com/) for usage instructions.
```shell
npm i bootstrap-icons
```
For those [using Packagist](https://packagist.org/packages/twbs/bootstrap-icons), you can also install Bootstrap Icons via Composer:
```shell
composer require twbs/bootstrap-icons
```
[Also available in Figma](https://www.figma.com/community/file/1042482994486402696/Bootstrap-Icons).
## Usage
Depending on your setup, you can include Bootstrap Icons in a handful of ways.
- Copy-paste SVGs as embedded HTML
- Reference via `<img>` element
- Use the SVG sprite
- Include via CSS
[See the docs for more information](https://icons.getbootstrap.com/#usage).
## Development
[![Build Status](https://img.shields.io/github/actions/workflow/status/twbs/icons/test.yml?branch=main&label=Tests&logo=github)](https://github.com/twbs/icons/actions/workflows/test.yml?query=workflow%3ATests+branch%3Amain)
[![npm version](https://img.shields.io/npm/v/bootstrap-icons?logo=npm&logoColor=fff)](https://www.npmjs.com/package/bootstrap-icons)
Clone the repo, install dependencies, and start the Hugo server locally.
```shell
git clone https://github.com/twbs/icons/
cd icons
npm i
npm start
```
Then open `http://localhost:4000` in your browser.
### npm scripts
Here are some key scripts you'll use during development. Be sure to look to our `package.json` or `npm run` output for a complete list of scripts.
| Script | Description |
|--------------|-------------------------------------------------------------------------------|
| `start` | Alias for running `docs-serve` |
| `docs-serve` | Starts a local Hugo server |
| `pages` | Generates permalink pages for each icon with template Markdown |
| `icons` | Processes and optimizes SVGs in `icons` directory, generates fonts and sprite |
## Adding SVGs
Icons are typically only added by @mdo, but exceptions can be made. New glyphs are designed in Figma first on a 16x16px grid, then exported as flattened SVGs with `fill` (no stroke). Once a new SVG icon has been added to the `icons` directory, we use an npm script to:
1. Optimize our SVGs with SVGO.
2. Modify the SVGs source code, removing all attributes before setting new attributes and values in our preferred order.
Use `npm run icons` to run the script, run `npm run pages` to build permalink pages, complete those pages, and, finally, commit the results in a new branch for updating.
**Warning**: Please exclude any auto-generated files, like `font/**` and `bootstrap-icons.svg` from your branch because they cause conflicts, and we generally update the dist files before a release.
## Publishing
Documentation is published automatically when a new Git tag is published. See our [GitHub Actions](https://github.com/twbs/icons/tree/main/.github/workflows) and [`package.json`](https://github.com/twbs/icons/blob/main/package.json) for more information.
## License
[MIT](LICENSE)
## Author
[@mdo](https://github.com/mdo)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.0 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +0,0 @@
{
"name": "bootstrap-icons",
"version": "1.11.3",
"description": "Official open source SVG icon library for Bootstrap",
"author": "mdo",
"license": "MIT",
"homepage": "https://icons.getbootstrap.com/",
"repository": {
"type": "git",
"url": "git+https://github.com/twbs/icons.git"
},
"bugs": {
"url": "https://github.com/twbs/icons/issues"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"keywords": [
"bootstrap",
"icons",
"svg",
"font",
"sprite",
"woff",
"woff2"
],
"style": "font/bootstrap-icons.css",
"sass": "font/bootstrap-icons.scss",
"files": [
"icons/*.svg",
"bootstrap-icons.svg",
"font",
"!.DS_Store"
],
"hugo-bin": {
"buildTags": "extended"
},
"scripts": {
"start": "npm run docs-serve",
"docs-serve": "hugo server --port 4000 --disableFastRender",
"docs-build": "hugo --cleanDestinationDir --printUnusedTemplates",
"docs-test": "npm-run-all docs-build docs-test:vnu",
"docs-test:vnu": "node build/vnu-jar.mjs",
"pages": "node build/build-pages.mjs",
"icons": "npm-run-all icons-main --aggregate-output --parallel icons-sprite icons-font",
"icons-main": "node build/build-svgs.mjs",
"icons-zip": "cross-env-shell \"rm -rf bootstrap-icons-$npm_package_version bootstrap-icons-$npm_package_version.zip && cp -r icons/ bootstrap-icons-$npm_package_version && cp bootstrap-icons.svg bootstrap-icons-$npm_package_version && cp -r font/ bootstrap-icons-$npm_package_version && zip -qr9 bootstrap-icons-$npm_package_version.zip bootstrap-icons-$npm_package_version && rm -rf bootstrap-icons-$npm_package_version\"",
"icons-sprite": "svg-sprite --config svg-sprite.json --log=info \"icons/*.svg\"",
"icons-font": "npm-run-all icons-font-*",
"icons-font-main": "fantasticon",
"icons-font-min": "cleancss -O1 --format breakWith=lf --with-rebase --output font/bootstrap-icons.min.css font/bootstrap-icons.css",
"release": "npm-run-all icons docs-build icons-zip",
"release-version": "node build/bump-version.mjs",
"netlify": "cross-env-shell HUGO_BASEURL=$DEPLOY_PRIME_URL npm-run-all icons docs-build",
"test:fusv": "fusv docs/assets/scss/",
"test:eslint": "eslint --cache --cache-location .cache/.eslintcache --report-unused-disable-directives --ext .js,.mjs .",
"test:stylelint": "stylelint docs/assets/scss/ --cache --cache-location .cache/.stylelintcache",
"test:lockfile-lint": "lockfile-lint --allowed-hosts npm --allowed-schemes https: --empty-hostname false --type npm --path package-lock.json",
"test:check-icons": "node build/check-icons.mjs",
"test": "npm-run-all --parallel --aggregate-output --continue-on-error test:*"
},
"devDependencies": {
"@twbs/fantasticon": "^2.7.2",
"autoprefixer": "^10.4.16",
"bootstrap": "^5.3.2",
"clean-css-cli": "^5.6.3",
"clipboard": "^2.0.11",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"find-unused-sass-variables": "^5.0.0",
"fuse.js": "^7.0.0",
"hugo-bin": "^0.118.0",
"lockfile-lint": "^4.12.1",
"npm-run-all2": "^6.1.1",
"picocolors": "^1.0.0",
"postcss": "^8.4.32",
"postcss-cli": "^11.0.0",
"stylelint": "^16.1.0",
"stylelint-config-twbs-bootstrap": "^13.0.0",
"svg-sprite": "^3.0.0-beta3",
"svgo": "^3.2.0",
"vnu-jar": "23.4.11"
}
}