Compare commits

25 Commits

Author SHA1 Message Date
d39018ca39 Add TfaRegistrationController for receiver TFA endpoints
Introduced TfaRegistrationController with endpoints to register and manage two-factor authentication for envelope receivers. Includes a GET endpoint to generate TFA registration metadata (QR code and deadline) and a POST endpoint to log out receivers. Implements error handling, logging, and uses dependency injection for required services. Added necessary using directives.
2026-01-30 15:05:32 +01:00
b49482137f Add ReadOnlyController for envelope sharing flows
Introduced ReadOnlyController to manage read-only envelope sharing. Added a POST endpoint for authorized users to create read-only receivers, send notification emails, and record sharing events in envelope history. Includes error handling and logging throughout the process.
2026-01-30 15:04:05 +01:00
bd40404d97 Add DocumentController for envelope document retrieval
Introduced DocumentController to provide a secured GET endpoint for authenticated receivers to download envelope documents. Handles missing or empty documents with error logging and NotFoundException. Utilizes MediatR and ILogger via dependency injection.
2026-01-30 14:48:10 +01:00
6f16921a79 Remove Obsolete attributes and update FirstAsync behavior
Removed Obsolete attributes from FirstAsync and Exceptions class. Changed FirstAsync return type to non-nullable Task<T> and updated its logic to throw the provided exception when the sequence is empty, instead of returning null.
2026-01-30 14:41:54 +01:00
1afc95f9c6 Add obsolete FirstAsync extension to TaskExtensions
Added FirstAsync<T, TException> as an obsolete extension method for Task<IEnumerable<T>>. This method returns the first element or throws a custom exception if the result is null, using a provided factory delegate. Intended for legacy .NET projects.
2026-01-30 14:24:57 +01:00
6aed820196 Mark TaskExtensions and new helpers as [Obsolete]
Marked TaskExtensions class and all its methods as [Obsolete] with guidance to implement Mediator behaviors instead. Added new [Obsolete] extension methods for null/empty checks and chaining. Introduced an [Obsolete] Exceptions class with factory methods for common exceptions. All changes are intended for legacy or transitional use only.
2026-01-30 14:20:05 +01:00
e17c4d02f8 Update Annotation model import to PsPdfKitAnnotation
Changed the import in ConfigController to use the PsPdfKitAnnotation namespace for annotation models instead of the previous Annotation namespace. This ensures the controller works with the updated annotation model definitions.
2026-01-30 13:07:00 +01:00
8187924a8c Add EnvelopeAuthExtensions for envelope claim handling
Introduces EnvelopeAuthExtensions with helper methods to retrieve envelope-specific claims from ClaimsPrincipal and to sign in envelope receivers using cookie authentication. Supports extracting envelope and receiver details via claims for authentication flows.
2026-01-30 13:06:40 +01:00
1bf530f7e7 Add EnvelopeClaimTypes for custom envelope claim strings
Introduced EnvelopeGenerator.GeneratorAPI namespace and EnvelopeClaimTypes static class. Added Title and Id claim type constants for envelope-related information, with XML documentation for clarity.
2026-01-30 13:06:27 +01:00
9cadc8e901 Add PSPDFKit annotation model and utilities
Introduce classes and interfaces for modeling PDF annotations, including support for layout, relative positioning, background rendering, and color. Added Annotation, AnnotationParams, Background, Color, Extensions, and IAnnotation to EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation. Enables flexible annotation management and rendering.
2026-01-30 13:02:34 +01:00
1d4ad13532 Add core model classes for auth, culture, images, and links
Introduced new models in EnvelopeGenerator.GeneratorAPI.Models:
- Auth, ContactLink, Culture, Cultures, CustomImages, ErrorViewModel, Image, MainViewModel, and TFARegParams.
These provide foundational structures for authentication, localization, error handling, image management, and contact links. All changes are new file additions.
2026-01-30 12:55:44 +01:00
03a8154b1c Add ConfigController to expose annotation config via API
Introduced a secured ConfigController with a GET endpoint at /api/Config/Annotations to provide annotation configuration data to clients. Utilizes dependency injection for configuration and includes necessary using directives.
2026-01-30 09:43:26 +01:00
20b8acd3fc Add AnnotationController for envelope annotation workflow
Introduces AnnotationController to manage envelope annotations and signature lifecycle. Includes endpoints for creating/updating annotations (for PSPDF Kit, obsolete) and rejecting documents, both requiring "FullyAuth" role. Utilizes MediatR for CQRS operations, dependency injection, and provides detailed logging and error handling. Legacy service dependencies are marked as obsolete.
2026-01-30 09:42:29 +01:00
a3afeb175f Refactor controllers for MediatR and cleaner API design
- Switch EnvelopeController and ReceiverController to MediatR for all operations
- Encapsulate UserId in CreateEnvelopeCommand via Authorize() method
- Change CreateEnvelopeCommand binding to [FromBody]
- Add CancellationToken support to EnvelopeReceiverController
- Remove obsolete CRUD logic from ReceiverController; now only supports GET via MediatR
- Clean up unused dependencies and update controller summaries for clarity
2026-01-28 14:14:04 +01:00
114555c843 Add XML docs to ReadEnvelopeReceiverQueryHandler ctor
Added XML documentation comments to the constructor of the ReadEnvelopeReceiverQueryHandler class, including parameter descriptions for envelopeReceiver, rcvRepo, and mapper. The summary section is currently empty.
2026-01-28 14:10:03 +01:00
f294ef2fde Refactor EnvelopeTypeController to use MediatR
Replace IEnvelopeTypeService with IMediator in EnvelopeTypeController and update GetAllAsync to use ReadEnvelopeTypesQuery. Remove obsolete service-based code and attributes. Also, remove AsNoTracking() from repository query in ReadEnvelopeTypesQueryHandler.
2026-01-28 13:48:02 +01:00
02ad819da9 Refactor EnvelopeReceiverController to use MediatR only
Remove all usage of IEnvelopeReceiverService from EnvelopeReceiverController, including constructor injection and obsolete attributes. Update endpoints to exclusively use MediatR for handling queries. Clean up related obsolete code, comments, and unused usings. Update documentation to reflect these changes.
2026-01-28 13:43:34 +01:00
041d98ca78 Refactor EnvelopeController to use MediatR exclusively
Removed all usage of the obsolete IEnvelopeService from EnvelopeController, refactoring endpoints to use MediatR with ReadEnvelopeQuery. Simplified GetAsync and GetDocResultAsync logic, updated method signatures, and eliminated obsolete code paths and attributes. Cleaned up unused usings and updated constructor dependencies.
2026-01-28 13:41:43 +01:00
afea2fb5ea Refactor envelope queries into unified ReadEnvelopeQuery
Consolidate envelope querying logic by extending ReadEnvelopeQuery to support user filtering and status options, and introduce ReadEnvelopeQueryHandler to process all envelope queries. Remove ReadUserEnvelopesQuery and its handler, reducing duplication and improving maintainability.
2026-01-28 13:34:54 +01:00
beeb9e4e75 Refactor EmailTemplateController to use generic IRepository
Replaces IEmailTemplateRepository with IRepository<EmailTemplate> in EmailTemplateController. Removes unused ILogger dependency and updates the Get method to use the new repository interface. Cleans up obsolete attributes and using directives.
2026-01-28 13:13:37 +01:00
30d13b1ffb Add MediatR query handler for reading receiver details
Introduced a CQRS-style ReadReceiverQueryHandler using MediatR to enable flexible querying of receivers by Id, EmailAddress, or Signature. Integrated AutoMapper for DTO mapping and updated ReadReceiverQuery to support MediatR's request/response pipeline.
2026-01-28 13:09:51 +01:00
814df63306 Add MediatR query/handler for reading envelope types
Introduced ReadEnvelopeTypesQuery and its handler to fetch all
envelope types from the repository, map them to DTOs, and
return the result via MediatR. This enables clean, decoupled
retrieval of envelope types in the application.
2026-01-28 12:55:44 +01:00
830d1af44a Add ReadUserEnvelopesQuery and handler with filtering
Implemented ReadUserEnvelopesQuery and its handler to fetch user envelopes with support for filtering by user ID, status (range, include, ignore), envelope ID, and UUID. Utilizes repository and AutoMapper to return EnvelopeDto results.
2026-01-28 12:54:22 +01:00
94018d2a36 Add query handler to fetch receiver's latest used name
Introduced ReadReceiverNameQueryHandler to retrieve the most recently used name ("Anrede") for a receiver, supporting envelope generation. Updated ReadReceiverNameQuery to use MediatR and CQRS patterns, and implemented repository-based lookups for receiver identification and name retrieval. Added necessary using directives for repository and MediatR support.
2026-01-28 12:51:40 +01:00
cf5a724bf2 Add username filter to ReadEnvelopeReceiverQuery handler
Extended ReadEnvelopeReceiverQuery to support optional username-based filtering. Refactored the handler to apply this filter, updated repository access for consistency, and improved code clarity and null-safety. Cleaned up comments and formatting.
2026-01-28 12:42:21 +01:00
34 changed files with 1354 additions and 290 deletions

