Compare commits

..

29 Commits

Author SHA1 Message Date
Developer 02
86c9fdfcd7 refactor: inject IScheduler via DI instead of using StdSchedulerFactory directly 2025-11-04 17:34:36 +01:00
Developer 02
89ec887510 feat(quartz): integrate Quartzmon dashboard and CommandDotNet
- Added `Quartzmon` package and configured its middleware for job monitoring.
- Integrated `CommandDotNet.Execution` for command-line execution support.
- Updated using directives and service registrations accordingly.
- Preserved existing Serilog logging, DB context, and EnvelopeGenerator setup.
2025-11-04 17:29:07 +01:00
Developer 02
7d5b988842 fix(middleware): add UseRouting before UseAuthorization
Added `app.UseRouting()` in the middleware pipeline to ensure proper endpoint routing before authorization and controller mapping.
2025-11-04 17:18:29 +01:00
Developer 02
3c456562cc refactor: replace QuartzHostedService with QuartzServer and remove unnecessary using
- Replaced `AddQuartzHostedService` with `AddQuartzServer` for better Quartz integration.
- Removed `Microsoft.Extensions.Options` using as it was unused.
- Updated Quartz job naming to remove GUID and simplify identity.
- Minor code cleanup in using statements and regions.
2025-11-04 17:06:00 +01:00
Developer 02
4d6b01030c refactor(startup): migrate from generic Host to WebApplication and integrate Web API support
- Replaced Host.CreateApplicationBuilder with WebApplication.CreateBuilder
- Added Web API service registrations (Controllers, Swagger)
- Organized startup into clear regions: Logging, Configuration, Worker, Services, Middleware
- Introduced Swagger and HTTPS middleware for API
- Improved structure and readability of Program.cs
2025-11-04 16:12:21 +01:00
Developer 02
75e7e9925b feat(Program): make Quartz cron schedule configurable via appsettings
- Replaced hardcoded cron expression with configuration-based `Worker:CronExpression`.
- Throws descriptive exception if cron expression is missing.
- Keeps previous worker and DB context setup unchanged.
2025-11-04 15:37:20 +01:00
Developer 02
0a175b9e9d refactor: remove unnecessary while loop in Worker.Execute 2025-11-04 15:18:38 +01:00
Developer 02
f611e74de1 refactor(config): allow GdPicture license key from configuration
- Updated GdPictureOptions setup to read license key from `GdPictureLicenseKey` config value.
- Falls back to reading from third-party module if config key is not set.
2025-11-04 14:56:44 +01:00
Developer 02
08ca116628 refactor(worker): replace BackgroundService with Quartz IJob for scheduled execution
- Removed inheritance from BackgroundService
- Implemented Quartz IJob interface for better scheduling control
- Replaced ExecuteAsync with Execute(IJobExecutionContext)
- Updated cancellation handling to use context.CancellationToken
2025-11-04 14:49:56 +01:00
Developer 02
4997f7d75c feat: add in-memory database support via appsettings UseInMemoryDb flag
- Introduced conditional EF Core configuration to support InMemoryDatabase for testing or lightweight runs.
- Added `UseInMemoryDb` config flag read from appsettings.
- Retained SQL Server as the default when the flag is false.
- Added missing Quartz namespace import.
2025-11-04 13:44:39 +01:00
b5cd42b6fa add WorkerOptions.
- bind IntervalInMin with worker task delay
2025-11-03 16:38:41 +01:00
187f4a42fc feat(GdPictureOptions): Created to configure parameters related to GdPicture.
- Configure the GdPicture license key via a third-party module entity.
2025-11-03 15:36:11 +01:00
23d4b2f31e chore: move finalizer to presentation layer 2025-11-03 14:51:13 +01:00
8e71e5b4bb feat(third-party-modules): add query and handler for reading third-party modules with filtering by name and active status 2025-11-03 14:49:54 +01:00
b693615561 feat(ThirdPartyModuleDto): create DTO of ThirdPartyModule with mapping profile 2025-11-03 14:30:45 +01:00
36dc9266bc Add ThirdPartyModule entity for 3rd party module tracking
- Created ThirdPartyModule class in EnvelopeGenerator.Domain.Entities
- Mapped to TBDD_3RD_PARTY_MODULES table with appropriate columns
- Added properties: Id, Active, Name, Description, License, Version, AddedWho, AddedWhen, ChangedWho, ChangedWhen
- Configured data annotations for primary key, required fields, string lengths, and nullable support
- Conditional nullable support based on compilation symbols
2025-11-03 13:05:24 +01:00
9aabe270b4 add appsettings for PDF burner 2025-11-03 11:56:10 +01:00
b303b7be06 chore(Finalizer.Program): enhance configuration and environment-specific JSON loading
- Changed Serilog configuration file from `appsettings.json` to `appsettings.Logging.json`.
- Added logging for application startup.
- Dynamically load environment-specific appsettings JSON files, excluding `Development` and `migration` files.
2025-11-03 11:41:55 +01:00
02937360ea chore(Finalizer.appsettings): update Serilog configuration to increase log verbosity and retention
- Changed default minimum log level from Information to Verbose.
- Updated console sink to use Verbose level.
- Renamed and consolidated log files for Verbose, Debug, Info, Warning, Error, and Fatal levels.
- Increased retained file count from 7 to 30 for all log levels.
2025-11-03 11:26:14 +01:00
c2735b92e0 feat(logging): enhance Serilog configuration with separate level-based log files 2025-11-03 10:48:35 +01:00
568f43186c refactor(Finalizer.Program): simplify EnvelopeGenerator service registration using AddEnvelopeGenerator extension
- Replaced manual service setup with AddEnvelopeGenerator fluent configuration
- Added EnvelopeGenerator.DependencyInjection namespace
- Integrated distributed SQL Server cache for EG services
- Improved DI structure and reduced obsolete warnings
2025-11-03 10:24:09 +01:00
3bc5439b5a feat(dependency-injection): add service configuration validation and simplify setup
- Removed unnecessary IConfiguration and SqlServerCacheOptions parameters from AddEnvelopeGenerator
- Added EnsureAllServicesConfigured() to validate that all required service methods are invoked
- Introduced _addingStatus dictionary to track configuration status
- Renamed internal queue field to _serviceRegs for consistency
- Added AddServices() method to explicitly register EnvelopeGenerator services
- Improved AddLocalization() to support optional custom localization options
2025-11-03 09:30:43 +01:00
22b494a262 refactor(di): unify Application and Infrastructure DI registrations under a central method
- Added central AddEnvelopeGenerator extension to aggregate existing DI setups
- Introduced EGConfiguration for modular service registration
- Standardized configuration pattern for Application and Infrastructure layers
- Simplified distributed cache and localization registration
2025-11-03 08:44:22 +01:00
209785dda5 refactor(DependencyInjection): created to handle DependencyInjection 2025-10-31 11:31:39 +01:00
44ea893f05 move memory-cache injection to Application-layer 2025-10-31 10:58:22 +01:00
b4aa7984aa chore(Application): Update your packages with vulnerabilities
- Added `SixLabors.ImageSharp` (v3.1.12) to main package references.
- Added `System.Formats.Asn1` for all target frameworks:
  - net7.0 → v8.0.2
  - net8.0, net9.0 → v9.0.10
2025-10-31 10:34:25 +01:00
7c5a505ad1 add serilog 2025-10-31 10:23:11 +01:00
bd6d57e1e8 feat: integrate EnvelopeGenerator infrastructure, DB context, and services
- Added references to EnvelopeGenerator.Application, Infrastructure, and EF Core.
- Configured DB context with SQL Server connection string from configuration.
- Registered EnvelopeGenerator services and infrastructure with dependency injection.
- Preserved warnings suppression for obsolete members.
- Structured DI registration under a dedicated region for clarity.
2025-10-30 17:05:45 +01:00
8403ce2c6a init EnvelopeGenerator.Finalizer 2025-10-30 16:31:22 +01:00
114 changed files with 1333 additions and 14888 deletions

View File

@@ -1 +0,0 @@
{}

View File

@@ -82,7 +82,7 @@ public record EnvelopeDto
/// <summary>
///
/// </summary>
public bool UseAccessCode { get; set; } = true;
public bool? UseAccessCode { get; set; }
/// <summary>
///

View File

@@ -36,6 +36,7 @@ public class MappingProfile : Profile
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
CreateMap<ElementAnnotation, AnnotationDto>();
CreateMap<ThirdPartyModule, ThirdPartyModuleDto>();
// DTO to Entity mappings
CreateMap<ConfigDto, Config>();

View File

@@ -0,0 +1,57 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public record ThirdPartyModuleDto
{
/// <summary>
///
/// </summary>
public int Id { get; init; }
/// <summary>
///
/// </summary>
public bool Active { get; init; }
/// <summary>
///
/// </summary>
public string Name { get; init; } = default!;
/// <summary>
///
/// </summary>
public string? Description { get; init; }
/// <summary>
///
/// </summary>
public string License { get; init; } = default!;
/// <summary>
///
/// </summary>
public string Version { get; init; } = default!;
/// <summary>
///
/// </summary>
public string? AddedWho { get; init; }
/// <summary>
///
/// </summary>
public DateTime? AddedWhen { get; init; }
/// <summary>
///
/// </summary>
public string? ChangedWho { get; init; }
/// <summary>
///
/// </summary>
public DateTime? ChangedWhen { get; init; }
}

View File

@@ -58,6 +58,12 @@ public static class DependencyInjection
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
});
// Add memory cache
services.AddMemoryCache();
// Register mail services
services.AddScoped<IEnvelopeMailService, EnvelopeMailService>();
return services;
}
}

View File

@@ -25,19 +25,23 @@
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="UserManager" Version="1.1.3" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="System.Formats.Asn1" Version="8.0.2" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="System.Formats.Asn1" Version="9.0.10" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="System.Formats.Asn1" Version="9.0.10" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,101 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Histories.Queries;
//TODO: Add sender query
/// <summary>
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
/// </summary>
public record CountHistoryQuery : HistoryQueryBase, IRequest<int>;
/// <summary>
///
/// </summary>
public static class CountHistoryQueryExtensions
{
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="uuid"></param>
/// <param name="statuses"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static async Task<bool> AnyHistoryAsync(this ISender sender, string uuid, IEnumerable<EnvelopeStatus> statuses, CancellationToken cancel = default)
{
var count = await sender.Send(new CountHistoryQuery
{
Envelope = new() { Uuid = uuid },
Statuses = new() { Include = statuses }
}, cancel);
return count > 0;
}
}
/// <summary>
///
/// </summary>
public class CountHistoryQueryHandler : IRequestHandler<CountHistoryQuery, int>
{
private readonly IRepository<History> _repo;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
public CountHistoryQueryHandler(IRepository<History> repo)
{
_repo = repo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public Task<int> Handle(CountHistoryQuery request, CancellationToken cancel = default)
{
var query = _repo.Query;
if (request.Envelope.Id is int envId)
query = query.Where(e => e.Id == envId);
else if (request.Envelope.Uuid is string uuid)
query = query.Where(e => e.Envelope!.Uuid == uuid);
#pragma warning disable CS0618 // Type or member is obsolete
else if (request.EnvelopeId is not null)
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
#pragma warning restore CS0618 // Type or member is obsolete
else
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
#pragma warning disable CS0618 // Type or member is obsolete
if (request.Status is not null)
query = query.Where(h => h.Status == request.Status);
#pragma warning restore CS0618 // Type or member is obsolete
if (request.Statuses is not null)
{
var status = request.Statuses;
if (status.Min is not null)
query = query.Where(er => er.Envelope!.Status >= status.Min);
if (status.Max is not null)
query = query.Where(er => er.Envelope!.Status <= status.Max);
if (status.Include?.Count() > 0)
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
if (status.Ignore is not null)
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
}
return query.CountAsync(cancel);
}
}

View File

@@ -1,60 +0,0 @@
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Constants;
using System.ComponentModel.DataAnnotations;
namespace EnvelopeGenerator.Application.Histories.Queries;
//TODO: Add sender query
/// <summary>
///
/// </summary>
public record HistoryQueryBase
{
/// <summary>
/// Die eindeutige Kennung des Umschlags.
/// </summary>
[Obsolete("Use Envelope property")]
public int? EnvelopeId { get; set; }
/// <summary>
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
/// </summary>
[Obsolete("Use statuses")]
public EnvelopeStatus? Status { get; set; }
/// <summary>
///
/// </summary>
public EnvelopeStatusQuery Statuses { get; set; } = new();
/// <summary>
///
/// </summary>
public EnvelopeQueryBase Envelope { get; set; } = new EnvelopeQueryBase();
}
/// <summary>
///
/// </summary>
public record EnvelopeStatusQuery
{
/// <summary>
/// Der minimale Statuswert, der berücksichtigt werden.
/// </summary>
public EnvelopeStatus? Min { get; init; }
/// <summary>
/// Der maximale Statuswert, der berücksichtigt werden.
/// </summary>
public EnvelopeStatus? Max { get; init; }
/// <summary>
/// Eine Liste von Statuswerten, die einbezogen werden.
/// </summary>
public IEnumerable<EnvelopeStatus>? Include { get; init; }
/// <summary>
/// Eine Liste von Statuswerten, die ignoriert werden werden.
/// </summary>
public IEnumerable<EnvelopeStatus>? Ignore { get; init; }
}

View File

@@ -1,10 +1,7 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace EnvelopeGenerator.Application.Histories.Queries;
@@ -12,81 +9,21 @@ namespace EnvelopeGenerator.Application.Histories.Queries;
/// <summary>
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
/// </summary>
public record ReadHistoryQuery : HistoryQueryBase, IRequest<IEnumerable<HistoryDto>>
public record ReadHistoryQuery : IRequest<IEnumerable<HistoryDto>>
{
/// <summary>
/// Die eindeutige Kennung des Umschlags.
/// </summary>
[Required]
public int EnvelopeId { get; init; }
/// <summary>
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
/// </summary>
public EnvelopeStatus? Status { get; init; }
/// <summary>
/// Abfrage zur Steuerung, ob nur der aktuelle Include oder der gesamte Datensatz zurückgegeben wird.
/// </summary>
public bool OnlyLast { get; init; } = true;
}
/// <summary>
///
/// </summary>
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
{
private readonly IRepository<History> _repo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
/// <param name="mapper"></param>
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
{
var query = _repo.Query;
if (request.Envelope.Id is int envId)
query = query.Where(e => e.Id == envId);
else if (request.Envelope.Uuid is string uuid)
query = query.Where(e => e.Envelope!.Uuid == uuid);
#pragma warning disable CS0618 // Type or member is obsolete
else if (request.EnvelopeId is not null)
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
#pragma warning restore CS0618 // Type or member is obsolete
else
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
#pragma warning disable CS0618 // Type or member is obsolete
if (request.Status is not null)
query = query.Where(h => h.Status == request.Status);
#pragma warning restore CS0618 // Type or member is obsolete
if (request.Statuses is not null)
{
var status = request.Statuses;
if (status.Min is not null)
query = query.Where(er => er.Envelope!.Status >= status.Min);
if (status.Max is not null)
query = query.Where(er => er.Envelope!.Status <= status.Max);
if (status.Include?.Count() > 0)
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
if (status.Ignore is not null)
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
}
if (request.OnlyLast)
query = query.OrderByDescending(x => x.AddedWhen);
var hists = await query.ToListAsync(cancel);
return _mapper.Map<List<HistoryDto>>(hists);
}
public bool? OnlyLast { get; init; } = true;
}

View File

@@ -0,0 +1,47 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Histories.Queries;
/// <summary>
///
/// </summary>
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
{
private readonly IRepository<History> _repo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
/// <param name="mapper"></param>
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
{
var query = _repo.ReadOnly().Where(h => h.EnvelopeId == request.EnvelopeId);
if (request.Status is not null)
query = query.Where(h => h.Status == request.Status);
var hists = await query.ToListAsync(cancel);
return _mapper.Map<List<HistoryDto>>(hists);
}
}

View File

@@ -0,0 +1,92 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.ThirdPartyModules.Queries;
/// <summary>
///
/// </summary>
public record ReadThirdPartyModuleQuery : IRequest<IEnumerable<ThirdPartyModuleDto>>
{
/// <summary>
///
/// </summary>
public string? Name { get; init; }
/// <summary>
///
/// </summary>
public bool? Active { get; init; }
}
/// <summary>
///
/// </summary>
public record ReadThirdPartyModuleQueryHandler : IRequestHandler<ReadThirdPartyModuleQuery, IEnumerable<ThirdPartyModuleDto>>
{
private readonly IMapper _mapper;
private readonly IRepository<ThirdPartyModule> _repo;
/// <summary>
///
/// </summary>
/// <param name="mapper"></param>
/// <param name="repo"></param>
public ReadThirdPartyModuleQueryHandler(IMapper mapper, IRepository<ThirdPartyModule> repo)
{
_mapper = mapper;
_repo = repo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<IEnumerable<ThirdPartyModuleDto>> Handle(ReadThirdPartyModuleQuery request, CancellationToken cancel)
{
var query = _repo.Query;
if(request.Name is string name)
query = query.Where(m => m.Name == name);
if (request.Active is bool active)
query = query.Where(m => m.Active == active);
var modules = await query.ToListAsync(cancel);
return _mapper.Map<IEnumerable<ThirdPartyModuleDto>>(modules);
}
}
/// <summary>
///
/// </summary>
public static class ReadThirdPartyModuleQueryExtensions
{
/// <summary>
///
/// </summary>
/// <param name="mediator"></param>
/// <param name="name"></param>
/// <param name="active"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static async Task<string?> ReadThirdPartyModuleLicenseAsync(this IMediator mediator, string name, bool active = true, CancellationToken cancel = default)
{
var modules = await mediator.Send(new ReadThirdPartyModuleQuery()
{
Name = name,
Active = active,
}, cancel);
return modules.FirstOrDefault()?.License;
}
}

View File

@@ -32,7 +32,7 @@ Public Class frmFinalizePDF
#Disable Warning BC40000 ' Type or member is obsolete
Factory.Shared _
.BehaveOnPostBuild(PostBuildBehavior.Ignore) _
.AddEnvelopeGeneratorInfrastructureServices(
.AddEGInfrastructureServices(
Sub(opt)
opt.AddDbTriggerParams(
Sub(triggers)

View File

@@ -72,7 +72,7 @@ Namespace Jobs
#Disable Warning BC40000 ' Type or member is obsolete
Factory.Shared _
.BehaveOnPostBuild(PostBuildBehavior.Ignore) _
.AddEnvelopeGeneratorInfrastructureServices(
.AddEGInfrastructureServices(
Sub(opt)
opt.AddDbTriggerParams(
Sub(triggers)

View File

@@ -0,0 +1,98 @@
using DigitalData.EmailProfilerDispatcher;
using DigitalData.UserManager.DependencyInjection;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Infrastructure;
using Microsoft.Extensions.Caching.SqlServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using static EnvelopeGenerator.Infrastructure.DependencyInjection;
namespace EnvelopeGenerator.DependencyInjection;
public static class DependencyInjection
{
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, Action<EGConfiguration> options)
{
var egConfig = new EGConfiguration();
options.Invoke(egConfig);
egConfig.EnsureAllServicesConfigured();
egConfig.RegisterAll(services);
// Add envelope generator services
#pragma warning disable CS0618
services.AddUserManager<EGDbContext>();
#pragma warning restore CS0618
services.AddDispatcher<EGDbContext>();
return services;
}
public record EGConfiguration
{
internal readonly Queue<Action<IServiceCollection>> _serviceRegs = new();
internal void RegisterAll(IServiceCollection services)
{
while (_serviceRegs.Count > 0)
_serviceRegs.Dequeue().Invoke(services);
}
// TODO: update to use attributes and reflections instead of _addingStatus-dictionary
private readonly Dictionary<string, bool> _addingStatus = new ()
{
{ nameof(AddLocalization), false },
{ nameof(AddDistributedSqlServerCache), false },
{ nameof(AddInfrastructure), false },
{ nameof(AddServices), false },
};
public EGConfiguration AddLocalization(Action<IServiceCollection>? customLocalizationOptions = null)
{
_serviceRegs.Enqueue(customLocalizationOptions ?? (s => s.AddLocalization()));
_addingStatus[nameof(AddLocalization)] = true;
return this;
}
public EGConfiguration AddDistributedSqlServerCache(Action<SqlServerCacheOptions> setupAction)
{
_serviceRegs.Enqueue(s => s.AddDistributedSqlServerCache(setupAction));
_addingStatus[nameof(AddDistributedSqlServerCache)] = true;
return this;
}
public EGConfiguration AddInfrastructure(Action<EGInfrastructureConfiguration> options)
{
#pragma warning disable CS0618
_serviceRegs.Enqueue(s => s.AddEGInfrastructureServices(options));
#pragma warning restore CS0618
_addingStatus[nameof(AddInfrastructure)] = true;
return this;
}
public EGConfiguration AddServices(IConfiguration config)
{
#pragma warning disable CS0618
_serviceRegs.Enqueue(s => s.AddEnvelopeGeneratorServices(config));
#pragma warning restore CS0618
_addingStatus[nameof(AddServices)] = true;
return this;
}
internal void EnsureAllServicesConfigured()
{
var missingServices = _addingStatus
.Where(kv => !kv.Value)
.Select(kv => kv.Key)
.ToList();
if (missingServices.Count > 0)
{
var missingList = string.Join(", ", missingServices);
throw new InvalidOperationException(
$"Service configuration incomplete. The following required service methods were not called: {missingList}. " +
"Please ensure all necessary configuration methods are invoked before building the application.");
}
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,73 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
#if NETFRAMEWORK
using System;
#endif
namespace EnvelopeGenerator.Domain.Entities
{
[Table("TBDD_3RD_PARTY_MODULES")]
public class ThirdPartyModule
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("GUID")]
public int Id { get; set; }
[Required]
[Column("ACTIVE")]
public bool Active { get; set; }
[Required]
[StringLength(50)]
[Column("NAME")]
public string Name { get; set; }
[StringLength(500)]
[Column("DESCRIPTION")]
public string
#if nullable
?
#endif
Description { get; set; }
[Required]
[Column("LICENSE", TypeName = "varchar(max)")]
public string License { get; set; }
[Required]
[StringLength(20)]
[Column("VERSION")]
public string Version { get; set; }
[StringLength(50)]
[Column("ADDED_WHO")]
public string
#if nullable
?
#endif
AddedWho { get; set; }
[Column("ADDED_WHEN")]
public DateTime
#if nullable
?
#endif
AddedWhen { get; set; }
[StringLength(50)]
[Column("CHANGED_WHO")]
public string
#if nullable
?
#endif
ChangedWho { get; set; }
[Column("CHANGED_WHEN")]
public DateTime
#if nullable
?
#endif
ChangedWhen { get; set; }
}
}

View File

@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-EnvelopeGenerator.Finalizer-6d5cc618-4159-4ff2-b600-8a15fbfa8099</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
<PackageReference Include="Quartzmon" Version="1.0.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Quartz" Version="3.15.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.DependencyInjection\EnvelopeGenerator.DependencyInjection.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Database.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.Logging.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
namespace EnvelopeGenerator.Finalizer.Models;
public class GdPictureOptions
{
/// <summary>
///
/// </summary>
public string License { get; set; } = null!;
}

View File

@@ -0,0 +1,157 @@
using CommandDotNet.Execution;
using EnvelopeGenerator.Application.ThirdPartyModules.Queries;
using EnvelopeGenerator.DependencyInjection;
using EnvelopeGenerator.Finalizer;
using EnvelopeGenerator.Finalizer.Models;
using EnvelopeGenerator.Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Quartz;
using Quartz.AspNetCore;
using Quartz.Impl;
using Quartzmon;
using Serilog;
// Load Serilog from appsettings.json
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(new ConfigurationBuilder()
.AddJsonFile("appsettings.Logging.json", optional: false, reloadOnChange: true)
.Build())
.CreateLogger();
try
{
Log.Information("Application is starting...");
var builder = WebApplication.CreateBuilder(args);
#region Logging
builder.Logging.ClearProviders();
builder.Logging.AddSerilog();
#endregion
#region Configuration
var config = builder.Configuration;
Directory
.GetFiles(builder.Environment.ContentRootPath, "appsettings.*.json", SearchOption.TopDirectoryOnly)
.Where(file => Path.GetFileName(file) != $"appsettings.Development.json")
.Where(file => Path.GetFileName(file) != $"appsettings.migration.json")
.ToList()
.ForEach(file => config.AddJsonFile(file, true, true));
#endregion
#region Web API Services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
#endregion
#region AQuartz
builder.Services.AddQuartz(q =>
{
var name = $"{typeof(Worker).FullName}";
var jobKey = new JobKey(name);
q.AddJob<Worker>(opts => opts.WithIdentity(jobKey));
var expression = config[nameof(Worker) + ":CronExpression"];
if (string.IsNullOrWhiteSpace(expression))
throw new InvalidOperationException(
"Cron expression for the Worker job is not configured. " +
"Please provide a valid cron schedule in the configuration under " +
$"'{nameof(Worker)}:CronExpression'.");
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity(name + "-trigger")
.WithCronSchedule(expression));
});
builder.Services.AddQuartzServer(options =>
{
options.WaitForJobsToComplete = true;
});
builder.Services.AddQuartzmon();
builder.Services.AddSingleton(provider =>
provider.GetRequiredService<ISchedulerFactory>().GetScheduler().Result
);
#endregion
#region Add DB Context, EG Inf. and Services
var cnnStrName = "Default";
var connStr = config.GetConnectionString(cnnStrName)
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
builder.Services.AddEnvelopeGenerator(egOptions => egOptions
.AddLocalization()
.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = connStr;
options.SchemaName = "dbo";
options.TableName = "TBDD_CACHE";
})
.AddInfrastructure(opt =>
{
opt.AddDbTriggerParams(config);
opt.AddDbContext((provider, options) =>
{
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
var useInMemoryDb = config.GetValue<bool>("UseInMemoryDb");
var dbCtxOpt = useInMemoryDb ? options.UseInMemoryDatabase("EGInMemoryDb") : options.UseSqlServer(connStr);
dbCtxOpt.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
});
})
.AddServices(config)
);
#endregion Add DB Context, EG Inf. and Services
builder.Services.AddOptions<GdPictureOptions>()
.Configure((GdPictureOptions opt, IServiceProvider sp) =>
{
var licenseKey = "GDPICTURE";
using var scope = sp.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
opt.License = config["GdPictureLicenseKey"]
?? mediator.ReadThirdPartyModuleLicenseAsync(licenseKey).GetAwaiter().GetResult()
?? throw new InvalidOperationException($"License record not found for key: {licenseKey}");
});
var app = builder.Build();
#region Web API Middleware
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseQuartzmon(new QuartzmonOptions()
{
Scheduler = app.Services.GetRequiredService<IScheduler>(),
VirtualPathRoot = "/quartz"
});
app.MapControllers();
#endregion
app.Run();
Log.Information("The worker was stopped.");
}
catch (Exception ex)
{
Log.Fatal(ex, "Worker could not be started!");
}
finally
{
Log.CloseAndFlush();
}

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:17119",
"sslPort": 44321
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5010",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "quartz",
"applicationUrl": "https://localhost:7141;http://localhost:5010",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": false,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,26 @@
using EnvelopeGenerator.Finalizer.Models;
using Microsoft.Extensions.Options;
using Quartz;
namespace EnvelopeGenerator.Finalizer
{
public class Worker : IJob
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
public Task Execute(IJobExecutionContext context)
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,22 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"UseDbMigration": false,
"ConnectionStrings": {
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
"DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
},
"DbTriggerParams": {
"Envelope": [ "TBSIG_ENVELOPE_AFT_INS" ],
"History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
"Receiver": [],
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
},
"UseInMemoryDb": true
}

View File

@@ -2,7 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,81 @@
{
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Verbose",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Verbose-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Verbose",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Debug-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Debug",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Info-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Information",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Warning-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Warning",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Error-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Error",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Fatal-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Fatal",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
}
}

View File

@@ -0,0 +1,14 @@
{
"IgnoredLabels": {
"Label": [
"Date",
"Datum",
"ZIP",
"PLZ",
"Place",
"Ort",
"Position",
"Stellung"
]
}
}

View File

@@ -0,0 +1,21 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"UseDbMigration": false,
"ConnectionStrings": {
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
"DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
},
"DbTriggerParams": {
"Envelope": [ "TBSIG_ENVELOPE_AFT_INS" ],
"History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
"Receiver": [],
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
}
}

View File

@@ -2,8 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
}

View File

@@ -0,0 +1,81 @@
{
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Verbose",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Verbose-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Verbose",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Debug-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Debug",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Info-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Information",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Warning-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Warning",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Error-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Error",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "E:/LogFiles/Digital Data/signFlow.Finalizer/log.Fatal-.txt",
"rollingInterval": "Day",
"restrictedToMinimumLevel": "Fatal",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
}
}

View File

@@ -0,0 +1,14 @@
{
"IgnoredLabels": {
"Label": [
"Date",
"Datum",
"ZIP",
"PLZ",
"Place",
"Ort",
"Position",
"Stellung"
]
}
}

View File

@@ -0,0 +1,5 @@
{
"Worker": {
"CronExpression": "* * * * * ?"
}
}

View File

@@ -0,0 +1,3 @@
{
"GdPictureLicenseKey": "kG1Qf9PwmqgR8aDmIW2zI_ebj48RzqAJegRxcystEmkbTGQqfkNBdFOXIb6C_A00Ra8zZkrHdfjqzOPXK7kgkF2YDhvrqKfqh4WDug2vOt0qO31IommzkANSuLjZ4zmraoubyEVd25rE3veQ2h_j7tGIoH_LyIHmy24GaXsxdG0yCzIBMdiLbMMMDwcPY-809KeZ83Grv76OVhFvcbBWyYc251vou1N-kGg5_ZlHDgfWoY85gTLRxafjD3KS_i9ARW4BMiy36y8n7UP2jN8kGRnW_04ubpFtfjJqvtsrP_J9D0x7bqV8xtVtT5JI6dpKsVTiMgDCrIcoFSo5gCC1fw9oUopX4TDCkBQttO4-WHBlOeq9dG5Yb0otonVmJKaQA2tP6sMR-lZDs3ql_WI9t91yPWgpssrJUxSHDd27_LMTH_owJIqkF3NOJd9mYQuAv22oNKFYbH8e41pVKb8cT33Y9CgcQ_sy6YDA5PTuIRi67mjKge_nD9rd0IN213Ir9M_EFWqg9e4haWzIdHXQUo0md70kVhPX4UIH_BKJnxEEnFfoFRNMh77bB0N4jkcBEHPl-ghOERv8dOztf4vCnNpzzWvcLD2cqWIm6THy8XGGq9h4hp8aEreRleSMwv9QQAC7mjLwhQ1rBYkpUHlpTjhTLnMwHknl6HH0Z6zzmsgkRKVyfquv94Pd7QbQfZrRka0ss_48pf9p8hAywEn81Q=="
}

View File

@@ -105,6 +105,7 @@ try
});
builder.Services.AddOpenApi();
// TODO: Update to configure with EnvelopeGenerator.DependencyInjection
//AddEF Core dbcontext
var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration");
var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default";
@@ -185,7 +186,7 @@ try
// Envelope generator serives
#pragma warning disable CS0618 // Type or member is obsolete
builder.Services
.AddEnvelopeGeneratorInfrastructureServices(opt =>
.AddEGInfrastructureServices(opt =>
{
opt.AddDbTriggerParams(config);
opt.AddDbContext((provider, options) =>

View File

@@ -38,10 +38,10 @@ namespace EnvelopeGenerator.Infrastructure
/// will be created per HTTP request (or per scope) within the dependency injection container.
/// </remarks>
[Obsolete("Use IRepository")]
public static IServiceCollection AddEnvelopeGeneratorInfrastructureServices(this IServiceCollection services, Action<Config> options)
public static IServiceCollection AddEGInfrastructureServices(this IServiceCollection services, Action<EGInfrastructureConfiguration> options)
{
// configure custom options
options(new Config(services));
options(new EGInfrastructureConfiguration(services));
#if NET
services.TryAddScoped<IConfigRepository, ConfigRepository>();
services.TryAddScoped<IDocumentReceiverElementRepository, DocumentReceiverElementRepository>();
@@ -160,11 +160,11 @@ namespace EnvelopeGenerator.Infrastructure
}
#endif
public class Config
public class EGInfrastructureConfiguration
{
private readonly IServiceCollection _services;
internal Config(IServiceCollection services)
internal EGInfrastructureConfiguration(IServiceCollection services)
{
_services = services;
}

View File

@@ -79,6 +79,8 @@ public abstract class EGDbContextBase : DbContext
public DbSet<ClientUser> ClientUsers { get; set; }
public DbSet<ThirdPartyModule> ThirdPartyModules { get; set; }
private readonly DbTriggerParams _triggers;
private readonly ILogger

View File

@@ -525,7 +525,7 @@ namespace EnvelopeGenerator.Infrastructure.Migrations
});
});
modelBuilder.Entity("EnvelopeGenerator.Domain.Entities.Config", b =>
modelBuilder.Entity("EnvelopeGenerator.Domain.Entities.EGInfrastructureConfiguration", b =>
{
b.Property<string>("ExportPath")
.HasColumnType("nvarchar(256)")

View File

@@ -522,7 +522,7 @@ namespace EnvelopeGenerator.Infrastructure.Migrations
});
});
modelBuilder.Entity("EnvelopeGenerator.Domain.Entities.Config", b =>
modelBuilder.Entity("EnvelopeGenerator.Domain.Entities.EGInfrastructureConfiguration", b =>
{
b.Property<string>("ExportPath")
.HasColumnType("nvarchar(256)")

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -1,96 +0,0 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -1,30 +0,0 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@@ -1,105 +0,0 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -1,19 +0,0 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -1,36 +0,0 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -1,7 +0,0 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -1,64 +0,0 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -1,6 +0,0 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -1,10 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUI
@using EnvelopeGenerator.ReceiverUI.Components

View File

@@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -1,27 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View File

@@ -1,38 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:26087",
"sslPort": 44331
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7124;http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,51 +0,0 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,13 +0,0 @@
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>EnvelopeGenerator.ReceiverUIBlazor</AssemblyName>
<RootNamespace>EnvelopeGenerator.ReceiverUIBlazor</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -1,428 +0,0 @@
@page "/"
@using Microsoft.JSInterop
@inject IJSRuntime JS
@implements IAsyncDisposable
<div class="page-shell">
<h1>Sign PDF (Blazor)</h1>
<div class="controls">
<InputFile OnChange="HandleFileSelected" accept="application/pdf" />
<button class="btn" @onclick="ShowSignaturePad" disabled="@(!HasPdf)">Add signature</button>
<button class="btn" @onclick="() => ShowTextOverlay(false)" disabled="@(!HasPdf)">Add text</button>
<button class="btn" @onclick="() => ShowTextOverlay(true)" disabled="@(!HasPdf)">Add date</button>
<button class="btn" @onclick="Reset" disabled="@(!HasPdf)">Reset</button>
<button class="btn secondary" @onclick="Download" disabled="@(!HasPdf)">Download</button>
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<div class="error-banner">@ErrorMessage</div>
}
@if (!HasPdf)
{
<div class="drop-hint">Drop or select a PDF to start.</div>
}
@if (HasPdf)
{
<div class="document-shell" @ref="PdfHostRef" style="@($"width:{ViewportWidthPx}px")">
<canvas id="pdf-canvas" @ref="PdfCanvasRef"></canvas>
@if (ShowSignature)
{
<div class="overlay signature" @ref="OverlayRef"
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px; width:{OverlayWidthPx}px; height:{OverlayHeightPx}px;")"
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
<div class="overlay-controls">
<button class="overlay-btn" @onclick="ApplySignature" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
</div>
<img src="@SignatureDataUrl" draggable="false" />
</div>
}
@if (ShowText)
{
<div class="overlay text" @ref="OverlayRef"
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px;")"
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
<div class="overlay-controls">
<button class="overlay-btn" @onclick="ApplyText" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
</div>
<input class="overlay-input" @bind="TextValue" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation />
</div>
}
</div>
<div class="paging">
<button class="btn" @onclick="PrevPage" disabled="@(!CanPrev)">&lt;</button>
<span>Page @DisplayPage / @PageCount</span>
<button class="btn" @onclick="NextPage" disabled="@(!CanNext)">&gt;</button>
</div>
}
</div>
@if (ShowSignaturePadModal)
{
<div class="modal-backdrop">
<div class="modal">
<h3>Add signature</h3>
<canvas id="@SignatureCanvasId" width="700" height="220"></canvas>
<div class="modal-row">
<label><input type="checkbox" @bind="AutoDate" /> Auto date/time</label>
<button class="btn" @onclick="ClearSignature">Clear</button>
<span class="spacer"></span>
<button class="btn" @onclick="ConfirmSignature">Use signature</button>
<button class="btn secondary" @onclick="CloseSignaturePad">Cancel</button>
</div>
</div>
</div>
}
@code {
private ElementReference PdfCanvasRef;
private ElementReference PdfHostRef;
private ElementReference OverlayRef;
private string? PdfBase64;
private string? OriginalPdfBase64;
private DotNetObjectReference<Index>? _dotNetRef;
private int PageIndex;
private int PageCount;
private double ViewportWidthPx = 800;
private double ViewportHeightPx;
private bool ShowSignaturePadModal;
private bool ShowSignature;
private bool ShowText;
private string SignatureCanvasId { get; } = $"sig-{Guid.NewGuid():N}";
private string? SignatureDataUrl;
private bool AutoDate = true;
private double OverlayXpx = 20;
private double OverlayYpx = 20;
private double OverlayWidthPx = 200;
private double OverlayHeightPx = 80;
private bool IsDragging;
private double DragStartX;
private double DragStartY;
private double StartLeft;
private double StartTop;
private string TextValue = "Text";
private string? ErrorMessage;
private DateTimeOffset _lastDragRender = DateTimeOffset.MinValue;
private bool HasPdf => !string.IsNullOrWhiteSpace(PdfBase64);
private int DisplayPage => PageIndex + 1;
private bool CanPrev => PageIndex > 0;
private bool CanNext => PageIndex + 1 < PageCount;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
_dotNetRef ??= DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("pdfInterop.registerDropHandler", _dotNetRef);
}
if (ShowSignaturePadModal)
{
await JS.InvokeVoidAsync("pdfInterop.initSignaturePad", SignatureCanvasId);
}
}
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
if (e.FileCount == 0)
{
return;
}
await LoadPdfFromBrowserFile(e.File);
}
private async Task LoadPdfFromBrowserFile(IBrowserFile file)
{
ErrorMessage = null;
try
{
await using var stream = file.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
await LoadPdfFromBase64Internal(Convert.ToBase64String(ms.ToArray()));
}
catch (Exception ex)
{
ErrorMessage = $"Fehler beim Laden der PDF: {ex.Message}";
PdfBase64 = null;
PageCount = 0;
PageIndex = 0;
}
}
[JSInvokable]
public Task LoadPdfFromBase64(string base64)
{
return LoadPdfFromBase64Internal(base64);
}
private async Task LoadPdfFromBase64Internal(string base64)
{
ErrorMessage = null;
PdfBase64 = base64;
OriginalPdfBase64 = PdfBase64;
// Show the canvas before we start rendering
await InvokeAsync(StateHasChanged);
await Task.Yield();
// Make sure pdf.js is ready
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
PageCount = result.Pages;
PageIndex = 0;
await RenderPage();
}
private async Task RenderPage()
{
if (!HasPdf)
{
return;
}
try
{
var viewport = await JS.InvokeAsync<ViewportInfo>("pdfInterop.renderPage", PageIndex, "pdf-canvas", ViewportWidthPx);
ViewportWidthPx = viewport.Width;
ViewportHeightPx = viewport.Height;
StateHasChanged();
}
catch (Exception ex)
{
ErrorMessage = $"Fehler beim Rendern: {ex.Message}";
}
}
private async Task Reset()
{
ErrorMessage = null;
CloseOverlays();
ShowSignaturePadModal = false;
OverlayXpx = 20;
OverlayYpx = 20;
OverlayWidthPx = 200;
OverlayHeightPx = 80;
TextValue = "Text";
if (string.IsNullOrWhiteSpace(OriginalPdfBase64))
{
return;
}
PdfBase64 = OriginalPdfBase64;
PageIndex = 0;
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
PageCount = result.Pages;
await RenderPage();
}
private void CloseOverlays()
{
ShowSignature = false;
ShowText = false;
SignatureDataUrl = null;
}
private void ShowSignaturePad()
{
ShowSignaturePadModal = true;
}
private async Task ConfirmSignature()
{
SignatureDataUrl = await JS.InvokeAsync<string>("pdfInterop.getSignatureDataUrl", SignatureCanvasId);
if (string.IsNullOrWhiteSpace(SignatureDataUrl))
{
return;
}
OverlayWidthPx = 200;
OverlayHeightPx = 80;
OverlayXpx = 20;
OverlayYpx = 20;
ShowSignature = true;
ShowText = false;
ShowSignaturePadModal = false;
}
private void CloseSignaturePad()
{
ShowSignaturePadModal = false;
}
private void ClearSignature()
{
JS.InvokeVoidAsync("pdfInterop.clearSignaturePad", SignatureCanvasId);
}
private void ShowTextOverlay(bool autoDate)
{
TextValue = autoDate ? DateTimeOffset.Now.ToString("M/d/yyyy HH:mm:ss zzz") : "Text";
OverlayWidthPx = 240;
OverlayHeightPx = 40;
OverlayXpx = 20;
OverlayYpx = 20;
ShowText = true;
ShowSignature = false;
}
private void StartDrag(PointerEventArgs args)
{
IsDragging = true;
DragStartX = args.ClientX;
DragStartY = args.ClientY;
StartLeft = OverlayXpx;
StartTop = OverlayYpx;
if (OverlayRef.Context != null)
{
JS.InvokeVoidAsync("pdfInterop.capturePointer", OverlayRef, args.PointerId);
}
}
private void OnDrag(PointerEventArgs args)
{
if (!IsDragging)
{
return;
}
var dx = args.ClientX - DragStartX;
var dy = args.ClientY - DragStartY;
OverlayXpx = StartLeft + dx;
OverlayYpx = StartTop + dy;
var now = DateTimeOffset.UtcNow;
if (now - _lastDragRender > TimeSpan.FromMilliseconds(16))
{
_lastDragRender = now;
InvokeAsync(StateHasChanged);
}
}
private void EndDrag(PointerEventArgs args)
{
IsDragging = false;
if (OverlayRef.Context != null)
{
JS.InvokeVoidAsync("pdfInterop.releasePointer", OverlayRef, args.PointerId);
}
}
private async Task ApplySignature()
{
if (SignatureDataUrl is null || !HasPdf)
{
return;
}
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applySignature", new
{
base64 = PdfBase64,
pageIndex = PageIndex,
left = OverlayXpx,
top = OverlayYpx,
width = OverlayWidthPx,
height = OverlayHeightPx,
renderWidth = ViewportWidthPx,
renderHeight = ViewportHeightPx,
dataUrl = SignatureDataUrl,
autoDate = AutoDate,
});
CloseOverlays();
await RenderPage();
}
private async Task ApplyText()
{
if (string.IsNullOrWhiteSpace(TextValue) || !HasPdf)
{
return;
}
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applyText", new
{
base64 = PdfBase64,
pageIndex = PageIndex,
left = OverlayXpx,
top = OverlayYpx,
width = OverlayWidthPx,
height = OverlayHeightPx,
renderWidth = ViewportWidthPx,
renderHeight = ViewportHeightPx,
text = TextValue,
fontSize = 20,
});
CloseOverlays();
await RenderPage();
}
private void CancelOverlay()
{
CloseOverlays();
}
private async Task PrevPage()
{
if (!CanPrev)
{
return;
}
PageIndex--;
await RenderPage();
}
private async Task NextPage()
{
if (!CanNext)
{
return;
}
PageIndex++;
await RenderPage();
}
private async Task Download()
{
if (!HasPdf)
{
return;
}
await JS.InvokeVoidAsync("pdfInterop.downloadPdf", PdfBase64, "document-signed.pdf");
}
private record RenderResult(int Pages);
private record ViewportInfo(double Width, double Height, double PageWidth, double PageHeight);
public async ValueTask DisposeAsync()
{
_dotNetRef?.Dispose();
await Task.CompletedTask;
}
}