View File

@@ -5,6 +5,7 @@ namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
/// Extension methods for tasks
/// </summary>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static class TaskExtensions
{
/// <summary>
@@ -17,6 +18,7 @@ public static class TaskExtensions
/// <param name="factory">Exception provider</param>
/// <returns>The awaited result if not <c>null</c>.</returns>
/// <exception>Thrown if the result is <c>null</c>.</exception>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<T> ThrowIfNull<T, TException>(this Task<T?> task, Func<TException> factory) where TException : Exception
{
var result = await task;
@@ -33,6 +35,7 @@ public static class TaskExtensions
/// <param name="factory">Exception provider</param>
/// <returns>The awaited collection if it is not <c>null</c> or empty.</returns>
/// <exception cref="NotFoundException">Thrown if the result is <c>null</c> or empty.</exception>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<IEnumerable<T>> ThrowIfEmpty<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory) where TException : Exception
{
var result = await task;
@@ -47,11 +50,33 @@ public static class TaskExtensions
/// <param name="task"></param>
/// <param name="act"></param>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<I> Then<T, I>(this Task<T> task, Func<T, I> act)
{
var res = await task;
return act(res);
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="task"></param>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task) => task.Then(t => t.FirstOrDefault());
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TException"></typeparam>
/// <param name="task"></param>
/// <param name="factory"></param>
/// <returns></returns>
public static Task<T> FirstAsync<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory)
where TException : Exception
=> task.Then(t => t.FirstOrDefault() ?? throw factory());
}
/// <summary>
@@ -68,11 +93,13 @@ public static class Exceptions
///
/// </summary>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static BadRequestException BadRequest() => new();
/// <summary>
///
/// </summary>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static ForbiddenException Forbidden() => new();
}

View File

@@ -1,4 +1,4 @@
using AutoMapper;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Envelopes.Queries;
@@ -47,7 +47,13 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
/// Die Antwort enthält Details wie den Include, die Zuordnung zwischen Umschlag und Empfänger
/// sowie zusätzliche Metadaten.
/// </remarks>
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>;
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>
{
/// <summary>
/// Optionaler Benutzernamefilter, um Ergebnisse auf Umschläge eines bestimmten Besitzers einzuschränken.
/// </summary>
public string? Username { get; init; }
}
/// <summary>
///
@@ -82,73 +88,74 @@ public static class Extensions
q.Receiver.Signature = signature;
return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
}
}
/// <summary>
///
/// </summary>
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
{
private readonly IRepository<EnvelopeReceiver> _repo;
private readonly IRepository<Receiver> _rcvRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// Verarbeitet <see cref="ReadEnvelopeReceiverQuery"/> und liefert passende <see cref="EnvelopeReceiverDto"/>-Ergebnisse.
/// </summary>
/// <param name="envelopeReceiver"></param>
/// <param name="mapper"></param>
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
{
_repo = envelopeReceiver;
_mapper = mapper;
_rcvRepo = rcvRepo;
}
private readonly IRepository<EnvelopeReceiver> _repo;
private readonly IRepository<Receiver> _rcvRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
{
var q = _repo.ReadOnly().Where(request, notnull: false);
if (request.Envelope.Status is not null)
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiver"></param>
/// <param name="rcvRepo"></param>
/// <param name="mapper"></param>
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
{
var status = request.Envelope.Status;
if (status.Min is not null)
q = q.Where(er => er.Envelope!.Status >= status.Min);
if (status.Max is not null)
q = q.Where(er => er.Envelope!.Status <= status.Max);
if (status.Include?.Length > 0)
q = q.Where(er => status.Include.Contains(er.Envelope!.Status));
if (status.Ignore is not null)
q = q.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
_repo = envelopeReceiver;
_mapper = mapper;
_rcvRepo = rcvRepo;
}
var envRcvs = await q
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
.Include(er => er.Envelope).ThenInclude(e => e!.User)
.Include(er => er.Receiver)
.ToListAsync(cancel);
if (request.Receiver.HasAnyCriteria && envRcvs.Any())
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
{
var receiver = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
var q = _repo.Query.Where(request, notnull: false);
foreach (var envRcv in envRcvs)
envRcv.Envelope?.Documents?.First().Elements.RemoveAll(s => s.ReceiverId != receiver.Id);
if (request.Username is string username)
q = q.Where(er => er.Envelope!.User.Username == username);
if (request.Envelope.Status is not null)
{
var status = request.Envelope.Status;
if (status.Min is not null)
q = q.Where(er => er.Envelope!.Status >= status.Min);
if (status.Max is not null)
q = q.Where(er => er.Envelope!.Status <= status.Max);
if (status.Include?.Length > 0)
q = q.Where(er => status.Include.Contains(er.Envelope!.Status));
if (status.Ignore is not null)
q = q.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
}
var envRcvs = await q
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
.Include(er => er.Envelope).ThenInclude(e => e!.User)
.Include(er => er.Receiver)
.ToListAsync(cancel);
if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0)
{
var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel);
foreach (var envRcv in envRcvs)
envRcv.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id);
}
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
}
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
}
}
}

View File

@@ -0,0 +1,45 @@
using EnvelopeGenerator.Application.Common.Dto;
using MediatR;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.EnvelopeTypes.Queries;
/// <summary>
///
/// </summary>
public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
/// <summary>
///
/// </summary>
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
{
private readonly IRepository<EnvelopeType> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
{
var types = await _repository.Query.ToListAsync(cancellationToken);
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
}
}

View File

@@ -33,7 +33,18 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
/// <summary>
/// ID des Absenders
/// </summary>
public int UserId { get; set; }
internal int UserId { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public bool Authorize(int userId)
{
UserId = userId;
return true;
}
/// <summary>
/// Determines which component is used for envelope processing.

View File

@@ -1,18 +1,33 @@
using MediatR;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Application.Common.Dto;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Envelopes.Queries;
/// <summary>
/// Repräsentiert eine Abfrage für Umschläge.
/// </summary>
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
{
/// <summary>
/// Abfrage des Include des Umschlags
/// </summary>
public EnvelopeStatusQuery? Status { get; init; }
/// <summary>
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
/// </summary>
public int? UserId { get; init; }
/// <summary>
/// Setzt den Benutzerkontext für die Abfrage.
/// </summary>
public ReadEnvelopeQuery Authorize(int userId) => this with { UserId = userId };
}
/// <summary>
@@ -65,4 +80,62 @@ public record EnvelopeStatusQuery
/// Eine Liste von Statuswerten, die ignoriert werden werden.
/// </summary>
public EnvelopeStatus[]? Ignore { get; init; }
}
/// <summary>
/// Verarbeitet <see cref="ReadEnvelopeQuery"/> und liefert passende <see cref="EnvelopeDto"/>-Ergebnisse.
/// </summary>
public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnumerable<EnvelopeDto>>
{
private readonly IRepository<Envelope> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadEnvelopeQueryHandler(IRepository<Envelope> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<IEnumerable<EnvelopeDto>> Handle(ReadEnvelopeQuery request, CancellationToken cancel)
{
var query = _repository.Query;
if (request.UserId is int userId)
query = query.Where(e => e.UserId == userId);
if (request.Id is int id)
query = query.Where(e => e.Id == id);
if (request.Uuid is string uuid)
query = query.Where(e => e.Uuid == uuid);
if (request.Status is { } status)
{
if (status.Min is not null)
query = query.Where(e => e.Status >= status.Min);
if (status.Max is not null)
query = query.Where(e => e.Status <= status.Max);
if (status.Include?.Length > 0)
query = query.Where(e => status.Include.Contains(e.Status));
if (status.Ignore?.Length > 0)
query = query.Where(e => !status.Ignore.Contains(e.Status));
}
var envelopes = await query
.Include(e => e.Documents)
.ToListAsync(cancel);
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
}
}

View File

@@ -1,4 +1,8 @@
using EnvelopeGenerator.Application.Receivers.Queries;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Query;
using MediatR;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Envelopes.Queries;
@@ -6,6 +10,54 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
/// Eine Abfrage, um die zuletzt verwendete Anrede eines Empfängers zu ermitteln,
/// damit diese für zukünftige Umschläge wiederverwendet werden kann.
/// </summary>
public record ReadReceiverNameQuery() : ReadReceiverQuery
public record ReadReceiverNameQuery() : ReceiverQueryBase, IRequest<string?>;
/// <summary>
///
/// </summary>
public class ReadReceiverNameQueryHandler : IRequestHandler<ReadReceiverNameQuery, string?>
{
}
private readonly IRepository<EnvelopeReceiver> _envelopeReceiverRepository;
private readonly IRepository<Receiver> _receiverRepository;
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiverRepository"></param>
/// <param name="receiverRepository"></param>
public ReadReceiverNameQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiverRepository, IRepository<Receiver> receiverRepository)
{
_envelopeReceiverRepository = envelopeReceiverRepository;
_receiverRepository = receiverRepository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string?> Handle(ReadReceiverNameQuery request, CancellationToken cancellationToken)
{
var receiverQuery = _receiverRepository.Query.AsNoTracking();
if (request.Id is int id)
receiverQuery = receiverQuery.Where(r => r.Id == id);
if (request.EmailAddress is string email)
receiverQuery = receiverQuery.Where(r => r.EmailAddress == email);
if (request.Signature is string signature)
receiverQuery = receiverQuery.Where(r => r.Signature == signature);
var receiver = await receiverQuery.FirstOrDefaultAsync(cancellationToken);
if (receiver is null)
return null;
var erName = await _envelopeReceiverRepository.Query
.Where(er => er.ReceiverId == receiver.Id)
.OrderByDescending(er => er.AddedWhen)
.Select(er => er.Name)
.FirstOrDefaultAsync(cancellationToken);
return erName;
}
}

View File