View File

@@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using EnvelopeGenerator.ReceiverUIBlazor;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>7f262ad4-53b1-42d3-9a5f-132cf50f150c</ProjectGuid>
<DesktopBuildPackageLocation>E:\TekH\Visual Studio\src\EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>ReceiverUIBlazor</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -1,12 +0,0 @@
{
"profiles": {
"EnvelopeGenerator.ReceiverUIBlazor": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:49582;http://localhost:49583"
}
}
}

View File

@@ -1,10 +0,0 @@
@inherits LayoutComponentBase
<div class="main-layout">
<header class="top-bar">
<div class="brand">Receiver UI (Blazor)</div>
</header>
<main class="content">
@Body
</main>
</div>

View File

@@ -1,8 +0,0 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUIBlazor
@using EnvelopeGenerator.ReceiverUIBlazor.Shared

View File

@@ -1,241 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Red+Hat+Text:wght@400;500;600;700&family=Teko:wght@500;600&display=swap');
:root {
--bg: #f7f7f8;
--bg-strong: #fff6f6;
--text: #474747;
--muted: #777777;
--border: #e7e7e7;
--shadow: 0 18px 55px rgba(20, 20, 20, 0.08);
--card: #ffffff;
--accent: #a52431;
--accent-strong: #8d1e2a;
--accent-soft: #f8e5e8;
--highlight: #ffd62f;
--danger: #a52431;
}
body {
margin: 0;
font-family: "Red Hat Text", "Segoe UI", system-ui, -apple-system, sans-serif;
background: radial-gradient(120% 120% at 6% 12%, var(--bg-strong) 0%, #fffdf7 45%, var(--bg) 85%);
color: var(--text);
line-height: 1.5;
}
.main-layout {
min-height: 100vh;
}
.top-bar {
display: flex;
align-items: center;
padding: 14px 24px;
background: var(--accent);
border-bottom: 1px solid var(--accent-strong);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
color: #ffd62f;
}
.top-bar .brand {
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
letter-spacing: 0.6px;
color: #ffd62f;
}
.content {
padding: 28px 32px 40px;
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
h1 {
margin: 0 0 10px;
letter-spacing: 0.2px;
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 14px;
justify-content: center;
}
.btn {
background: #4a4a4a;
color: #ffffff;
border: 1px solid #404040;
padding: 10px 15px;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
background: #3f3f3f;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
box-shadow: none;
}
.btn.secondary {
background: #f4f4f4;
color: #474747;
border: 1px solid #d3d3d3;
box-shadow: none;
}
.btn.secondary:hover:not(:disabled) {
background: #e9e9e9;
transform: translateY(-1px);
}
.drop-hint {
padding: 26px;
border: 1px dashed var(--border);
border-radius: 14px;
text-align: center;
color: var(--muted);
background: #ffffff;
width: min(1100px, 100%);
margin: 0 auto;
box-shadow: 0 8px 18px rgba(165, 36, 49, 0.06);
}
.error-banner {
margin-top: 10px;
padding: 12px 14px;
border-radius: 10px;
background: #fcebec;
border: 1px solid #f3c6cd;
color: var(--accent-strong);
}
.document-shell {
position: relative;
margin-top: 14px;
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--card);
border: 1px solid var(--border);
margin-left: auto;
margin-right: auto;
}
canvas {
display: block;
width: 100%;
height: auto;
background: #ffffff;
}
.overlay {
position: absolute;
border: 1px dashed var(--accent);
border-radius: 10px;
padding: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 12px 30px rgba(165, 36, 49, 0.15);
user-select: none;
touch-action: none;
}
.overlay.signature img {
display: block;
max-width: 100%;
max-height: 100%;
}
.overlay-controls {
position: absolute;
top: -44px;
right: 0;
display: flex;
gap: 6px;
}
.overlay-btn {
background: #ffffff;
color: var(--accent-strong);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
box-shadow: 0 6px 16px rgba(165, 36, 49, 0.16);
}
.overlay-input {
border: 1px solid var(--border);
background: #ffffff;
color: var(--text);
font-size: 18px;
padding: 6px 8px;
min-width: 180px;
border-radius: 8px;
outline: none;
}
.paging {
margin-top: 14px;
display: flex;
gap: 10px;
align-items: center;
color: var(--muted);
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.35);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 18px;
min-width: 760px;
box-shadow: var(--shadow);
}
.modal-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
color: var(--muted);
}
.modal canvas {
background: #ffffff;
border: 1px solid var(--border);
border-radius: 10px;
}
.spacer {
flex: 1;
}

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Receiver UI (Blazor)</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<!-- pdf.js 3.11 UMD + classic worker for compatibility; SRI removed to avoid digest mismatches -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"></script>
<script src="js/pdfInterop.js"></script>
</head>
<body>
<div id="app">Loading...</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -1,339 +0,0 @@
(function () {
// Stick to pdf.js 3.11 UMD + classic worker for compatibility.
const PDF_JS_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
const WORKER_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
const state = {
pdfDoc: null,
pdfBytes: null,
lastViewport: null,
pdfJsReady: null,
};
function base64ToUint8(base64) {
const binStr = atob(base64);
const len = binStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binStr.charCodeAt(i);
}
return bytes;
}
async function reloadFromBase64(base64) {
state.pdfBytes = base64ToUint8(base64);
state.pdfDoc = await pdfjsLib.getDocument({ data: state.pdfBytes }).promise;
return { pages: state.pdfDoc.numPages };
}
function dataUrlDownload(dataUrl, filename) {
const a = document.createElement('a');
a.href = dataUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
const pointerPads = new Map();
function loadScriptOnce(url) {
return new Promise((resolve, reject) => {
// If already present, resolve immediately
const existing = Array.from(document.getElementsByTagName('script')).find(s => s.src === url);
if (existing && existing.dataset.loaded === "true") {
resolve();
return;
}
const script = existing || document.createElement('script');
script.src = url;
script.defer = true;
script.onload = () => {
script.dataset.loaded = "true";
resolve();
};
script.onerror = (e) => reject(new Error(`Script load failed: ${url}`));
if (!existing) {
document.head.appendChild(script);
}
});
}
async function ensurePdfJsLoaded() {
if (typeof pdfjsLib !== "undefined") {
return;
}
if (!state.pdfJsReady) {
state.pdfJsReady = loadScriptOnce(PDF_JS_SRC);
}
await state.pdfJsReady;
if (typeof pdfjsLib === "undefined") {
throw new Error("pdfjsLib could not be loaded");
}
}
window.pdfInterop = {
ensureReady: async () => {
// Ensure pdf.js is present and the worker path is set explicitly.
await ensurePdfJsLoaded();
if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
if (pdfjsLib.GlobalWorkerOptions.workerSrc !== WORKER_SRC) {
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
}
} else {
throw new Error("pdf.js not available after load");
}
},
loadPdf: async (base64) => {
await ensurePdfJsLoaded();
try {
const result = await reloadFromBase64(base64);
if (!result || !result.pages) {
throw new Error("PDF has keine Seiten erkannt");
}
return result;
} catch (err) {
console.error("pdfInterop.loadPdf failed", err);
throw err;
}
},
renderPage: async (pageIndex, canvasId, targetWidth) => {
await ensurePdfJsLoaded();
if (!state.pdfDoc) {
throw new Error('PDF not loaded');
}
const page = await state.pdfDoc.getPage(pageIndex + 1);
const rawViewport = page.getViewport({ scale: 1 });
const scale = targetWidth / rawViewport.width;
const viewport = page.getViewport({ scale });
let canvas = document.getElementById(canvasId);
if (!canvas) {
// give the UI a tiny delay to render the canvas into the DOM
await new Promise(r => setTimeout(r, 40));
canvas = document.getElementById(canvasId);
}
if (!canvas) {
console.error("renderPage: canvas not found", canvasId);
throw new Error('Canvas not found');
}
const ctx = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
state.lastViewport = {
width: viewport.width,
height: viewport.height,
pageWidth: rawViewport.width,
pageHeight: rawViewport.height,
};
return state.lastViewport;
},
applySignature: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
dataUrl,
autoDate,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const pngImage = await pdfDoc.embedPng(dataUrl);
const drawWidth = width * scaleX;
const drawHeight = height * scaleY;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawImage(pngImage, {
x,
y,
width: drawWidth,
height: drawHeight,
});
if (autoDate) {
const text = `Signed ${new Date().toLocaleString()}`;
page.drawText(text, {
x,
y: y - 14 * scaleY,
size: 14 * scaleX,
color: PDFLib.rgb(0.11, 0.25, 0.56),
});
}
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
applyText: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
text,
fontSize,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawText(text, {
x,
y,
size: fontSize * scaleX,
color: PDFLib.rgb(0.2, 0.23, 0.28),
});
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
downloadPdf: (base64, filename) => {
dataUrlDownload(`data:application/pdf;base64,${base64}`, filename);
},
initSignaturePad: (canvasId) => {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = '#1c3d8f';
const padState = {
drawing: false,
lastX: 0,
lastY: 0,
};
function getPos(evt) {
const rect = canvas.getBoundingClientRect();
const scaleX = rect.width ? canvas.width / rect.width : 1;
const scaleY = rect.height ? canvas.height / rect.height : 1;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY,
};
}
function start(e) {
padState.drawing = true;
const pos = getPos(e);
padState.lastX = pos.x;
padState.lastY = pos.y;
}
function move(e) {
if (!padState.drawing) return;
const pos = getPos(e);
const x = pos.x;
const y = pos.y;
ctx.beginPath();
ctx.moveTo(padState.lastX, padState.lastY);
ctx.lineTo(x, y);
ctx.stroke();
padState.lastX = x;
padState.lastY = y;
}
function end() {
padState.drawing = false;
}
canvas.onpointerdown = start;
canvas.onpointermove = move;
canvas.onpointerup = end;
canvas.onpointerleave = end;
pointerPads.set(canvasId, { ctx, canvas });
},
registerDropHandler: (dotNetRef) => {
if (window.__pdfDropRegistered) return;
window.__pdfDropRegistered = true;
const prevent = (e) => {
e.preventDefault();
e.stopPropagation();
};
['dragenter', 'dragover', 'dragleave'].forEach(evt => {
document.addEventListener(evt, prevent, false);
});
document.addEventListener('drop', (e) => {
prevent(e);
const files = e.dataTransfer?.files;
if (!files || files.length === 0) {
return;
}
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
if (typeof result === 'string') {
const base64 = result.split(',')[1] || result;
dotNetRef?.invokeMethodAsync('LoadPdfFromBase64', base64);
}
};
reader.readAsDataURL(file);
}, false);
},
clearSignaturePad: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return;
pad.ctx.clearRect(0, 0, pad.canvas.width, pad.canvas.height);
},
getSignatureDataUrl: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return null;
return pad.canvas.toDataURL('image/png');
},
capturePointer: (element, pointerId) => {
if (element && element.setPointerCapture) {
try {
element.setPointerCapture(pointerId);
} catch (err) {
console.warn('capturePointer failed', err);
}
}
},
releasePointer: (element, pointerId) => {
if (element && element.releasePointerCapture) {
try {
element.releasePointerCapture(pointerId);
} catch (err) {
console.warn('releasePointer failed', err);
}
}
}
};
})();

View File

@@ -1,52 +0,0 @@
{
"homepage": "https://slavik0329.github.io/pdf-sign",
"name": "pdf-sign",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"annotpdf": "^1.0.12",
"dayjs": "^1.9.6",
"pdf-lib": "^1.12.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.3",
"react-dropzone": "^11.2.4",
"react-icons": "^4.1.0",
"react-pdf": "^5.0.0",
"react-scripts": "4.0.1",
"react-signature-canvas": "^1.0.3",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^3.1.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Finally, a simple way to put your signature on a PDF for free."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>PDF-sign</title>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,10 +0,0 @@
body {
font-family: "Open Sans";
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none;
}

View File

@@ -1,278 +0,0 @@
import "./App.css";
import { useRef, useState } from "react";
import Drop from "./Drop";
import { Document, Page, pdfjs } from "react-pdf";
import { PDFDocument, rgb } from "pdf-lib";
import { blobToURL } from "./utils/Utils";
import PagingControl from "./components/PagingControl";
import { AddSigDialog } from "./components/AddSigDialog";
import { Header } from "./Header";
import { BigButton } from "./components/BigButton";
import DraggableSignature from "./components/DraggableSignature";
import DraggableText from "./components/DraggableText";
import dayjs from "dayjs";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
function downloadURI(uri, name) {
var link = document.createElement("a");
link.download = name;
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function App() {
const styles = {
container: {
maxWidth: 900,
margin: "0 auto",
},
sigBlock: {
display: "inline-block",
border: "1px solid #000",
},
documentBlock: {
maxWidth: 800,
margin: "20px auto",
marginTop: 8,
border: "1px solid #999",
},
controls: {
maxWidth: 800,
margin: "0 auto",
marginTop: 8,
},
};
const [pdf, setPdf] = useState(null);
const [autoDate, setAutoDate] = useState(true);
const [signatureURL, setSignatureURL] = useState(null);
const [position, setPosition] = useState(null);
const [signatureDialogVisible, setSignatureDialogVisible] = useState(false);
const [textInputVisible, setTextInputVisible] = useState(false);
const [pageNum, setPageNum] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [pageDetails, setPageDetails] = useState(null);
const documentRef = useRef(null);
return (
<div>
<Header />
<div style={styles.container}>
{signatureDialogVisible ? (
<AddSigDialog
autoDate={autoDate}
setAutoDate={setAutoDate}
onClose={() => setSignatureDialogVisible(false)}
onConfirm={(url) => {
setSignatureURL(url);
setSignatureDialogVisible(false);
}}
/>
) : null}
{!pdf ? (
<Drop
onLoaded={async (files) => {
const URL = await blobToURL(files[0]);
setPdf(URL);
}}
/>
) : null}
{pdf ? (
<div>
<div style={styles.controls}>
{!signatureURL ? (
<BigButton
marginRight={8}
title={"Add signature"}
onClick={() => setSignatureDialogVisible(true)}
/>
) : null}
<BigButton
marginRight={8}
title={"Add Date"}
onClick={() => setTextInputVisible("date")}
/>
<BigButton
marginRight={8}
title={"Add Text"}
onClick={() => setTextInputVisible(true)}
/>
<BigButton
marginRight={8}
title={"Reset"}
onClick={() => {
setTextInputVisible(false);
setSignatureDialogVisible(false);
setSignatureURL(null);
setPdf(null);
setTotalPages(0);
setPageNum(0);
setPageDetails(null);
}}
/>
{pdf ? (
<BigButton
marginRight={8}
inverted={true}
title={"Download"}
onClick={() => {
downloadURI(pdf, "file.pdf");
}}
/>
) : null}
</div>
<div ref={documentRef} style={styles.documentBlock}>
{textInputVisible ? (
<DraggableText
initialText={
textInputVisible === "date"
? dayjs().format("M/d/YYYY")
: null
}
onCancel={() => setTextInputVisible(false)}
onEnd={setPosition}
onSet={async (text) => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y +
(12 * scale) -
position.offsetY -
documentRef.current.offsetTop);
const x =
position.x -
166 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
firstPage.drawText(text, {
x: newX,
y: newY,
size: 20 * scale,
});
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setTextInputVisible(false);
}}
/>
) : null}
{signatureURL ? (
<DraggableSignature
url={signatureURL}
onCancel={() => {
setSignatureURL(null);
}}
onSet={async () => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y -
position.offsetY +
64 -
documentRef.current.offsetTop);
const x =
position.x -
160 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
const pngImage = await pdfDoc.embedPng(signatureURL);
const pngDims = pngImage.scale( scale * .3);
firstPage.drawImage(pngImage, {
x: newX,
y: newY,
width: pngDims.width,
height: pngDims.height,
});
if (autoDate) {
firstPage.drawText(
`Signed ${dayjs().format(
"M/d/YYYY HH:mm:ss ZZ"
)}`,
{
x: newX,
y: newY - 10,
size: 14 * scale,
color: rgb(0.074, 0.545, 0.262),
}
);
}
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setSignatureURL(null);
}}
onEnd={setPosition}
/>
) : null}
<Document
file={pdf}
onLoadSuccess={(data) => {
setTotalPages(data.numPages);
}}
>
<Page
pageNumber={pageNum + 1}
width={800}
height={1200}
onLoadSuccess={(data) => {
setPageDetails(data);
}}
/>
</Document>
</div>
<PagingControl
pageNum={pageNum}
setPageNum={setPageNum}
totalPages={totalPages}
/>
</div>
) : null}
</div>
</div>
);
}
export default App;