@@ -1,4 +1,10 @@
using EnvelopeGenerator.Application.Common.Query;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Query;
using MediatR;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Receivers.Queries;
@@ -6,4 +12,53 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
/// um spezifische Informationen über einen Empfänger abzurufen.
/// </summary>
public record ReadReceiverQuery : ReceiverQueryBase;
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>;
/// <summary>
///
/// </summary>
public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnumerable<ReceiverDto>>
{
private readonly IRepository<Receiver> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadReceiverQueryHandler(IRepository<Receiver> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IEnumerable<ReceiverDto>> Handle(ReadReceiverQuery request, CancellationToken cancellationToken)
{
var query = _repository.Query;
if (request.Id is int id)
{
query = query.Where(r => r.Id == id);
}
if (request.EmailAddress is string email)
{
query = query.Where(r => r.EmailAddress == email);
}
if (request.Signature is string signature)
{
query = query.Where(r => r.Signature == signature);
}
var receiver = await query.ToListAsync(cancellationToken);
return _mapper.Map<IEnumerable<ReceiverDto>>(receiver);
}
}

View File

@@ -0,0 +1,130 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Manages annotations and signature lifecycle for envelopes.
/// </summary>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[ApiController]
[Route("api/[controller]")]
public class AnnotationController : ControllerBase
{
[Obsolete("Use MediatR")]
private readonly IEnvelopeHistoryService _historyService;
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverService _envelopeReceiverService;
private readonly IMediator _mediator;
private readonly ILogger<AnnotationController> _logger;
/// <summary>
/// Initializes a new instance of <see cref="AnnotationController"/>.
/// </summary>
[Obsolete("Use MediatR")]
public AnnotationController(
ILogger<AnnotationController> logger,
IEnvelopeHistoryService envelopeHistoryService,
IEnvelopeReceiverService envelopeReceiverService,
IMediator mediator)
{
_historyService = envelopeHistoryService;
_envelopeReceiverService = envelopeReceiverService;
_mediator = mediator;
_logger = logger;
}
/// <summary>
/// Creates or updates annotations for the authenticated envelope receiver.
/// </summary>
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
/// <param name="cancel">Cancellation token.</param>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost]
[Obsolete("This endpoint is for PSPDF Kit.")]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
{
var signature = User.GetAuthReceiverSignature();
var uuid = User.GetAuthEnvelopeUuid();
if (signature is null || uuid is null)
{
_logger.LogError("Authorization failed: authenticated user does not have a valid signature or envelope UUID.");
return Unauthorized("User authentication is incomplete. Missing required claims for processing this request.");
}
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null)
return BadRequest();
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
return Problem(statusCode: StatusCodes.Status409Conflict);
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
return Problem(statusCode: StatusCodes.Status423Locked);
var docSignedNotification = await _mediator
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
.ToDocSignedNotification(psPdfKitAnnotation)
?? throw new NotFoundException("Envelope receiver is not found.");
await _mediator.PublishSafely(docSignedNotification, cancel);
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
/// <summary>
/// Rejects the document for the current receiver.
/// </summary>
/// <param name="reason">Optional rejection reason.</param>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("reject")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> Reject([FromBody] string? reason = null)
{
var signature = User.GetAuthReceiverSignature();
var uuid = User.GetAuthEnvelopeUuid();
var mail = User.GetAuthReceiverMail();
if (uuid is null || signature is null || mail is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature,
message: @$"Unauthorized POST request in api\\envelope\\reject. One of claims, Envelope, signature or mail ({mail}) is null.");
return Unauthorized();
}
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
if (envRcvRes.IsFailed)
{
_logger.LogNotice(envRcvRes.Notices);
return Unauthorized("you are not authorized");
}
var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason);
if (histRes.IsSuccess)
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return NoContent();
}
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject");
_logger.LogNotice(histRes.Notices);
return StatusCode(500, histRes.Messages);
}
}

View File

@@ -0,0 +1,29 @@
using EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Exposes configuration data required by the client applications.
/// </summary>
/// <remarks>
/// Initializes a new instance of <see cref="ConfigController"/>.
/// </remarks>
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
{
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
/// <summary>
/// Returns annotation configuration that was previously rendered by MVC.
/// </summary>
[HttpGet("Annotations")]
public IActionResult GetAnnotationParams()
{
return Ok(_annotationParams.AnnotationJSObject);
}
}

View File

@@ -0,0 +1,43 @@
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Provides access to envelope documents for authenticated receivers.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="DocumentController"/> class.
/// </remarks>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[ApiController]
[Route("api/[controller]")]
public class DocumentController(IMediator mediator, ILogger<DocumentController> logger) : ControllerBase
{
/// <summary>
/// Returns the document bytes for the specified envelope receiver key.
/// </summary>
/// <param name="query">Encoded envelope key.</param>
/// <param name="cancel">Cancellation token.</param>
[HttpGet]
public async Task<IActionResult> GetDocument(ReadEnvelopeReceiverQuery query, CancellationToken cancel)
{
var envRcv = await mediator.Send(query, cancel).FirstAsync(Exceptions.NotFound);
var byteData = envRcv.Envelope?.Documents?.FirstOrDefault()?.ByteData;
if (byteData is null || byteData.Length == 0)
{
logger.LogError("Document byte data is null or empty for envelope-receiver entity:\n{envelopeKey}.",
envRcv.ToJson(Format.Json.ForDiagnostics));
throw new NotFoundException("Document is empty.");
}
return File(byteData, "application/octet-stream");
}
}

View File

@@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using System.Threading.Tasks;
using DigitalData.UserManager.Application.Services;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -23,12 +23,9 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
[Authorize]
public class EmailTemplateController : ControllerBase
{
private readonly ILogger<EmailTemplateController> _logger;
private readonly IMapper _mapper;
[Obsolete("Use IRepository")]
private readonly IEmailTemplateRepository _repository;
private readonly IRepository<EmailTemplate> _repository;
private readonly IMediator _mediator;
@@ -39,12 +36,11 @@ public class EmailTemplateController : ControllerBase
/// <param name="repository">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
[Obsolete("Use IRepository")]
public EmailTemplateController(IMapper mapper, IEmailTemplateRepository repository, ILogger<EmailTemplateController> logger, IMediator mediator)
[Obsolete("Use MediatR")]
public EmailTemplateController(IMapper mapper, IRepository<EmailTemplate> repository, IMediator mediator)
{
_mapper = mapper;
_repository = repository;
_logger = logger;
_mediator = mediator;
}
@@ -67,7 +63,7 @@ public class EmailTemplateController : ControllerBase
{
if (emailTemplate is null || (emailTemplate.Id is null && emailTemplate.Type is null))
{
var temps = await _repository.ReadAllAsync();
var temps = await _repository.Query.ToListAsync();
return Ok(_mapper.Map<IEnumerable<EmailTemplateDto>>(temps));
}
else

View File

@@ -1,11 +1,9 @@
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Envelopes.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -29,21 +27,16 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
public class EnvelopeController : ControllerBase
{
private readonly ILogger<EnvelopeController> _logger;
[Obsolete("Use MediatR")]
private readonly IEnvelopeService _envelopeService;
private readonly IMediator _mediator;
/// <summary>
/// Erstellt eine neue Instanz des EnvelopeControllers.
/// </summary>
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
/// <param name="envelopeService">Der Dienst, der für die Verarbeitung von Umschlägen zuständig ist.</param>
/// <param name="mediator"></param>
[Obsolete("Use MediatR")]
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeService envelopeService, IMediator mediator)
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
{
_logger = logger;
_envelopeService = envelopeService;
_mediator = mediator;
}
@@ -59,98 +52,56 @@ public class EnvelopeController : ControllerBase
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[Authorize]
[HttpGet]
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
{
if (User.GetId() is int intId)
return await _envelopeService.ReadByUserAsync(intId, min_status: envelope.Status?.Min, max_status: envelope.Status?.Max).ThenAsync(
Success: envelopes =>
{
if (envelope.Id is int id)
envelopes = envelopes.Where(e => e.Id == id);
if (envelope.Status is EnvelopeStatusQuery status)
envelopes = envelopes.Where(e => e.Status == status);
if (envelope.Uuid is string uuid)
envelopes = envelopes.Where(e => e.Uuid == uuid);
return envelopes.Any() ? Ok(envelopes) : NotFound();
},
Fail: IActionResult (msg, ntc) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
else
{
_logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein.");
return StatusCode(StatusCodes.Status500InternalServerError);
}
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
return result.Any() ? Ok(result) : NotFound();
}
/// <summary>
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
/// </summary>
/// <param name="id">Die eindeutige ID des Umschlags.</param>
/// <param name="query"></param>
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[HttpGet("doc-result")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetDocResultAsync([FromQuery] int id, [FromQuery] bool view = false)
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
{
if (User.GetId() is int intId)
return await _envelopeService.ReadByUserAsync(intId).ThenAsync(
Success: envelopes =>
{
var envelope = envelopes.Where(e => e.Id == id).FirstOrDefault();
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
var envelope = envelopes.FirstOrDefault();
if (envelope is null)
return NotFound("Envelope not available.");
else if (envelope?.DocResult is null)
return NotFound("The document has not been fully signed or the result has not yet been released.");
else
{
if (view)
{
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
return File(envelope.DocResult, "application/pdf");
}
else
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
}
},
Fail: IActionResult (msg, ntc) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
else
if (envelope is null)
return NotFound("Envelope not available.");
if (envelope.DocResult is null)
return NotFound("The document has not been fully signed or the result has not yet been released.");
if (view)
{
_logger.LogError("Trotz erfolgreicher Autorisierung wurde die Benutzer-ID nicht als Ganzzahl erkannt. Dies könnte auf eine fehlerhafte Erstellung der Anspruchsliste zurückzuführen sein.");
return StatusCode(StatusCodes.Status500InternalServerError);
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
return File(envelope.DocResult, "application/pdf");
}
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
}
/// <summary>
///
/// </summary>
/// <param name="envelope"></param>
/// <param name="command"></param>
/// <returns></returns>
[NonAction]
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromQuery] CreateEnvelopeCommand envelope)
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
{
envelope.UserId = User.GetId();
var res = await _mediator.Send(envelope);
var res = await _mediator.Send(command.Authorize(User.GetId()));
if (res is null)
{
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(envelope));
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
return StatusCode(StatusCodes.Status500InternalServerError);
}
else

View File

@@ -1,5 +1,4 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Envelopes.Queries;
@@ -11,12 +10,8 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using System.Data;
using System.Reflection.Metadata;
using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.SQL;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -33,38 +28,19 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
public class EnvelopeReceiverController : ControllerBase
{
private readonly ILogger<EnvelopeReceiverController> _logger;
[Obsolete("Use MediatR")]
private readonly IEnvelopeReceiverService _erService;
private readonly IMediator _mediator;
private readonly IMapper _mapper;
private readonly IEnvelopeExecutor _envelopeExecutor;
private readonly IEnvelopeReceiverExecutor _erExecutor;
private readonly IDocumentExecutor _documentExecutor;
private readonly string _cnnStr;
/// <summary>
/// Konstruktor für den EnvelopeReceiverController.
/// </summary>
/// <param name="logger">Logger-Instanz zur Protokollierung von Informationen und Fehlern.</param>
/// <param name="envelopeReceiverService">Service zur Verwaltung von Umschlagempfängern.</param>
/// <param name="mediator">Mediator-Instanz zur Verarbeitung von Befehlen und Abfragen.</param>
/// <param name="mapper"></param>
/// <param name="envelopeExecutor"></param>
/// <param name="erExecutor"></param>
/// <param name="documentExecutor"></param>
/// <param name="csOpt"></param>
[Obsolete("Use MediatR")]
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IEnvelopeReceiverService envelopeReceiverService, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
{
_logger = logger;
_erService = envelopeReceiverService;
_mediator = mediator;
_mapper = mapper;
_envelopeExecutor = envelopeExecutor;
@@ -87,7 +63,6 @@ public class EnvelopeReceiverController : ControllerBase
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[Authorize]
[HttpGet]
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
{
var username = User.GetUsernameOrDefault();
@@ -99,20 +74,11 @@ public class EnvelopeReceiverController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError);
}
return await _erService.ReadByUsernameAsync(
username: username,
min_status: envelopeReceiver.Envelope.Status?.Min,
max_status: envelopeReceiver.Envelope.Status?.Max,
envelopeQuery: envelopeReceiver.Envelope,
receiverQuery: envelopeReceiver.Receiver,
ignore_statuses: envelopeReceiver.Envelope.Status?.Ignore ?? Array.Empty<EnvelopeStatus>())
.ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError, msg);
});
envelopeReceiver = envelopeReceiver with { Username = username };
var result = await _mediator.Send(envelopeReceiver);
return Ok(result);
}
/// <summary>
@@ -129,25 +95,17 @@ public class EnvelopeReceiverController : ControllerBase
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[Authorize]
[HttpGet("salute")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
{
return await _erService.ReadLastUsedReceiverNameByMailAsync(receiver.EmailAddress, receiver.Id, receiver.Signature).ThenAsync(
Success: res => res is null ? NotFound() : Ok(res),
Fail: IActionResult (msg, ntc) =>
{
if (ntc.HasFlag(Flag.NotFound))
return NotFound();
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
var name = await _mediator.Send(receiver);
return name is null ? NotFound() : Ok(name);
}
/// <summary>
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns>HTTP-Antwort</returns>
/// <remarks>
/// Sample request:
@@ -183,13 +141,10 @@ public class EnvelopeReceiverController : ControllerBase
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request)
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
{
CancellationToken cancel = default;
int userId = User.GetId();
#region Create Envelope
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancel);
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
#endregion
#region Add receivers

View File

@@ -1,5 +1,5 @@
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using MediatR;
using EnvelopeGenerator.Application.EnvelopeTypes.Queries;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -13,36 +13,27 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
public class EnvelopeTypeController : ControllerBase
{
private readonly ILogger<EnvelopeTypeController> _logger;
[Obsolete("Use MediatR")]
private readonly IEnvelopeTypeService _service;
private readonly IMediator _mediator;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="service"></param>
[Obsolete("Use MediatR")]
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IEnvelopeTypeService service)
/// <param name="mediator"></param>
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IMediator mediator)
{
_logger = logger;
_service = service;
_mediator = mediator;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
[Obsolete("Use MediatR")]
[HttpGet]
public async Task<IActionResult> GetAllAsync()
{
return await _service.ReadAllAsync().ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
_logger.LogNotice(ntc);
return ntc.HasFlag(Flag.NotFound) ? NotFound() : StatusCode(StatusCodes.Status500InternalServerError);
});
var result = await _mediator.Send(new ReadEnvelopeTypesQuery());
return Ok(result);
}
}

View File