View File

@@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,37 +0,0 @@
import React, { useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { cleanBorder, primary45 } from "./utils/colors";
export default function Drop({ onLoaded }) {
const styles = {
container: {
textAlign: "center",
border: cleanBorder,
padding: 20,
marginTop: 12,
color: primary45,
fontSize: 18,
fontWeight: 600,
borderRadius: 4,
userSelect: "none",
outline: 0,
cursor: "pointer",
},
};
const onDrop = useCallback((acceptedFiles) => {
onLoaded(acceptedFiles);
// Do something with the files
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: "application/pdf",
});
return (
<div {...getRootProps()} style={styles.container}>
<input {...getInputProps()} />
{isDragActive ? <p>Drop a PDF here</p> : <p>Drag a PDF here</p>}
</div>
);
}

View File

@@ -1,15 +0,0 @@
import {primary45} from "./utils/colors";
export function Header() {
const styles = {
container: {
backgroundColor: primary45,
color: '#FFF',
padding: 12,
fontWeight: 600,
}
}
return <div style={styles.container}>
<div>Open PDF Sign</div>
</div>
}

View File

@@ -1,77 +0,0 @@
import { Dialog } from "./Dialog";
import SignatureCanvas from "react-signature-canvas";
import { ConfirmOrCancel } from "./ConfirmOrCancel";
import { primary45 } from "../utils/colors";
import { useRef } from "react";
export function AddSigDialog({ onConfirm, onClose, autoDate, setAutoDate }) {
const sigRef = useRef(null);
const styles = {
sigContainer: {
display: "flex",
justifyContent: "center",
},
sigBlock: {
display: "inline-block",
border: `1px solid ${primary45}`,
},
instructions: {
display: "flex",
justifyContent: "space-between",
textAlign: "center",
color: primary45,
marginTop: 8,
width: 600,
alignSelf: "center",
},
instructionsContainer: {
display: "flex",
justifyContent: "center",
},
};
return (
<Dialog
isVisible={true}
title={"Add signature"}
body={
<div style={styles.container}>
<div style={styles.sigContainer}>
<div style={styles.sigBlock}>
<SignatureCanvas
velocityFilterWeight={1}
ref={sigRef}
canvasProps={{
width: "600",
height: 200,
className: "sigCanvas",
}}
/>
</div>
</div>
<div style={styles.instructionsContainer}>
<div style={styles.instructions}>
<div>
Auto date/time{" "}
<input
type={"checkbox"}
checked={autoDate}
onChange={(e) => setAutoDate(e.target.checked)}
/>
</div>
<div>Draw your signature above</div>
</div>
</div>
<ConfirmOrCancel
onCancel={onClose}
onConfirm={() => {
const sigURL = sigRef.current.toDataURL();
onConfirm(sigURL);
}}
/>
</div>
}
/>
);
}

View File

@@ -1,81 +0,0 @@
import React from "react";
import { primary45 } from "../utils/colors";
import useHover from "../hooks/useHover";
export function BigButton({
title,
onClick,
inverted,
fullWidth,
customFillColor,
customWhiteColor,
style,
noHover,
id,
small,
disabled,
marginRight,
}) {
const [hoverRef, isHovered] = useHover();
let fillColor = customFillColor || primary45;
const whiteColor = customWhiteColor || "#FFF";
let initialBg = null;
let hoverBg = fillColor;
let initialColor = fillColor;
let hoverColor = whiteColor;
if (inverted) {
initialBg = fillColor;
hoverBg = null;
initialColor = whiteColor;
hoverColor = fillColor;
}
if (disabled) {
initialBg = "#ddd";
hoverBg = "#ddd";
fillColor = "#ddd";
}
const styles = {
container: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: fullWidth ? "100%" : null,
backgroundColor: isHovered && !noHover ? hoverBg : initialBg,
color:
isHovered && !noHover && !disabled
? hoverColor
: disabled
? "#999"
: initialColor,
borderRadius: 4,
padding: small ? "2px 4px" : "6px 8px",
fontSize: small ? 14 : null,
border: `1px solid ${fillColor}`,
cursor: !disabled ? "pointer" : null,
userSelect: "none",
boxSizing: "border-box",
marginRight,
},
};
return (
<div
id={id}
ref={hoverRef}
style={{ ...styles.container, ...style }}
onClick={() => {
if (!disabled) {
onClick();
}
}}
>
{title}
</div>
);
}

View File

@@ -1,37 +0,0 @@
import { BigButton } from "./BigButton";
import React from "react";
export function ConfirmOrCancel({
onCancel,
onConfirm,
confirmTitle = "Confirm",
leftBlock,
hideCancel,
disabled
}) {
const styles = {
actions: {
display: "flex",
justifyContent: "space-between",
},
cancel: {
marginRight: 8,
},
};
return (
<div style={styles.actions}>
<div>{leftBlock}</div>
<div>
{!hideCancel ? (
<BigButton
title={"Cancel"}
style={styles.cancel}
onClick={onCancel}
/>
) : null}
<BigButton title={confirmTitle} inverted={true} onClick={onConfirm} disabled={disabled}/>
</div>
</div>
);
}

View File

@@ -1,56 +0,0 @@
import React from 'react';
import {primary45} from '../utils/colors';
import {FaTimes} from 'react-icons/fa';
import {Modal} from './Modal';
export function Dialog({
isVisible,
body,
onClose,
title,
noPadding,
backgroundColor,
positionTop,
style,
}) {
if (!isVisible) {
return null;
}
const styles = {
header: {
backgroundColor: primary45,
color: '#FFF',
padding: 8,
fontSize: 14,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
body: {
padding: noPadding ? 0 : 14,
backgroundColor: backgroundColor ? backgroundColor : '#FFF',
},
xIcon: {
cursor: 'pointer',
},
};
return (
<Modal onClose={onClose} isVisible={isVisible} positionTop={positionTop} style={style}>
<div style={styles.container}>
<div style={styles.header}>
<div>{title}</div>
<FaTimes
color={'#FFF'}
size={16}
style={styles.xIcon}
className={'dialogClose'}
onClick={onClose}
/>
</div>
<div style={styles.body}>{body}</div>
</div>
</Modal>
);
}

View File

@@ -1,37 +0,0 @@
import Draggable from "react-draggable";
import {BigButton} from "./BigButton"; // The default
import {FaCheck, FaTimes} from 'react-icons/fa'
import {cleanBorder, errorColor, goodColor, primary45} from "../utils/colors";
export default function DraggableSignature({ url, onEnd, onSet, onCancel }) {
const styles = {
container: {
position: 'absolute',
zIndex: 100000,
border: `2px solid ${primary45}`,
},
controls: {
position: 'absolute',
right: 0,
display: 'inline-block',
backgroundColor: primary45,
// borderRadius: 4,
},
smallButton: {
display: 'inline-block',
cursor: 'pointer',
padding: 4,
}
}
return (
<Draggable onStop={onEnd}>
<div style={styles.container}>
<div style={styles.controls}>
<div style={styles.smallButton} onClick={onSet}><FaCheck color={goodColor}/></div>
<div style={styles.smallButton} onClick={onCancel}><FaTimes color={errorColor}/></div>
</div>
<img src={url} width={200} style={styles.img} draggable={false} />
</div>
</Draggable>
);
}

View File

@@ -1,66 +0,0 @@
import Draggable from "react-draggable";
import { FaCheck, FaTimes } from "react-icons/fa";
import { cleanBorder, errorColor, goodColor, primary45 } from "../utils/colors";
import { useState, useEffect, useRef } from "react";
export default function DraggableText({ onEnd, onSet, onCancel, initialText }) {
const [text, setText] = useState("Text");
const inputRef = useRef(null);
useEffect(() => {
if (initialText) {
setText(initialText)
} else {
inputRef.current.focus();
inputRef.current.select()
}
}, [])
const styles = {
container: {
position: "absolute",
zIndex: 100000,
border: `2px solid ${primary45}`,
},
controls: {
position: "absolute",
right: 0,
display: "inline-block",
backgroundColor: primary45,
// borderRadius: 4,
},
smallButton: {
display: "inline-block",
cursor: "pointer",
padding: 4,
},
input: {
border: 0,
fontSize: 20,
padding: 3,
backgroundColor: 'rgba(0,0,0,0)',
cursor: 'move'
}
};
return (
<Draggable onStop={onEnd}>
<div style={styles.container}>
<div style={styles.controls}>
<div style={styles.smallButton} onClick={()=>onSet(text)}>
<FaCheck color={goodColor} />
</div>
<div style={styles.smallButton} onClick={onCancel}>
<FaTimes color={errorColor} />
</div>
</div>
<input
ref={inputRef}
style={styles.input}
value={text}
placeholder={'Text'}
onChange={(e) => setText(e.target.value)}
/>
</div>
</Draggable>
);
}

View File

@@ -1,43 +0,0 @@
import React from 'react';
import {primary45} from '../utils/colors';
import {useIsSmallScreen} from '../hooks/useIsSmallScreen';
export function Modal({onClose, children, isVisible, style, positionTop}) {
const isSmallScreen = useIsSmallScreen();
const styles = {
container: {
position: isSmallScreen ? 'fixed' : 'absolute',
backgroundColor: '#FFF',
border: `1px solid ${primary45}`,
borderRadius: 4,
top: positionTop ? positionTop : isSmallScreen ? 60 : 150,
left: '50%',
transform: 'translateX(-50%)',
width: '94%',
fontFamily: 'Open Sans',
zIndex: 10000,
boxShadow: '0 0px 14px hsla(0, 0%, 0%, 0.2)',
},
background: {
position: 'fixed',
width: '100%',
height: '100%',
top: 0,
left: 0,
backgroundColor: '#00000033',
zIndex: 5000,
},
};
if (!isVisible) {
return null;
}
return (
<div style={styles.outer}>
<div style={styles.background} onClick={onClose} />
<div style={{...styles.container, ...style}}>{children}</div>
</div>
);
}

View File

@@ -1,40 +0,0 @@
import { BigButton } from "./BigButton";
import {primary45} from "../utils/colors";
export default function PagingControl({totalPages, pageNum, setPageNum}) {
const styles= {
container: {
marginTop: 8,
marginBottom: 8,
},
inlineFlex: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
pageInfo: {
padding: 8,
color: primary45,
fontSize: 14,
}
}
return (
<div style={styles.container}>
<div style={styles.inlineFlex}>
<BigButton
title={"<"}
onClick={() => setPageNum(pageNum - 1)}
disabled={pageNum-1===-1}
/>
<div style={styles.pageInfo}>
Page: {pageNum + 1}/{totalPages}
</div>
<BigButton
title={">"}
onClick={() => setPageNum(pageNum + 1)}
disabled={pageNum+1>totalPages-1}
/>
</div>
</div>
);
}

View File

@@ -1,29 +0,0 @@
import React, {useCallback, useRef, useState} from 'react';
export default function useHover() {
const [value, setValue] = useState(false);
const handleMouseOver = useCallback(() => setValue(true), []);
const handleMouseOut = useCallback(() => setValue(false), []);
const ref = useRef();
const callbackRef = useCallback(
(node) => {
if (ref.current) {
ref.current.removeEventListener('mouseenter', handleMouseOver);
ref.current.removeEventListener('mouseleave', handleMouseOut);
}
ref.current = node;
if (ref.current) {
ref.current.addEventListener('mouseenter', handleMouseOver);
ref.current.addEventListener('mouseleave', handleMouseOut);
}
},
[handleMouseOver, handleMouseOut],
);
return [callbackRef, value];
}

View File

@@ -1,6 +0,0 @@
import {useWindowSize} from './useWindowSize';
export function useIsSmallScreen() {
const windowSize = useWindowSize();
return windowSize.width < 600;
}

View File

@@ -1,29 +0,0 @@
import React, {useState, useEffect} from 'react';
export function useWindowSize() {
const isClient = typeof window === 'object';
function getSize() {
return {
width: isClient ? window.innerWidth : undefined,
height: isClient ? window.innerHeight : undefined,
};
}
const [windowSize, setWindowSize] = useState(getSize);
useEffect(() => {
if (!isClient) {
return false;
}
function handleResize() {
setWindowSize(getSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount and unmount
return windowSize;
}

View File

@@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -1,27 +0,0 @@
export function blobToURL(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
const base64data = reader.result;
resolve(base64data);
};
});
}
export async function fileToBlob(file, handleUpdate) {
const { content, size } = file;
let chunks = [];
let i = 0;
const totalCount = Math.round(size / 250000);
for await (const chunk of content) {
if (handleUpdate) {
handleUpdate(i, totalCount);
}
chunks.push(chunk);
i++;
}
// eslint-disable-next-line no-undef
return new Blob(chunks);
}

View File

@@ -1,28 +0,0 @@
export const primary = '#2b6284';
export const primary2 = '#ecf4f9';
export const primary3 = '#9fc7e0';
export const primary35 = '#97bace';
export const primary4 = 'hsl(204,38%,55%)';
export const primary45 = 'hsl(218,49%,66%)';
export const primary46 = '#6778cb';
export const primary15 = 'rgb(241 249 255)';
export const primary5 = '#3881ad';
export const primary6 = '#132b3a';
// export const primary = '#666';
// export const primary2 = '#EEE';
// export const primary3 = '#CCC';
// export const primary4 = '#AAA';
// export const primary5 = '#888';
// export const primary6 = '#333';
export const primary16 = 'hsl(208 100% 96% / 1)';
export const errorColor = '#ef6565';
export const lightErrorColor = '#ef9c9c';
export const goodColor = '#53c171';
export const cleanBorder = '1px solid rgb(208, 227, 239)';
export const lightBorder = 'hsl(203 51% 80% / 1)';

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@ 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.Web.Extensions;
using MediatR;
@@ -12,6 +11,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Dynamic;
namespace EnvelopeGenerator.Web.Controllers;
@@ -58,9 +58,7 @@ public class AnnotationController : ControllerBase
// Again check if receiver has already signed
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
return Problem(statusCode: 409);
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
return Problem(statusCode: 423);
return Problem(statusCode: 403);
var docSignedNotification = await _mediator
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
@@ -73,7 +71,7 @@ public class AnnotationController : ControllerBase
return Ok();
}
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("reject")]
[Obsolete("Use DigitalData.Core.Exceptions and .Middleware")]