@@ -0,0 +1,95 @@
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Manages read-only envelope sharing flows.
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class ReadOnlyController : ControllerBase
{
private readonly ILogger<ReadOnlyController> _logger;
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
private readonly IEnvelopeMailService _mailService;
private readonly IEnvelopeHistoryService _historyService;
/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyController"/> class.
/// </summary>
public ReadOnlyController(ILogger<ReadOnlyController> logger, IEnvelopeReceiverReadOnlyService readOnlyService, IEnvelopeMailService mailService, IEnvelopeHistoryService historyService)
{
_logger = logger;
_readOnlyService = readOnlyService;
_mailService = mailService;
_historyService = historyService;
}
/// <summary>
/// Creates a new read-only receiver for the current envelope.
/// </summary>
/// <param name="createDto">Creation payload.</param>
[HttpPost]
[Authorize(Roles = ReceiverRole.FullyAuth)]
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
{
var authReceiverMail = User.GetAuthReceiverMail();
if (authReceiverMail is null)
{
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
return Unauthorized();
}
var envelopeId = User.GetAuthEnvelopeId();
if (envelopeId is null)
{
_logger.LogError("Envelope Id claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
return Unauthorized();
}
createDto.AddedWho = authReceiverMail;
createDto.EnvelopeId = envelopeId;
var creationRes = await _readOnlyService.CreateAsync(createDto: createDto);
if (creationRes.IsFailed)
{
_logger.LogNotice(creationRes);
return StatusCode(StatusCodes.Status500InternalServerError);
}
var readRes = await _readOnlyService.ReadByIdAsync(creationRes.Data.Id);
if (readRes.IsFailed)
{
_logger.LogNotice(creationRes);
return StatusCode(StatusCodes.Status500InternalServerError);
}
var newReadOnly = readRes.Data;
return await _mailService.SendAsync(newReadOnly).ThenAsync<int, IActionResult>(SuccessAsync: async _ =>
{
var histRes = await _historyService.RecordAsync((int)createDto.EnvelopeId, createDto.AddedWho, EnvelopeStatus.EnvelopeShared);
if (histRes.IsFailed)
{
_logger.LogError("Although the envelope was sent as read-only, the EnvelopeShared history could not be saved. Create DTO:\n{createDto}", JsonConvert.SerializeObject(createDto));
_logger.LogNotice(histRes.Notices);
}
return Ok();
},
Fail: (msg, ntc) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
});
}
}

View File

@@ -1,12 +1,7 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.API;
using MediatR;
using EnvelopeGenerator.Application.Receivers.Queries;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using EnvelopeGenerator.Application.Receivers.Commands;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -14,22 +9,22 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// Controller für die Verwaltung von Empfängern.
/// </summary>
/// <remarks>
/// Dieser Controller bietet Endpunkte für CRUD-Operationen (Erstellen, Lesen, Aktualisieren, Löschen)
/// sowie zusätzliche Funktionen wie das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
/// </remarks>
[Route("api/[controller]")]
[ApiController]
[Authorize]
[Obsolete("Use MediatR")]
public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverService, CreateReceiverCommand, ReceiverDto, UpdateReceiverCommand, Receiver, int>
public class ReceiverController : ControllerBase
{
private readonly IMediator _mediator;
/// <summary>
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
/// </summary>
/// <param name="logger">Der Logger für die Protokollierung.</param>
/// <param name="service">Der Dienst für Empfängeroperationen.</param>
public ReceiverController(ILogger<ReceiverController> logger, IReceiverService service) : base(logger, service)
/// <param name="mediator">Mediator für Anfragen.</param>
public ReceiverController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
@@ -40,60 +35,13 @@ public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverS
[HttpGet]
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
{
if (receiver.Id is null && receiver.EmailAddress is null && receiver.Signature is null)
return await base.GetAll();
if (!receiver.HasAnyCriteria)
{
var all = await _mediator.Send(new ReadReceiverQuery());
return Ok(all);
}
if (receiver.Id is int id)
return await _service.ReadByIdAsync(id).ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
return NotFound();
});
return await _service.ReadByAsync(emailAddress: receiver.EmailAddress, signature: receiver.Signature).ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
return NotFound();
});
var result = await _mediator.Send(receiver);
return result is null ? NotFound() : Ok(result);
}
#region REMOVED ENDPOINTS
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> GetAll() => base.GetAll();
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> Delete([FromRoute] int id) => base.Delete(id);
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> Update(UpdateReceiverCommand updateDto) => base.Update(updateDto);
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> Create(CreateReceiverCommand createDto)
{
return base.Create(createDto);
}
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> GetById([FromRoute] int id)
{
return base.GetById(id);
}
#endregion
}

View File

@@ -0,0 +1,130 @@
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using EnvelopeGenerator.GeneratorAPI.Models;
using Ganss.Xss;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
/// </summary>
[ApiController]
[Route("api/tfa")]
public class TfaRegistrationController : ControllerBase
{
private readonly ILogger<TfaRegistrationController> _logger;
private readonly IEnvelopeReceiverService _envelopeReceiverService;
private readonly IAuthenticator _authenticator;
private readonly IReceiverService _receiverService;
private readonly TFARegParams _parameters;
private readonly IStringLocalizer<Resource> _localizer;
/// <summary>
/// Initializes a new instance of the <see cref="TfaRegistrationController"/> class.
/// </summary>
public TfaRegistrationController(
ILogger<TfaRegistrationController> logger,
IEnvelopeReceiverService envelopeReceiverService,
IAuthenticator authenticator,
IReceiverService receiverService,
IOptions<TFARegParams> tfaRegParamsOptions,
IStringLocalizer<Resource> localizer)
{
_logger = logger;
_envelopeReceiverService = envelopeReceiverService;
_authenticator = authenticator;
_receiverService = receiverService;
_parameters = tfaRegParamsOptions.Value;
_localizer = localizer;
}
/// <summary>
/// Generates registration metadata (QR code and deadline) for a receiver.
/// </summary>
/// <param name="envelopeReceiverId">Encoded envelope receiver id.</param>
[Authorize]
[HttpGet("{envelopeReceiverId}")]
public async Task<IActionResult> RegisterAsync(string envelopeReceiverId)
{
try
{
var (uuid, signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
if (uuid is null || signature is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
return Unauthorized(new { message = _localizer.WrongEnvelopeReceiverId() });
}
var secretResult = await _envelopeReceiverService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
if (secretResult.IsFailed)
{
_logger.LogNotice(secretResult.Notices);
return NotFound(new { message = _localizer.WrongEnvelopeReceiverId() });
}
var envelopeReceiver = secretResult.Data;
if (!envelopeReceiver.Envelope!.TFAEnabled)
return Unauthorized(new { message = _localizer.WrongAccessCode() });
var receiver = envelopeReceiver.Receiver;
receiver!.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
await _receiverService.UpdateAsync(receiver);
var totpQr64 = _authenticator.GenerateTotpQrCode(userEmail: receiver.EmailAddress, secretKey: receiver.TotpSecretkey).ToBase64String();
if (receiver.TfaRegDeadline is null)
{
receiver.TfaRegDeadline = _parameters.Deadline;
await _receiverService.UpdateAsync(receiver);
}
else if (receiver.TfaRegDeadline <= DateTime.Now)
{
return StatusCode(StatusCodes.Status410Gone, new { message = _localizer.WrongAccessCode() });
}
return Ok(new
{
envelopeReceiver.EnvelopeId,
envelopeReceiver.Envelope!.Uuid,
envelopeReceiver.Receiver!.Signature,
receiver.TfaRegDeadline,
TotpQR64 = totpQr64
});
}
catch (Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer.WrongEnvelopeReceiverId());
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
}
}
/// <summary>
/// Logs out the envelope receiver from cookie authentication.
/// </summary>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("auth/logout")]
public async Task<IActionResult> LogOutAsync()
{
try
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "{message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
}
}
}