View File

@@ -95,23 +95,6 @@ public class EnvelopeController : ViewControllerBase
}
#endregion
#region UseAccessCode
if (!er.Envelope!.UseAccessCode)
{
(string? uuid, string? signature) = decoded.ParseEnvelopeReceiverId();
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid!, signature: signature!);
if (er_secret_res.IsFailed)
{
_logger.LogNotice(er_secret_res.Notices);
return this.ViewEnvelopeNotFound();
}
var er_secret = er_secret_res.Data;
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.FullyAuth);
return await CreateShowEnvelopeView(er_secret);
}
#endregion UseAccessCode
#region Send Access Code
bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress);
if (!accessCodeAlreadyRequested)
@@ -138,7 +121,7 @@ public class EnvelopeController : ViewControllerBase
[HttpPost("{envelopeReceiverId}")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth, CancellationToken cancel)
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth)
{
try
{
@@ -162,15 +145,6 @@ public class EnvelopeController : ViewControllerBase
}
var er_secret = er_secret_res.Data;
//check rejection
var rejRcvrs = await _historyService.ReadRejectingReceivers(er_secret.Envelope!.Id);
if (rejRcvrs.Any())
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
ViewBag.IsExt = !rejRcvrs.Contains(er_secret.Receiver); //external if the current user is not rejected
return View("EnvelopeRejected", er_secret);
}
// show envelope if already logged in
if (User.IsInRole(ReceiverRole.FullyAuth))
return await CreateShowEnvelopeView(er_secret);
@@ -216,7 +190,7 @@ public class EnvelopeController : ViewControllerBase
return this.ViewInnerServiceError();
}
}
private async Task<IActionResult> CreateEnvelopeLockedView(EnvelopeReceiverDto er, CancellationToken cancel)
{
var uuidClaim = User.GetAuthEnvelopeUuid();

View File

@@ -1,49 +0,0 @@
using Microsoft.AspNetCore.Authentication.Cookies;
namespace EnvelopeGenerator.Web;
public class EnvelopeCookieManager : ICookieManager
{
private readonly IEnumerable<string> _envelopeKeyBasedCookieNames;
private readonly ChunkingCookieManager _inner = new();
public EnvelopeCookieManager(params string[] envelopeKeyBasedCookieNames)
{
_envelopeKeyBasedCookieNames = envelopeKeyBasedCookieNames;
}
private string GetCookieName(HttpContext context, string key)
{
if (!_envelopeKeyBasedCookieNames.Contains(key))
return key;
var envId = context.GetRouteValue("envelopeReceiverId")?.ToString();
if (string.IsNullOrEmpty(envId) && context.Request.Query.TryGetValue("envKey", out var envKeyValue))
envId = envKeyValue;
if (string.IsNullOrEmpty(envId))
return key;
return $"{key}-{envId}";
}
public string? GetRequestCookie(HttpContext context, string key)
{
var cookieName = GetCookieName(context, key);
return _inner.GetRequestCookie(context, cookieName);
}
public void AppendResponseCookie(HttpContext context, string key, string? value, CookieOptions options)
{
var cookieName = GetCookieName(context, key);
_inner.AppendResponseCookie(context, cookieName, value, options);
}
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
{
var cookieName = GetCookieName(context, key);
_inner.DeleteCookie(context, cookieName, options);
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>EnvelopeGenerator.Web</PackageId>
@@ -12,9 +12,9 @@
<PackageTags>digital data envelope generator web</PackageTags>
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Version>3.8.2</Version>
<AssemblyVersion>3.8.2</AssemblyVersion>
<FileVersion>3.8.2</FileVersion>
<Version>3.5.0</Version>
<AssemblyVersion>3.5.0</AssemblyVersion>
<FileVersion>3.5.0</FileVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup>
@@ -2094,13 +2094,20 @@
<None Include="wwwroot\lib\bootstrap-icons\icons\zoom-out.svg" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
<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="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.5" />
@@ -2119,56 +2126,6 @@
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
<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="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
<PackageReference Include="Quartz" Version="3.8.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
<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="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
<PackageReference Include="Quartz" Version="3.8.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.11" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
<PackageReference Include="System.Drawing.Common" Version="9.0.5" />
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />

View File

@@ -1,4 +1,3 @@
using EnvelopeGenerator.Application.Services;
using Microsoft.EntityFrameworkCore;
using NLog;
using Quartz;
@@ -16,8 +15,6 @@ using EnvelopeGenerator.Web.Sanitizers;
using EnvelopeGenerator.Web.Models.Annotation;
using DigitalData.UserManager.DependencyInjection;
using EnvelopeGenerator.Web.Middleware;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Web;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
@@ -89,6 +86,7 @@ try
builder.Services.AddSwaggerGen();
}
// TODO: Update to configure with EnvelopeGenerator.DependencyInjection
//AddEF Core dbcontext
var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration");
var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default";
@@ -104,7 +102,7 @@ try
// Add envelope generator services
#pragma warning disable CS0618 // Type or member is obsolete
builder.Services.AddEnvelopeGeneratorInfrastructureServices(
builder.Services.AddEGInfrastructureServices(
opt =>
{
opt.AddDbTriggerParams(config);
@@ -135,22 +133,41 @@ try
options.ConsentCookie.Name = "cookie-consent-settings";
});
var authCookieName = "env_auth";
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = authCookieName;
options.CookieManager = new EnvelopeCookieManager(authCookieName);
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = context =>
{
// Dynamically calculate the redirection path, for example:
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
},
OnRedirectToLogout = context =>
{
// Apply a similar redirection logic for logout
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
}
};
});
builder.Services.AddSingleton(config.GetSection("ContactLink").Get<ContactLink>() ?? new());
builder.Services.AddCookieBasedLocalizer();
builder.Services.AddSingleton(HtmlEncoder.Default);
builder.Services.AddSingleton(UrlEncoder.Default);
builder.Services.AddSanitizer<HtmlSanitizer>();
@@ -168,15 +185,8 @@ try
builder.Services.Configure<Cultures>(config.GetSection("Cultures"));
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<Cultures>>().Value);
// Register mail services
#pragma warning disable CS0618 // Type or member is obsolete
builder.Services.AddScoped<IEnvelopeMailService, EnvelopeMailService>();
#pragma warning restore CS0618 // Type or member is obsolete
builder.Services.AddDispatcher<EGDbContext>();
builder.Services.AddMemoryCache();
builder.ConfigureBySection<CustomImages>();
builder.ConfigureBySection<AnnotationParams>();
@@ -231,7 +241,7 @@ try
app.UseAuthorization();
var cultures = app.Services.GetRequiredService<Cultures>();
if (!cultures.Any())
if(!cultures.Any())
throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
@@ -246,7 +256,7 @@ try
}
]");
if (!config.GetValue<bool>("DisableMultiLanguage"))
if(!config.GetValue<bool>("DisableMultiLanguage"))
app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
app.UseCors("SameOriginPolicy");
@@ -255,7 +265,7 @@ try
app.MapFallbackToController("Error404", "Home");
app.Run();
}
catch (Exception ex)
catch(Exception ex)
{
logger.Error(ex, "Stopped program because of exception");
throw;

Some files were not shown because too many files have changed in this diff Show More