View File

@@ -0,0 +1,18 @@
namespace EnvelopeGenerator.GeneratorAPI
{
/// <summary>
/// Provides custom claim types for envelope-related information.
/// </summary>
public static class EnvelopeClaimTypes
{
/// <summary>
/// Claim type for the title of an envelope.
/// </summary>
public static readonly string Title = $"Envelope{nameof(Title)}";
/// <summary>
/// Claim type for the ID of an envelope.
/// </summary>
public static readonly string Id = $"Envelope{nameof(Id)}";
}
}

View File

@@ -0,0 +1,87 @@
using System.Security.Claims;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace EnvelopeGenerator.GeneratorAPI.Extensions;
/// <summary>
/// Provides helper methods for working with envelope-specific authentication claims.
/// </summary>
public static class EnvelopeAuthExtensions
{
/// <summary>
/// Retrieves a claim value by type.
/// </summary>
/// <param name="user">The current claims principal.</param>
/// <param name="claimType">The claim type to resolve.</param>
/// <returns>The claim value or null when missing.</returns>
public static string? GetClaimValue(this ClaimsPrincipal user, string claimType) => user.FindFirstValue(claimType);
/// <summary>
/// Gets the authenticated envelope UUID from the claims.
/// </summary>
public static string? GetAuthEnvelopeUuid(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.NameIdentifier);
/// <summary>
/// Gets the authenticated receiver signature from the claims.
/// </summary>
public static string? GetAuthReceiverSignature(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Hash);
/// <summary>
/// Gets the authenticated receiver display name from the claims.
/// </summary>
public static string? GetAuthReceiverName(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Name);
/// <summary>
/// Gets the authenticated receiver email address from the claims.
/// </summary>
public static string? GetAuthReceiverMail(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Email);
/// <summary>
/// Gets the authenticated envelope title from the claims.
/// </summary>
public static string? GetAuthEnvelopeTitle(this ClaimsPrincipal user) => user.FindFirstValue(EnvelopeClaimTypes.Title);
/// <summary>
/// Gets the authenticated envelope identifier from the claims.
/// </summary>
public static int? GetAuthEnvelopeId(this ClaimsPrincipal user)
{
var envIdStr = user.FindFirstValue(EnvelopeClaimTypes.Id);
return int.TryParse(envIdStr, out var envId) ? envId : null;
}
/// <summary>
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
/// </summary>
/// <param name="context">The current HTTP context.</param>
/// <param name="envelopeReceiver">Envelope receiver DTO to extract claims from.</param>
/// <param name="receiverRole">Role to attach to the authentication ticket.</param>
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto envelopeReceiver, string receiverRole)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, envelopeReceiver.Envelope!.Uuid),
new(ClaimTypes.Hash, envelopeReceiver.Receiver!.Signature),
new(ClaimTypes.Name, envelopeReceiver.Name ?? string.Empty),
new(ClaimTypes.Email, envelopeReceiver.Receiver.EmailAddress),
new(EnvelopeClaimTypes.Title, envelopeReceiver.Envelope.Title),
new(EnvelopeClaimTypes.Id, envelopeReceiver.Envelope.Id.ToString()),
new(ClaimTypes.Role, receiverRole)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = false,
IsPersistent = false
};
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
}

View File

@@ -0,0 +1,14 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
{
public bool HasAccessCode => AccessCode is not null;
public bool HasSmsCode => SmsCode is not null;
public bool HasAuthenticatorCode => AuthenticatorCode is not null;
public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1;
public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode);
}

View File

@@ -0,0 +1,60 @@
namespace EnvelopeGenerator.GeneratorAPI.Models
{
/// <summary>
/// Represents a hyperlink for contact purposes with various HTML attributes.
/// </summary>
public class ContactLink
{
/// <summary>
/// Gets or sets the label of the hyperlink.
/// </summary>
public string Label { get; init; } = "Contact";
/// <summary>
/// Gets or sets the URL that the hyperlink points to.
/// </summary>
public string Href { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the target where the hyperlink should open.
/// Commonly used values are "_blank", "_self", "_parent", "_top".
/// </summary>
public string Target { get; set; } = "_blank";
/// <summary>
/// Gets or sets the relationship of the linked URL as space-separated link types.
/// Examples include "nofollow", "noopener", "noreferrer".
/// </summary>
public string Rel { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the filename that should be downloaded when clicking the hyperlink.
/// This attribute will only have an effect if the href attribute is set.
/// </summary>
public string Download { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the language of the linked resource. Useful when linking to
/// content in another language.
/// </summary>
public string HrefLang { get; set; } = "en";
/// <summary>
/// Gets or sets the MIME type of the linked URL. Helps browsers to handle
/// the type correctly when the link is clicked.
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// Gets or sets additional information about the hyperlink, typically viewed
/// as a tooltip when the mouse hovers over the link.
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Gets or sets an identifier for the hyperlink, unique within the HTML document.
/// </summary>
public string Id { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,17 @@
using System.Globalization;
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class Culture
{
private string _language = string.Empty;
public string Language { get => _language;
init {
_language = value;
Info = new(value);
}
}
public string FIClass { get; init; } = string.Empty;
public CultureInfo? Info { get; init; }
}

View File

@@ -0,0 +1,12 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class Cultures : List<Culture>
{
public IEnumerable<string> Languages => this.Select(c => c.Language);
public IEnumerable<string> FIClasses => this.Select(c => c.FIClass);
public Culture Default => this.First();
public Culture? this[string? language] => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault();
}

View File

@@ -0,0 +1,6 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class CustomImages : Dictionary<string, Image>
{
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
}

View File

@@ -0,0 +1,10 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class ErrorViewModel
{
public string Title { get; init; } = "404";
public string Subtitle { get; init; } = "Hmmm...";
public string Body { get; init; } = "It looks like one of the developers fell asleep";
}

View File

@@ -0,0 +1,10 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class Image
{
public string Src { get; init; } = string.Empty;
public Dictionary<string, string> Classes { get; init; } = new();
public string GetClassIn(string page) => Classes.TryGetValue(page, out var cls) && cls is not null ? cls : string.Empty;
}

View File

@@ -0,0 +1,6 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
public class MainViewModel
{
public string? Title { get; init; }
}

View File

@@ -0,0 +1,92 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public record Annotation : IAnnotation
{
public required string Name { get; init; }
#region Bound Annotation
[JsonIgnore]
public string? HorBoundAnnotName { get; init; }
[JsonIgnore]
public string? VerBoundAnnotName { get; init; }
#endregion
#region Layout
[JsonIgnore]
public double? MarginLeft { get; set; }
[JsonIgnore]
public double MarginLeftRatio { get; init; } = 1;
[JsonIgnore]
public double? MarginTop { get; set; }
[JsonIgnore]
public double MarginTopRatio { get; init; } = 1;
public double? Width { get; set; }
[JsonIgnore]
public double WidthRatio { get; init; } = 1;
public double? Height { get; set; }
[JsonIgnore]
public double HeightRatio { get; init; } = 1;
#endregion
#region Position
public double Left => (MarginLeft ?? 0) + (HorBoundAnnot?.HorBoundary ?? 0);
public double Top => (MarginTop ?? 0) + (VerBoundAnnot?.VerBoundary ?? 0);
#endregion
#region Boundary
[JsonIgnore]
public double HorBoundary => Left + (Width ?? 0);
[JsonIgnore]
public double VerBoundary => Top + (Height ?? 0);
#endregion
#region BoundAnnot
[JsonIgnore]
public Annotation? HorBoundAnnot { get; set; }
[JsonIgnore]
public Annotation? VerBoundAnnot { get; set; }
#endregion
public Color? BackgroundColor { get; init; }
#region Border
public Color? BorderColor { get; init; }
public string? BorderStyle { get; init; }
public int? BorderWidth { get; set; }
#endregion
[JsonIgnore]
internal Annotation Default
{
set
{
// To set null value, annotation must have null (0) value but null must has non-null value
if (MarginLeft == null && value.MarginLeft != null)
MarginLeft = value.MarginLeft * MarginLeftRatio;
if (MarginTop == null && value.MarginTop != null)
MarginTop = value.MarginTop * MarginTopRatio;
if (Width == null && value.Width != null)
Width = value.Width * WidthRatio;
if (Height == null && value.Height != null)
Height = value.Height * HeightRatio;
}
}
};

View File

@@ -0,0 +1,79 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public class AnnotationParams
{
public AnnotationParams()
{
_AnnotationJSObjectInitor = new(CreateAnnotationJSObject);
}
public Background? Background { get; init; }
#region Annotation
[JsonIgnore]
public Annotation? DefaultAnnotation { get; init; }
private readonly List<Annotation> _annots = new List<Annotation>();
public bool TryGet(string name, out Annotation annotation)
{
#pragma warning disable CS8601 // Possible null reference assignment.
annotation = _annots.FirstOrDefault(a => a.Name == name);
#pragma warning restore CS8601 // Possible null reference assignment.
return annotation is not null;
}
public required IEnumerable<Annotation> Annotations
{
get => _annots;
init
{
_annots = value.ToList();
if (DefaultAnnotation is not null)
foreach (var annot in _annots)
annot.Default = DefaultAnnotation;
for (int i = 0; i < _annots.Count; i++)
{
#region set bound annotations
// horizontal
if (_annots[i].HorBoundAnnotName is string horBoundAnnotName)
if (TryGet(horBoundAnnotName, out var horBoundAnnot))
_annots[i].HorBoundAnnot = horBoundAnnot;
else
throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined.");
// vertical
if (_annots[i].VerBoundAnnotName is string verBoundAnnotName)
if (TryGet(verBoundAnnotName, out var verBoundAnnot))
_annots[i].VerBoundAnnot = verBoundAnnot;
else
throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined.");
#endregion
}
}
}
#endregion
#region AnnotationJSObject
private Dictionary<string, IAnnotation> CreateAnnotationJSObject()
{
var dict = _annots.ToDictionary(a => a.Name.ToLower(), a => a as IAnnotation);
if (Background is not null)
{
Background.Locate(_annots);
dict.Add(Background.Name.ToLower(), Background);
}
return dict;
}
private readonly Lazy<Dictionary<string, IAnnotation>> _AnnotationJSObjectInitor;
public Dictionary<string, IAnnotation> AnnotationJSObject => _AnnotationJSObjectInitor.Value;
#endregion
}

View File

@@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
/// <summary>
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
/// It is only the first annotation as a background for other annotations.
/// </summary>
public record Background : IAnnotation
{
[JsonIgnore]
public double Margin { get; init; }
public string Name { get; } = "Background";
public double? Width { get; set; }
public double? Height { get; set; }
public double Left { get; set; }
public double Top { get; set; }
public Color? BackgroundColor { get; init; }
#region Border
public Color? BorderColor { get; init; }
public string? BorderStyle { get; init; }
public int? BorderWidth { get; set; }
#endregion
public void Locate(IEnumerable<IAnnotation> annotations)
{
// set Top
if (annotations.MinBy(a => a.Top)?.Top is double minTop)
Top = minTop;
// set Left
if (annotations.MinBy(a => a.Left)?.Left is double minLeft)
Left = minLeft;
// set Width
if(annotations.MaxBy(a => a.GetRight())?.GetRight() is double maxRight)
Width = maxRight - Left;
// set Height
if (annotations.MaxBy(a => a.GetBottom())?.GetBottom() is double maxBottom)
Height = maxBottom - Top;
// add margins
Top -= Margin;
Left -= Margin;
Width += Margin * 2;
Height += Margin * 2;
}
}

View File

@@ -0,0 +1,10 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public record Color
{
public int R { get; init; } = 0;
public int G { get; init; } = 0;
public int B { get; init; } = 0;
}

View File

@@ -0,0 +1,8 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public static class Extensions
{
public static double GetRight(this IAnnotation annotation) => annotation.Left + annotation?.Width ?? 0;
public static double GetBottom(this IAnnotation annotation) => annotation.Top + annotation?.Height ?? 0;
}

View File

@@ -0,0 +1,22 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
public interface IAnnotation
{
string Name { get; }
double? Width { get; }
double? Height { get; }
double Left { get; }
double Top { get; }
Color? BackgroundColor { get; }
Color? BorderColor { get; }
string? BorderStyle { get; }
int? BorderWidth { get; }
}

View File

@@ -0,0 +1,17 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
/// <summary>
/// Represents the parameters for two-factor authentication (2FA) registration.
/// </summary>
public class TFARegParams
{
/// <summary>
/// The maximum allowed time for completing the registration process.
/// </summary>
public TimeSpan TimeLimit { get; init; } = new(0, 30, 0);
/// <summary>
/// The deadline for registration, calculated as the current time plus the <see cref="TimeLimit"/>.
/// </summary>
public DateTime Deadline => DateTime.Now.AddTicks(TimeLimit.Ticks);
}