Compare commits
12 Commits
88317e40f5
...
b6ec5307b6
| Author | SHA1 | Date | |
|---|---|---|---|
| b6ec5307b6 | |||
| 106e62a912 | |||
| 27940f5d34 | |||
| e776c2edb4 | |||
| 3f0f5d7fb9 | |||
| e11bc9df8e | |||
| 4dca17d39c | |||
| 8baf6b5553 | |||
| 3ca99fdd83 | |||
| 9e37bf1fe2 | |||
| 9a0837caa9 | |||
| 030646f33d |
@@ -51,8 +51,8 @@ public static class DependencyInjection
|
||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||
|
||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
||||
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
|
||||
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||
services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
|
||||
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
||||
services.TryAddSingleton<QRCodeGenerator>();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public class Adjustment
|
||||
{
|
||||
public static Adjustment CreateBalanceForward(DateTime dt, int random)
|
||||
@@ -1,7 +1,7 @@
|
||||
using DevExpress.DataAccess.Sql;
|
||||
using DevExpress.DataAccess.Sql.DataApi;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public class Customer {
|
||||
static List<Customer> currentCustomers = new List<Customer>();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public class DataItem {
|
||||
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
|
||||
public string CustomerID { get; set; }
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public class DataItemList : IList<DataItem>, IList {
|
||||
readonly int rowCount;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
class DeterministicRandom {
|
||||
const int randomCount = 10000;
|
||||
static readonly int[] deterministicRandomNumbers;
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public struct Term {
|
||||
public static readonly Term[] Terms = new Term[] {
|
||||
new Term("Payment seven days after invoice date" ),
|
||||
@@ -17,6 +17,7 @@
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||
@@ -1,4 +1,4 @@
|
||||
@using EnvelopeGenerator.WebUI.Client.Services;
|
||||
@using EnvelopeGenerator.Server.Client.Services;
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Models;
|
||||
namespace EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pre-assigned signature annotation position on a specific page.
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Models.Constants
|
||||
namespace EnvelopeGenerator.Server.Client.Models.Constants
|
||||
{
|
||||
public enum SenderAppType
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Models.Constants;
|
||||
namespace EnvelopeGenerator.Server.Client.Models.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the unit of measurement for coordinate values in signature positioning.
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Models;
|
||||
namespace EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side model for the envelope receiver returned by
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Models;
|
||||
namespace EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
||||
@@ -1,6 +1,6 @@
|
||||
using EnvelopeGenerator.WebUI.Client.Models.Constants;
|
||||
using EnvelopeGenerator.Server.Client.Models.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Models;
|
||||
namespace EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a signature position on a PDF page.
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Options;
|
||||
namespace EnvelopeGenerator.Server.Client.Options;
|
||||
|
||||
public class ApiOptions
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Options;
|
||||
namespace EnvelopeGenerator.Server.Client.Options;
|
||||
|
||||
public class PdfViewerOptions
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/envelope/login/{EnvelopeKey}"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@using EnvelopeGenerator.WebUI.Client.Services
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/sender/login"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@using EnvelopeGenerator.WebUI.Client.Services
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DevExpress.XtraReports.UI;
|
||||
namespace EnvelopeGenerator.WebUI.Client.PredefinedReports {
|
||||
namespace EnvelopeGenerator.Server.Client.PredefinedReports {
|
||||
public class Report : XtraReport {
|
||||
private TopMarginBand topMarginBand1;
|
||||
private XRPageInfo xrPageInfo4;
|
||||
@@ -1081,7 +1081,7 @@ namespace EnvelopeGenerator.WebUI.Client.PredefinedReports {
|
||||
objectConstructorInfo1.Parameters.AddRange(new DevExpress.DataAccess.ObjectBinding.Parameter[] {
|
||||
parameter1});
|
||||
this.objectDataSource1.Constructor = objectConstructorInfo1;
|
||||
this.objectDataSource1.DataSource = typeof(EnvelopeGenerator.WebUI.Client.Data.DataItemList);
|
||||
this.objectDataSource1.DataSource = typeof(EnvelopeGenerator.Server.Client.Data.DataItemList);
|
||||
this.objectDataSource1.Name = "objectDataSource1";
|
||||
//
|
||||
// Title
|
||||
@@ -1,6 +1,6 @@
|
||||
using DevExpress.XtraReports.UI;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.PredefinedReports {
|
||||
namespace EnvelopeGenerator.Server.Client.PredefinedReports {
|
||||
public static class ReportsFactory
|
||||
{
|
||||
public static readonly Dictionary<string, Func<XtraReport>> Reports = new() {
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using EnvelopeGenerator.WebUI.Client.Services;
|
||||
using EnvelopeGenerator.WebUI.Client.Options;
|
||||
using EnvelopeGenerator.Server.Client.Services;
|
||||
using EnvelopeGenerator.Server.Client.Options;
|
||||
using DevExpress.Blazor.Reporting;
|
||||
using DevExpress.XtraReports.Web.Extensions;
|
||||
using DevExpress.DataAccess.Web;
|
||||
@@ -8,7 +8,7 @@ using DevExpress.XtraReports.Services;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
// HTTP Client (uses WebUI's YARP proxy)
|
||||
// HTTP Client (uses Server's YARP proxy)
|
||||
builder.Services.AddScoped(sp => new HttpClient {
|
||||
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||
});
|
||||
@@ -41,8 +41,8 @@ builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSou
|
||||
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
|
||||
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
|
||||
|
||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.WebUI.Client.Data.DataItemList));
|
||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.WebUI.Client.PredefinedReports.Report));
|
||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.Server.Client.Data.DataItemList));
|
||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.Server.Client.PredefinedReports.Report));
|
||||
|
||||
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
|
||||
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.WebUI.Client.Models;
|
||||
using EnvelopeGenerator.WebUI.Client.Options;
|
||||
using EnvelopeGenerator.Server.Client.Models;
|
||||
using EnvelopeGenerator.Server.Client.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves annotation positions from the API.
|
||||
@@ -15,13 +16,14 @@ namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
/// YARP route in <c>yarp.json</c> — no code change required.
|
||||
/// </summary>
|
||||
[Obsolete("Use SignatureService.")]
|
||||
public class AnnotationService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
public class AnnotationService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var url = $"{apiOptions.Value.BaseUrl}/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var url = $"/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides application version for cache busting static assets.
|
||||
@@ -1,15 +1,16 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using EnvelopeGenerator.WebUI.Client.Options;
|
||||
using EnvelopeGenerator.Server.Client.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
||||
|
||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||
|
||||
public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
public class AuthService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private readonly ApiOptions _api = apiOptions.Value;
|
||||
|
||||
@@ -19,7 +20,8 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
/// </summary>
|
||||
public async Task<bool> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.GetAsync($"/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
||||
return response.StatusCode == HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
@@ -30,11 +32,12 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
/// </summary>
|
||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var form = new MultipartFormDataContent();
|
||||
form.Add(new StringContent(accessCode), "AccessCode");
|
||||
|
||||
var response = await http.PostAsync(
|
||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
|
||||
$"/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
|
||||
form, cancel);
|
||||
|
||||
return response.StatusCode switch
|
||||
@@ -52,8 +55,9 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
/// </summary>
|
||||
public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.PostAsync(
|
||||
$"{_api.BaseUrl}/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
|
||||
$"/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
|
||||
null, cancel);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
@@ -65,10 +69,11 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
/// </summary>
|
||||
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var requestBody = new { username, password };
|
||||
|
||||
var response = await http.PostAsJsonAsync(
|
||||
$"{_api.BaseUrl}/api/auth?cookie=true",
|
||||
$"/api/auth?cookie=true",
|
||||
requestBody, cancel);
|
||||
|
||||
return response.StatusCode switch
|
||||
@@ -2,7 +2,7 @@
|
||||
using DevExpress.DataAccess.Web;
|
||||
using DevExpress.DataAccess.Wizard.Services;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using DevExpress.DataAccess.Json;
|
||||
using DevExpress.DataAccess.Web;
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public class CustomJsonDataConnectionProviderFactory : IJsonDataConnectionProviderFactory {
|
||||
public IJsonDataConnectionProviderService Create() {
|
||||
@@ -1,8 +1,8 @@
|
||||
using DevExpress.XtraReports.UI;
|
||||
using DevExpress.XtraReports.Services;
|
||||
using EnvelopeGenerator.WebUI.Client.PredefinedReports;
|
||||
using EnvelopeGenerator.Server.Client.PredefinedReports;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public class CustomReportProvider : IReportProviderAsync {
|
||||
private readonly InMemoryReportStorageWebExtension reportStorage;
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using EnvelopeGenerator.WebUI.Client.Options;
|
||||
using EnvelopeGenerator.Server.Client.Options;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
public class DocumentService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private readonly ApiOptions _api = apiOptions.Value;
|
||||
|
||||
@@ -16,7 +16,8 @@ public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||
public async Task<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var response = await http.GetAsync($"{_api.BaseUrl}/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.GetAsync($"/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -1,17 +1,18 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.WebUI.Client.Models;
|
||||
using EnvelopeGenerator.WebUI.Client.Options;
|
||||
using EnvelopeGenerator.Server.Client.Models;
|
||||
using EnvelopeGenerator.Server.Client.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
||||
/// from <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||
/// </summary>
|
||||
public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
@@ -22,7 +23,8 @@ public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOp
|
||||
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||
public async Task<EnvelopeReceiverDto?> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var url = $"{apiOptions.Value.BaseUrl}/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var url = $"/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -1,6 +1,6 @@
|
||||
using DevExpress.Drawing;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public static class FontLoader {
|
||||
public async static Task LoadFonts(HttpClient httpClient, List<string> fontNames) {
|
||||
@@ -1,8 +1,8 @@
|
||||
using DevExpress.XtraReports.UI;
|
||||
using DevExpress.XtraReports.Web.Extensions;
|
||||
using EnvelopeGenerator.WebUI.Client.PredefinedReports;
|
||||
using EnvelopeGenerator.Server.Client.PredefinedReports;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public class InMemoryReportStorageWebExtension : ReportStorageWebExtension
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using DevExpress.DataAccess.Web;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public class ObjectDataSourceWizardCustomTypeProvider : IObjectDataSourceWizardTypeProvider {
|
||||
public IEnumerable<Type> GetAvailableTypes(string context) {
|
||||
@@ -1,14 +1,15 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using EnvelopeGenerator.WebUI.Client.Options;
|
||||
using EnvelopeGenerator.WebUI.Client.Models;
|
||||
using EnvelopeGenerator.Server.Client.Options;
|
||||
using EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Client service for managing cached signatures via API.
|
||||
/// </summary>
|
||||
public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
public class SignatureCacheService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private readonly ApiOptions _api = apiOptions.Value;
|
||||
|
||||
@@ -17,8 +18,9 @@ public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOpti
|
||||
SignatureCaptureDto signature,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.PostAsJsonAsync(
|
||||
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||
$"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||
signature,
|
||||
cancel);
|
||||
|
||||
@@ -33,8 +35,9 @@ public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOpti
|
||||
string envelopeKey,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.GetAsync(
|
||||
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||
$"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||
cancel);
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
@@ -53,8 +56,9 @@ public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOpti
|
||||
string envelopeKey,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.DeleteAsync(
|
||||
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||
$"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||
cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -1,18 +1,20 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.WebUI.Client.Models;
|
||||
using EnvelopeGenerator.WebUI.Client.Options;
|
||||
using EnvelopeGenerator.Server.Client.Models;
|
||||
using EnvelopeGenerator.Server.Client.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public class SignatureService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
public class SignatureService(IHttpClientFactory httpClientFactory, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var url = $"/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -7,10 +7,10 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.WebUI.Client
|
||||
@using EnvelopeGenerator.WebUI.Client.Services
|
||||
@using EnvelopeGenerator.WebUI.Client.Models
|
||||
@using EnvelopeGenerator.WebUI.Client.Options
|
||||
@using EnvelopeGenerator.Server.Client
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@using EnvelopeGenerator.Server.Client.Models
|
||||
@using EnvelopeGenerator.Server.Client.Options
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace EnvelopeGenerator.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Authentication scheme names for envelope generator.
|
||||
/// </summary>
|
||||
public static class AuthScheme
|
||||
{
|
||||
/// <summary>
|
||||
/// Scheme name used for per-envelope receiver JWT authentication.
|
||||
/// </summary>
|
||||
public const string Receiver = "EnvelopeGenerator.Server.ReceiverJWT";
|
||||
|
||||
/// <summary>
|
||||
/// Scheme name used for per-envelope sender JWT authentication.
|
||||
/// </summary>
|
||||
public const string Sender = "EnvelopeGenerator.Server.SenderJWT";
|
||||
}
|
||||
@@ -8,12 +8,12 @@
|
||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="stylesheet" href="css/envelope-viewer.css" />
|
||||
<link rel="stylesheet" href="EnvelopeGenerator.WebUI.styles.css" />
|
||||
<HeadOutlet @rendermode="InteractiveAuto" />
|
||||
<link rel="stylesheet" href="EnvelopeGenerator.Server.styles.css" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes @rendermode="InteractiveAuto" />
|
||||
<Routes />
|
||||
<script src="_content/DevExpress.Blazor.Resources/js/preload-script.js"></script>
|
||||
<script src="js/typed.umd.js"></script>
|
||||
<script src="js/receiver-signature.js?v=9"></script>
|
||||
@@ -1,10 +1,10 @@
|
||||
@page "/envelope/{EnvelopeKey}"
|
||||
@rendermode InteractiveServer
|
||||
@using EnvelopeGenerator.WebUI.Client.Models
|
||||
@using EnvelopeGenerator.WebUI.Client.Models.Constants
|
||||
@using EnvelopeGenerator.WebUI.Client.Services
|
||||
@using EnvelopeGenerator.Server.Client.Models
|
||||
@using EnvelopeGenerator.Server.Client.Models.Constants
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@using Microsoft.Extensions.Options
|
||||
@using EnvelopeGenerator.WebUI.Client.Options
|
||||
@using EnvelopeGenerator.Server.Client.Options
|
||||
@using Microsoft.JSInterop
|
||||
@using DevExpress.Blazor
|
||||
@inject DocumentService DocumentService
|
||||
@@ -14,8 +14,8 @@
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject SignatureService SignatureService
|
||||
@inject SignatureCacheService SignatureCacheService
|
||||
@inject EnvelopeGenerator.WebUI.Client.Services.AuthService AuthService
|
||||
@inject EnvelopeGenerator.WebUI.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||
@inject AppVersionService AppVersion
|
||||
@inject ILogger<EnvelopeReceiverPage> logger
|
||||
@implements IAsyncDisposable
|
||||
@@ -1,8 +1,8 @@
|
||||
@page "/envelope/DxPdfViewer"
|
||||
@rendermode InteractiveServer
|
||||
@using System.IO
|
||||
@using DevExpress.Blazor
|
||||
@using System.Reflection
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
@@ -91,7 +91,7 @@ else
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.WebUI.Resources.Invoice.pdf");
|
||||
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.Server.Resources.Invoice.pdf");
|
||||
if (stream != null)
|
||||
{
|
||||
using (stream)
|
||||
@@ -3,8 +3,8 @@
|
||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@using Microsoft.Extensions.Options
|
||||
@using EnvelopeGenerator.WebUI.Client.Options
|
||||
@using EnvelopeGenerator.WebUI.Client.Services
|
||||
@using EnvelopeGenerator.Server.Client.Options
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
||||
@inject DocumentService DocumentService
|
||||
@inject IOptions<ApiOptions> AppOptions
|
||||
@@ -92,7 +92,7 @@ else
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.WebUI.Resources.Invoice.pdf");
|
||||
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.Server.Resources.Invoice.pdf");
|
||||
if (stream != null)
|
||||
{
|
||||
using (stream)
|
||||
@@ -6,6 +6,7 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.WebUI
|
||||
@using EnvelopeGenerator.WebUI.Client
|
||||
@using EnvelopeGenerator.WebUI.Components
|
||||
@using EnvelopeGenerator.Server
|
||||
@using EnvelopeGenerator.Server.Client
|
||||
@using EnvelopeGenerator.Server.Components
|
||||
@using DevExpress.Blazor
|
||||
@@ -0,0 +1,132 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages annotations and signature lifecycle for envelopes.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AnnotationController : ControllerBase
|
||||
{
|
||||
[Obsolete("Use MediatR")]
|
||||
private readonly IEnvelopeHistoryService _historyService;
|
||||
|
||||
[Obsolete("Use MediatR")]
|
||||
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
||||
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
private readonly ILogger<AnnotationController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="AnnotationController"/>.
|
||||
/// </summary>
|
||||
[Obsolete("Use MediatR")]
|
||||
public AnnotationController(
|
||||
ILogger<AnnotationController> logger,
|
||||
IEnvelopeHistoryService envelopeHistoryService,
|
||||
IEnvelopeReceiverService envelopeReceiverService,
|
||||
IMediator mediator)
|
||||
{
|
||||
_historyService = envelopeHistoryService;
|
||||
_envelopeReceiverService = envelopeReceiverService;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates annotations for the authenticated envelope receiver.
|
||||
/// </summary>
|
||||
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost]
|
||||
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||
{
|
||||
var signature = User.ReceiverSignature();
|
||||
var uuid = User.EnvelopeUuid();
|
||||
|
||||
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
||||
|
||||
if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null)
|
||||
return BadRequest();
|
||||
|
||||
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status409Conflict);
|
||||
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||
|
||||
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
||||
var docSignedNotification = envelopeReceiverDto is not null
|
||||
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
||||
: throw new NotFoundException("Envelope receiver is not found.");
|
||||
|
||||
try
|
||||
{
|
||||
await _mediator.Publish(docSignedNotification, cancel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _mediator.Publish(new RemoveSignatureNotification()
|
||||
{
|
||||
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
|
||||
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
|
||||
}, cancel);
|
||||
throw;
|
||||
}
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rejects the document for the current receiver.
|
||||
/// </summary>
|
||||
/// <param name="reason">Optional rejection reason.</param>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost("reject")]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||
{
|
||||
var signature = User.ReceiverSignature();
|
||||
var uuid = User.EnvelopeUuid();
|
||||
var mail = User.ReceiverMail();
|
||||
|
||||
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||
|
||||
if (envRcvRes.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(envRcvRes.Notices);
|
||||
return Unauthorized("you are not authorized");
|
||||
}
|
||||
|
||||
var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason);
|
||||
if (histRes.IsSuccess)
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject");
|
||||
_logger.LogNotice(histRes.Notices);
|
||||
return StatusCode(500, histRes.Messages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using DigitalData.Auth.Claims;
|
||||
using EnvelopeGenerator.Server.Controllers.Interfaces;
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus.
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions, IAuthorizationService authService) : ControllerBase, IAuthController
|
||||
{
|
||||
private readonly AuthTokenKeys authTokenKeys = authTokenKeyOptions.Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IAuthorizationService AuthService { get; } = authService;
|
||||
|
||||
/// <summary>
|
||||
/// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie)
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Gibt eine HTTP 200 oder 401.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/auth/logout
|
||||
///
|
||||
/// </remarks>
|
||||
/// <response code="200">Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat.</response>
|
||||
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
|
||||
Response.Cookies.Delete(authTokenKeys.Cookie);
|
||||
else if (await this.IsUserInPolicyAsync(AuthPolicy.ReceiverOrReceiverTFA))
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
else
|
||||
return Unauthorized();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prüft, ob der Benutzer ein autorisiertes Token hat.
|
||||
/// </summary>
|
||||
/// <returns>Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401</returns>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
///
|
||||
/// GET /api/auth
|
||||
///
|
||||
/// </remarks>
|
||||
/// <response code="200">Wenn es einen autorisierten Cookie gibt.</response>
|
||||
/// <response code="401">Wenn kein Cookie vorhanden ist oder nicht autorisierte.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[HttpGet("check")]
|
||||
[Authorize]
|
||||
public IActionResult Check(string? role = null)
|
||||
=> role is not null && !User.IsInRole(role)
|
||||
? Unauthorized()
|
||||
: Ok();
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key.
|
||||
/// The request must carry a cookie named <c>AuthTokenSignFLOWReceiver.{envelopeKey}</c>.
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey">The unique envelope key extracted from the route.</param>
|
||||
/// <response code="200">Valid per-envelope token found.</response>
|
||||
/// <response code="401">Token is missing, expired or invalid.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpGet("check/envelope/{envelopeKey}")]
|
||||
public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the per-envelope receiver cookie for the given envelope key.
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey">The unique envelope key whose cookie should be deleted.</param>
|
||||
/// <response code="200">Cookie successfully deleted.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[HttpPost("logout/envelope/{envelopeKey}")]
|
||||
public IActionResult LogoutEnvelopeReceiver([FromRoute] string envelopeKey)
|
||||
{
|
||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||
Response.Cookies.Delete(cookieName);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all per-envelope receiver cookies from the current request.
|
||||
/// </summary>
|
||||
/// <response code="200">All envelope receiver cookies successfully deleted.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[HttpPost("logout/envelope")]
|
||||
public IActionResult LogoutAllEnvelopeReceivers()
|
||||
{
|
||||
foreach (var cookieName in Request.Cookies.Keys.Where(k => CookieNames.IsEnvelopeReceiverCookie(k, authTokenKeys.Cookie)))
|
||||
Response.Cookies.Delete(cookieName);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.Server.Options;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages cached data for receivers using distributed cache.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
public class CacheController(
|
||||
IDistributedCache cache,
|
||||
IOptions<CacheOptions> cacheOptions) : ControllerBase
|
||||
{
|
||||
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
||||
|
||||
/// <summary>
|
||||
/// Stores a receiver's signature in cache for the specified envelope.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost("SignatureCapture/{envelopeKey}")]
|
||||
public async Task<IActionResult> SaveSignature(
|
||||
[FromRoute] string envelopeKey,
|
||||
[FromBody] SignatureCacheRequest request,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||
var json = JsonSerializer.Serialize(request);
|
||||
|
||||
var options = cacheOptions.Value.SignatureCacheExpiration.HasValue
|
||||
? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value }
|
||||
: null;
|
||||
|
||||
await cache.SetStringAsync(cacheKey, json, options ?? new DistributedCacheEntryOptions(), cancel);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a cached signature for the specified envelope.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpGet("SignatureCapture/{envelopeKey}")]
|
||||
public async Task<IActionResult> GetSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||
{
|
||||
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||
var json = await cache.GetStringAsync(cacheKey, cancel);
|
||||
|
||||
if (json is null)
|
||||
return NotFound();
|
||||
|
||||
var signature = JsonSerializer.Deserialize<SignatureCacheRequest>(json);
|
||||
return Ok(signature);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a cached signature for the specified envelope.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpDelete("SignatureCapture/{envelopeKey}")]
|
||||
public async Task<IActionResult> DeleteSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||
{
|
||||
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||
await cache.RemoveAsync(cacheKey, cancel);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for caching signature data.
|
||||
/// </summary>
|
||||
public sealed record SignatureCacheRequest(
|
||||
string DataUrl,
|
||||
string FullName,
|
||||
string Place,
|
||||
string? Position = null);
|
||||
@@ -0,0 +1,30 @@
|
||||
using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes configuration data required by the client applications.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of <see cref="ConfigController"/>.
|
||||
/// </remarks>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||
{
|
||||
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||
|
||||
/// <summary>
|
||||
/// Returns annotation configuration that was previously rendered by MVC.
|
||||
/// </summary>
|
||||
[HttpGet("Annotations")]
|
||||
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||
public IActionResult GetAnnotationParams()
|
||||
{
|
||||
return Ok(_annotationParams.AnnotationJSObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using DigitalData.Auth.Claims;
|
||||
using EnvelopeGenerator.Server.Controllers.Interfaces;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Application.Documents.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to envelope documents for authenticated receivers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="DocumentController"/> class.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DocumentController(IMediator mediator, IAuthorizationService authService, ILogger<DocumentController> logger) : ControllerBase, IAuthController
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IAuthorizationService AuthService => authService;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the document bytes receiver.
|
||||
/// </summary>
|
||||
/// <param name="query">Encoded envelope key.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
|
||||
{
|
||||
// Sender: expects query with envelope key
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
|
||||
{
|
||||
if (query is null)
|
||||
return BadRequest("Missing document query.");
|
||||
|
||||
var senderDoc = await mediator.Send(query, cancel);
|
||||
return senderDoc.ByteData is byte[] senderDocByte
|
||||
? File(senderDocByte, "application/octet-stream")
|
||||
: NotFound("Document is empty.");
|
||||
}
|
||||
|
||||
// Receiver: resolve envelope id from claims
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Receiver))
|
||||
{
|
||||
if (query is not null)
|
||||
return BadRequest("Query parameters are not allowed for receiver role.");
|
||||
|
||||
var envelopeId = User.EnvelopeId();
|
||||
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
|
||||
return receiverDoc.ByteData is byte[] receiverDocByte
|
||||
? File(receiverDocByte, "application/octet-stream")
|
||||
: NotFound("Document is empty.");
|
||||
}
|
||||
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the document for the specified envelope key.
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpGet("{envelopeKey}")]
|
||||
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
|
||||
{
|
||||
int envelopeId = User.EnvelopeId();
|
||||
|
||||
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||
|
||||
if (senderDoc.ByteData is not byte[] senderDocByte)
|
||||
return NotFound("Document is empty.");
|
||||
|
||||
Response.Headers.ContentDisposition = $"inline; filename=\"{envelopeKey}.pdf\"";
|
||||
return File(senderDocByte, "application/pdf");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using AutoMapper;
|
||||
using EnvelopeGenerator.Application.EmailTemplates.Commands;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.EmailTemplates.Queries;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing temp templates.
|
||||
/// Steuerung zur Verwaltung von E-Mail-Vorlagen.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
|
||||
/// </remarks>
|
||||
/// <param name="mediator">
|
||||
/// Die Mediator-Instanz, die zum Senden von Befehlen und Abfragen verwendet wird.
|
||||
/// </param>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize(Policy = AuthPolicy.Sender)]
|
||||
public class EmailTemplateController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Ruft E-Mail-Vorlagen basierend auf der angegebenen Abfrage ab.
|
||||
/// Gibt alles zurück, wenn keine Id- oder Typ-Informationen eingegeben wurden.
|
||||
/// </summary>
|
||||
/// <param name="emailTemplate">Die Abfrageparameter zum Abrufen von E-Mail-Vorlagen.</param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns>Gibt HTTP-Antwort zurück</returns>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
/// GET /api/EmailTemplate?emailTemplateId=123
|
||||
/// </remarks>
|
||||
/// <response code="200">Wenn die E-Mail-Vorlagen erfolgreich abgerufen werden.</response>
|
||||
/// <response code="400">Wenn die Abfrageparameter ungültig sind.</response>
|
||||
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
|
||||
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Get([FromQuery] ReadEmailTemplateQuery emailTemplate, CancellationToken cancel)
|
||||
{
|
||||
var result = await mediator.Send(emailTemplate, cancel);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an temp template or resets it if no update command is provided.
|
||||
/// Aktualisiert eine E-Mail-Vorlage oder setzt sie zurück, wenn kein Aktualisierungsbefehl angegeben ist.
|
||||
/// </summary>
|
||||
/// <param name="update"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <response code="200">Wenn die E-Mail-Vorlage erfolgreich aktualisiert oder zurückgesetzt wird.</response>
|
||||
/// <response code="400">Wenn die Abfrage ohne einen String gesendet wird.</response>
|
||||
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
|
||||
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
|
||||
[HttpPut]
|
||||
public async Task<IActionResult> Update([FromBody] UpdateEmailTemplateCommand update, CancellationToken cancel)
|
||||
{
|
||||
await mediator.Send(update, cancel);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Die API ermöglicht das Abrufen und Verwalten von Umschlägen basierend auf Benutzerinformationen und Statusfiltern.
|
||||
///
|
||||
/// Mögliche Antworten:
|
||||
/// - 200 OK: Die Anfrage war erfolgreich, und die angeforderten Daten werden zurückgegeben.
|
||||
/// - 400 Bad Request: Die Anfrage war fehlerhaft oder unvollständig.
|
||||
/// - 401 Unauthorized: Der Benutzer ist nicht authentifiziert.
|
||||
/// - 403 Forbidden: Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.
|
||||
/// - 404 Not Found: Die angeforderte Ressource wurde nicht gefunden.
|
||||
/// - 500 Internal Server Error: Ein unerwarteter Fehler ist aufgetreten.
|
||||
/// </remarks>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class EnvelopeController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<EnvelopeController> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt eine neue Instanz des EnvelopeControllers.
|
||||
/// </summary>
|
||||
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
|
||||
/// <param name="mediator"></param>
|
||||
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab.
|
||||
/// </summary>
|
||||
/// <param name="envelope"></param>
|
||||
/// <returns>Eine IActionResult-Instanz, die die abgerufenen Umschläge oder einen Fehlerstatus enthält.</returns>
|
||||
/// <response code="200">Die Anfrage war erfolgreich, und die Umschläge werden zurückgegeben.</response>
|
||||
/// <response code="400">Die Anfrage war fehlerhaft oder unvollständig.</response>
|
||||
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
|
||||
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
||||
{
|
||||
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
|
||||
return result.Any() ? Ok(result) : NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
|
||||
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
|
||||
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
|
||||
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[HttpGet("doc-result")]
|
||||
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
|
||||
{
|
||||
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
|
||||
var envelope = envelopes.FirstOrDefault();
|
||||
|
||||
if (envelope is null)
|
||||
return NotFound("Envelope not available.");
|
||||
if (envelope.DocResult is null)
|
||||
return NotFound("The document has not been fully signed or the result has not yet been released.");
|
||||
|
||||
if (view)
|
||||
{
|
||||
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
|
||||
return File(envelope.DocResult, "application/pdf");
|
||||
}
|
||||
|
||||
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="command"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
||||
{
|
||||
var res = await _mediator.Send(command.WithAuth(User.GetId()));
|
||||
|
||||
if (res is null)
|
||||
{
|
||||
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
else
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using AutoMapper;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Data;
|
||||
using EnvelopeGenerator.Application.Common.SQL;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller für die Verwaltung von Umschlagempfängern.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten.
|
||||
/// </remarks>
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class EnvelopeReceiverController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<EnvelopeReceiverController> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEnvelopeExecutor _envelopeExecutor;
|
||||
private readonly IEnvelopeReceiverExecutor _erExecutor;
|
||||
private readonly IDocumentExecutor _documentExecutor;
|
||||
private readonly string _cnnStr;
|
||||
|
||||
/// <summary>
|
||||
/// Konstruktor für den EnvelopeReceiverController.
|
||||
/// </summary>
|
||||
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_mapper = mapper;
|
||||
_envelopeExecutor = envelopeExecutor;
|
||||
_erExecutor = erExecutor;
|
||||
_documentExecutor = documentExecutor;
|
||||
_cnnStr = csOpt.Value.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab.
|
||||
/// </summary>
|
||||
/// <param name="envelopeReceiver">Die Abfrageparameter für die Filterung von Umschlagempfängern.</param>
|
||||
/// <returns>Eine HTTP-Antwort mit der Liste der gefundenen Umschlagempfänger oder einem Fehlerstatus.</returns>
|
||||
/// <remarks>
|
||||
/// Dieser Endpunkt ermöglicht es, Umschlagempfänger basierend auf dem Benutzernamen und optionalen Statusfiltern abzurufen.
|
||||
/// Wenn der Benutzername nicht ermittelt werden kann, wird ein Serverfehler zurückgegeben.
|
||||
/// </remarks>
|
||||
/// <response code="200">Die Liste der Umschlagempfänger wurde erfolgreich abgerufen.</response>
|
||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
|
||||
{
|
||||
envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
|
||||
|
||||
var result = await _mediator.Send(envelopeReceiver);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpGet("{envelopeKey}")]
|
||||
public async Task<IActionResult> GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||
{
|
||||
var er = await _mediator.Send(new ReadEnvelopeReceiverQuery()
|
||||
{
|
||||
Key = envelopeKey
|
||||
}, cancel);
|
||||
|
||||
return Ok(er.SingleOrDefault());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
|
||||
/// </summary>
|
||||
/// <param name="receiver">Abfrage, bei der nur eine der Angaben ID, Signatur oder E-Mail-Adresse des Empfängers eingegeben werden muss.</param>
|
||||
/// <returns>Eine HTTP-Antwort mit dem Namen des Empfängers oder einem Fehlerstatus.</returns>
|
||||
/// <remarks>
|
||||
/// Dieser Endpunkt ermöglicht es, den Namen des zuletzt verwendeten Empfängers basierend auf der E-Mail-Adresse abzurufen.
|
||||
/// </remarks>
|
||||
/// <response code="200">Der Name des Empfängers wurde erfolgreich abgerufen.</response>
|
||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||
/// <response code="404">Kein Empfänger gefunden.</response>
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[Authorize]
|
||||
[HttpGet("salute")]
|
||||
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
|
||||
{
|
||||
var name = await _mediator.Send(receiver);
|
||||
return name is null ? NotFound() : Ok(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns>HTTP-Antwort</returns>
|
||||
/// <remarks>
|
||||
/// Sample request:
|
||||
///
|
||||
/// POST /api/envelope
|
||||
/// {
|
||||
/// "title": "Vertragsdokument",
|
||||
/// "message": "Bitte unterschreiben Sie dieses Dokument.",
|
||||
/// "document": {
|
||||
/// "dataAsBase64": "dGVzdC1iYXNlNjQtZGF0YQ=="
|
||||
/// },
|
||||
/// "receivers": [
|
||||
/// {
|
||||
/// "emailAddress": "example@example.com",
|
||||
/// "signatures": [
|
||||
/// {
|
||||
/// "x": 100,
|
||||
/// "y": 200,
|
||||
/// "page": 1
|
||||
/// }
|
||||
/// ],
|
||||
/// "name": "Max Mustermann",
|
||||
/// "phoneNumber": "+49123456789"
|
||||
/// }
|
||||
/// ],
|
||||
/// "tfaEnabled": false
|
||||
/// }
|
||||
///
|
||||
/// </remarks>
|
||||
/// <response code="202">Envelope-Erstellung und Sendeprozessbefehl erfolgreich</response>
|
||||
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||
{
|
||||
#region Create Envelope
|
||||
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
||||
#endregion
|
||||
|
||||
#region Add receivers
|
||||
List<EnvelopeReceiver> sentReceivers = new();
|
||||
List<ReceiverGetOrCreateCommand> unsentReceivers = new();
|
||||
|
||||
foreach (var receiver in request.Receivers)
|
||||
{
|
||||
var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
|
||||
|
||||
if (envelopeReceiver is null)
|
||||
unsentReceivers.Add(receiver);
|
||||
else
|
||||
sentReceivers.Add(envelopeReceiver);
|
||||
}
|
||||
|
||||
var res = _mapper.Map<CreateEnvelopeReceiverResponse>(envelope);
|
||||
res.UnsentReceivers = unsentReceivers;
|
||||
res.SentReceiver = _mapper.Map<List<ReceiverDto>>(sentReceivers.Select(er => er.Receiver));
|
||||
#endregion
|
||||
|
||||
#region Add document
|
||||
var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
|
||||
|
||||
if (document is null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed.");
|
||||
#endregion
|
||||
|
||||
#region Add document element
|
||||
// @DOC_ID, @RECEIVER_ID, @POSITION_X, @POSITION_Y, @PAGE
|
||||
string sql = @"
|
||||
DECLARE @OUT_SUCCESS bit;
|
||||
|
||||
EXEC [dbo].[PRSIG_API_ADD_DOC_RECEIVER_ELEM]
|
||||
{0},
|
||||
{1},
|
||||
{2},
|
||||
{3},
|
||||
{4},
|
||||
@OUT_SUCCESS OUTPUT;
|
||||
|
||||
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||
|
||||
foreach (var rcv in res.SentReceiver)
|
||||
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.DocReceiverElementCreateDto>())
|
||||
{
|
||||
using SqlConnection conn = new(_cnnStr);
|
||||
conn.Open();
|
||||
|
||||
var formattedSQL = string.Format(sql, document.Id.ToSqlParam(), rcv.Id.ToSqlParam(), sign.X.ToSqlParam(), sign.Y.ToSqlParam(), sign.Page.ToSqlParam());
|
||||
|
||||
using SqlCommand cmd = new(formattedSQL, conn);
|
||||
cmd.CommandType = CommandType.Text;
|
||||
|
||||
using SqlDataReader reader = cmd.ExecuteReader();
|
||||
if (reader.Read())
|
||||
{
|
||||
bool outSuccess = reader.GetBoolean(0);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Create history
|
||||
// ENV_UID, STATUS_ID, USER_ID,
|
||||
string sql_hist = @"
|
||||
USE [DD_ECM]
|
||||
|
||||
DECLARE @OUT_SUCCESS bit;
|
||||
|
||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||
{0},
|
||||
{1},
|
||||
{2},
|
||||
@OUT_SUCCESS OUTPUT;
|
||||
|
||||
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||
|
||||
using (SqlConnection conn = new(_cnnStr))
|
||||
{
|
||||
conn.Open();
|
||||
var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), User.GetId().ToSqlParam());
|
||||
using SqlCommand cmd = new(formattedSQL_hist, conn);
|
||||
cmd.CommandType = CommandType.Text;
|
||||
|
||||
using SqlDataReader reader = cmd.ExecuteReader();
|
||||
if (reader.Read())
|
||||
{
|
||||
bool outSuccess = reader.GetBoolean(0);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsBase64String(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
Convert.FromBase64String(input);
|
||||
return true;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Application.EnvelopeTypes.Queries;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class EnvelopeTypeController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<EnvelopeTypeController> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="mediator"></param>
|
||||
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAllAsync()
|
||||
{
|
||||
var result = await _mediator.Send(new ReadEnvelopeTypesQuery());
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class HistoryController : ControllerBase
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Konstruktor für den HistoryController.
|
||||
/// </summary>
|
||||
/// <param name="memoryCache"></param>
|
||||
/// <param name="mediator"></param>
|
||||
public HistoryController(IMemoryCache memoryCache, IMediator mediator)
|
||||
{
|
||||
_memoryCache = memoryCache;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt alle möglichen Verweise auf alle möglichen Include in einem Verlaufsdatensatz zurück. (z. B. DocumentSigned bezieht sich auf Receiver.)
|
||||
/// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können.
|
||||
/// 1 - Sender:
|
||||
/// Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 1* beginnen.
|
||||
/// 2 - Receiver:
|
||||
/// Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 2* beginnen.
|
||||
/// 3 - System:
|
||||
/// Historische Datensätze, die sich auf den allgemeinen Zustand des Umschlags beziehen. Diese haben Statuscodes, die mit 3* beginnen.
|
||||
/// 4 - Unknown:
|
||||
/// Ein unbekannter Datensatz weist auf einen möglichen Mangel oder eine Unstimmigkeit im Aktualisierungsprozess der Anwendung hin.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("related")]
|
||||
[Authorize]
|
||||
public IActionResult GetReferenceTypes(ReferenceType? referenceType = null)
|
||||
{
|
||||
return referenceType is null
|
||||
? Ok(_memoryCache.GetEnumAsDictionary<ReferenceType>("gen.api", ReferenceType.Unknown))
|
||||
: Ok(referenceType.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt alle möglichen Include in einem Verlaufsdatensatz zurück.
|
||||
/// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können.
|
||||
/// 1003: EnvelopeQueued
|
||||
/// 1006: EnvelopeCompletelySigned
|
||||
/// 1007: EnvelopeReportCreated
|
||||
/// 1008: EnvelopeArchived
|
||||
/// 1009: EnvelopeDeleted
|
||||
/// 10007: EnvelopeRejected
|
||||
/// 10009: EnvelopeWithdrawn
|
||||
/// 2001: AccessCodeRequested
|
||||
/// 2002: AccessCodeCorrect
|
||||
/// 2003: AccessCodeIncorrect
|
||||
/// 2004: DocumentOpened
|
||||
/// 2005: DocumentSigned
|
||||
/// 2006: DocumentForwarded
|
||||
/// 2007: DocumentRejected
|
||||
/// 2008: EnvelopeShared
|
||||
/// 2009: EnvelopeViewed
|
||||
/// 3001: MessageInvitationSent (Wird von Trigger verwendet)
|
||||
/// 3002: MessageAccessCodeSent
|
||||
/// 3003: MessageConfirmationSent
|
||||
/// 3004: MessageDeletionSent
|
||||
/// 3005: MessageCompletionSent
|
||||
/// </summary>
|
||||
/// <param name="status">
|
||||
/// Abfrageparameter, der angibt, auf welche Referenz sich der Include bezieht.
|
||||
/// 1 - Sender: Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 1* beginnen.
|
||||
/// 2 - Receiver: Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 2* beginnen.
|
||||
/// 3 - System: Diese werden durch Datenbank-Trigger aktualisiert und sind in den Tabellen EnvelopeHistory und EmailOut zu finden.Sie arbeiten
|
||||
/// integriert mit der Anwendung EmailProfiler, um E-Mails zu versenden und haben die Codes, die mit 3* beginnen.
|
||||
/// </param>
|
||||
/// <returns>Gibt die HTTP-Antwort zurück.</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("status")]
|
||||
[Authorize]
|
||||
public IActionResult GetEnvelopeStatus([FromQuery] EnvelopeStatus? status = null)
|
||||
{
|
||||
return status is null
|
||||
? Ok(_memoryCache.GetEnumAsDictionary<EnvelopeStatus>("gen.api", Status.NonHist, Status.RelatedToFormApp))
|
||||
: Ok(status.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft die gesamte Umschlaghistorie basierend auf den angegebenen Abfrageparametern ab.
|
||||
/// </summary>
|
||||
/// <param name="historyQuery">Die Abfrageparameter, die die Filterkriterien für die Umschlaghistorie definieren.</param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns>Eine Liste von Historieneinträgen, die den angegebenen Kriterien entsprechen, oder nur der letzte Eintrag.</returns>
|
||||
/// <response code="200">Die Anfrage war erfolgreich, und die Umschlaghistorie wird zurückgegeben.</response>
|
||||
/// <response code="400">Die Anfrage war ungültig oder unvollständig.</response>
|
||||
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
|
||||
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
||||
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetAllAsync([FromQuery] ReadHistoryQuery historyQuery, CancellationToken cancel)
|
||||
{
|
||||
var history = await _mediator.Send(historyQuery, cancel).ThrowIfEmpty(Exceptions.NotFound);
|
||||
return Ok((historyQuery.OnlyLast) ? history.MaxBy(h => h.AddedWhen) : history);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public interface IAuthController
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
IAuthorizationService AuthService { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
ClaimsPrincipal User { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class AuthControllerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="controller"></param>
|
||||
/// <param name="policyName"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> IsUserInPolicyAsync(this IAuthController controller, string policyName)
|
||||
{
|
||||
var result = await controller.AuthService.AuthorizeAsync(controller.User, policyName);
|
||||
return result.Succeeded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using DigitalData.Core.API;
|
||||
using EnvelopeGenerator.Application.Resources;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using EnvelopeGenerator.Application.Resources;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class LocalizationController : ControllerBase
|
||||
{
|
||||
private static readonly Guid L_KEY = Guid.NewGuid();
|
||||
|
||||
private readonly ILogger<LocalizationController> _logger;
|
||||
private readonly IStringLocalizer<Resource> _mLocalizer;
|
||||
private readonly IStringLocalizer<Resource> _localizer;
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Konstruktor für den <see cref="LocalizationController"/>.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger für die Protokollierung.</param>
|
||||
/// <param name="localizer">Lokalisierungsdienst für Ressourcen.</param>
|
||||
/// <param name="memoryCache">Speicher-Cache für die Zwischenspeicherung von Daten.</param>
|
||||
/// <param name="_modelLocalizer">Lokalisierungsdienst für Modelle.</param>
|
||||
public LocalizationController(
|
||||
ILogger<LocalizationController> logger,
|
||||
IStringLocalizer<Resource> localizer,
|
||||
IMemoryCache memoryCache,
|
||||
IStringLocalizer<Resource> _modelLocalizer)
|
||||
{
|
||||
_logger = logger;
|
||||
_localizer = localizer;
|
||||
_cache = memoryCache;
|
||||
_mLocalizer = _modelLocalizer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft alle lokalisierten Daten ab.
|
||||
/// </summary>
|
||||
/// <returns>Eine Liste aller lokalisierten Daten.</returns>
|
||||
[HttpGet]
|
||||
public IActionResult GetAll() => Ok(_cache.GetOrCreate(Language ?? string.Empty + L_KEY, _ => _mLocalizer.ToDictionary()));
|
||||
|
||||
/// <summary>
|
||||
/// Ruft die aktuelle Sprache ab.
|
||||
/// </summary>
|
||||
/// <returns>Die aktuelle Sprache oder ein NotFound-Ergebnis, wenn keine Sprache gesetzt ist.</returns>
|
||||
[HttpGet("lang")]
|
||||
public IActionResult GetLanguage() => Language is null ? NotFound() : Ok(Language);
|
||||
|
||||
/// <summary>
|
||||
/// Setzt die Sprache.
|
||||
/// </summary>
|
||||
/// <param name="language">Die zu setzende Sprache.</param>
|
||||
/// <returns>Ein Ok-Ergebnis, wenn die Sprache erfolgreich gesetzt wurde, oder ein BadRequest-Ergebnis, wenn die Eingabe ungültig ist.</returns>
|
||||
[HttpPost("lang")]
|
||||
public IActionResult SetLanguage([FromQuery] string language)
|
||||
{
|
||||
if (string.IsNullOrEmpty(language))
|
||||
return BadRequest();
|
||||
|
||||
Language = language;
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Löscht die aktuelle Sprache.
|
||||
/// </summary>
|
||||
/// <returns>Ein Ok-Ergebnis, wenn die Sprache erfolgreich gelöscht wurde.</returns>
|
||||
[HttpDelete("lang")]
|
||||
public IActionResult DeleteLanguage()
|
||||
{
|
||||
Language = null;
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eigenschaft für die Verwaltung der aktuellen Sprache über Cookies.
|
||||
/// </summary>
|
||||
private string? Language
|
||||
{
|
||||
get
|
||||
{
|
||||
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
|
||||
|
||||
if (string.IsNullOrEmpty(cookieValue))
|
||||
return null;
|
||||
|
||||
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
|
||||
return culture?.Value ?? null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value is null)
|
||||
Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName);
|
||||
else
|
||||
{
|
||||
var cookieOptions = new CookieOptions()
|
||||
{
|
||||
Expires = DateTimeOffset.UtcNow.AddYears(1),
|
||||
Secure = false,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
HttpOnly = true
|
||||
};
|
||||
|
||||
Response.Cookies.Append(
|
||||
CookieRequestCultureProvider.DefaultCookieName,
|
||||
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)),
|
||||
cookieOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Manages read-only envelope sharing flows.
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class ReadOnlyController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<ReadOnlyController> _logger;
|
||||
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
|
||||
private readonly IEnvelopeMailService _mailService;
|
||||
private readonly IEnvelopeHistoryService _historyService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReadOnlyController"/> class.
|
||||
/// </summary>
|
||||
public ReadOnlyController(ILogger<ReadOnlyController> logger, IEnvelopeReceiverReadOnlyService readOnlyService, IEnvelopeMailService mailService, IEnvelopeHistoryService historyService)
|
||||
{
|
||||
_logger = logger;
|
||||
_readOnlyService = readOnlyService;
|
||||
_mailService = mailService;
|
||||
_historyService = historyService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new read-only receiver for the current envelope.
|
||||
/// </summary>
|
||||
/// <param name="createDto">Creation payload.</param>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
||||
{
|
||||
var authReceiverMail = User.ReceiverMail();
|
||||
if (authReceiverMail is null)
|
||||
{
|
||||
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var envelopeId = User.EnvelopeId();
|
||||
|
||||
createDto.AddedWho = authReceiverMail;
|
||||
createDto.EnvelopeId = envelopeId;
|
||||
|
||||
var creationRes = await _readOnlyService.CreateAsync(createDto: createDto);
|
||||
|
||||
if (creationRes.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(creationRes);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
var readRes = await _readOnlyService.ReadByIdAsync(creationRes.Data.Id);
|
||||
if (readRes.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(creationRes);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
var newReadOnly = readRes.Data;
|
||||
|
||||
return await _mailService.SendAsync(newReadOnly).ThenAsync<int, IActionResult>(SuccessAsync: async _ =>
|
||||
{
|
||||
var histRes = await _historyService.RecordAsync((int)createDto.EnvelopeId, createDto.AddedWho, EnvelopeStatus.EnvelopeShared);
|
||||
if (histRes.IsFailed)
|
||||
{
|
||||
_logger.LogError("Although the envelope was sent as read-only, the EnvelopeShared history could not be saved. Create DTO:\n{createDto}", JsonConvert.SerializeObject(createDto));
|
||||
_logger.LogNotice(histRes.Notices);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
},
|
||||
|
||||
Fail: (msg, ntc) =>
|
||||
{
|
||||
_logger.LogNotice(ntc);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller für die Verwaltung von Empfängern.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
|
||||
/// </remarks>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ReceiverController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
|
||||
/// </summary>
|
||||
/// <param name="mediator">Mediator für Anfragen.</param>
|
||||
public ReceiverController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft eine Liste von Empfängern ab, basierend auf den angegebenen Abfrageparametern.
|
||||
/// </summary>
|
||||
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
||||
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
|
||||
{
|
||||
if (!receiver.HasAnyCriteria)
|
||||
{
|
||||
var all = await _mediator.Send(new ReadReceiverQuery());
|
||||
return Ok(all);
|
||||
}
|
||||
|
||||
var result = await _mediator.Send(receiver);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Documents.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SignatureController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
||||
/// </summary>
|
||||
public SignatureController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
//TODO: update to use signature query
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpGet("{envelopeKey}")]
|
||||
public async Task<IActionResult> Get(string envelopeKey, CancellationToken cancel)
|
||||
{
|
||||
int envelopeId = User.EnvelopeId();
|
||||
|
||||
int receiverId = User.ReceiverId();
|
||||
|
||||
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||
|
||||
if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
|
||||
return NotFound("Document is empty.");
|
||||
|
||||
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
|
||||
|
||||
if (rcvSignatures is null)
|
||||
return NotFound("No signatures found for the current receiver.");
|
||||
else
|
||||
return Ok(rcvSignatures);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Resources;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/tfa")]
|
||||
public class TfaRegistrationController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<TfaRegistrationController> _logger;
|
||||
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
||||
private readonly IAuthenticator _authenticator;
|
||||
private readonly IReceiverService _receiverService;
|
||||
private readonly TFARegParams _parameters;
|
||||
private readonly IStringLocalizer<Resource> _localizer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TfaRegistrationController"/> class.
|
||||
/// </summary>
|
||||
public TfaRegistrationController(
|
||||
ILogger<TfaRegistrationController> logger,
|
||||
IEnvelopeReceiverService envelopeReceiverService,
|
||||
IAuthenticator authenticator,
|
||||
IReceiverService receiverService,
|
||||
IOptions<TFARegParams> tfaRegParamsOptions,
|
||||
IStringLocalizer<Resource> localizer)
|
||||
{
|
||||
_logger = logger;
|
||||
_envelopeReceiverService = envelopeReceiverService;
|
||||
_authenticator = authenticator;
|
||||
_receiverService = receiverService;
|
||||
_parameters = tfaRegParamsOptions.Value;
|
||||
_localizer = localizer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates registration metadata (QR code and deadline) for a receiver.
|
||||
/// </summary>
|
||||
/// <param name="envelopeReceiverId">Encoded envelope receiver id.</param>
|
||||
[Authorize]
|
||||
[HttpGet("{envelopeReceiverId}")]
|
||||
public async Task<IActionResult> RegisterAsync(string envelopeReceiverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (uuid, signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
|
||||
|
||||
if (uuid is null || signature is null)
|
||||
{
|
||||
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
|
||||
return Unauthorized(new { message = _localizer.WrongEnvelopeReceiverId() });
|
||||
}
|
||||
|
||||
var secretResult = await _envelopeReceiverService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||
if (secretResult.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(secretResult.Notices);
|
||||
return NotFound(new { message = _localizer.WrongEnvelopeReceiverId() });
|
||||
}
|
||||
|
||||
var envelopeReceiver = secretResult.Data;
|
||||
|
||||
if (!envelopeReceiver.Envelope!.TFAEnabled)
|
||||
return Unauthorized(new { message = _localizer.WrongAccessCode() });
|
||||
|
||||
var receiver = envelopeReceiver.Receiver;
|
||||
receiver!.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
|
||||
await _receiverService.UpdateAsync(receiver);
|
||||
var totpQr64 = _authenticator.GenerateTotpQrCode(userEmail: receiver.EmailAddress, secretKey: receiver.TotpSecretkey).ToBase64String();
|
||||
|
||||
if (receiver.TfaRegDeadline is null)
|
||||
{
|
||||
receiver.TfaRegDeadline = _parameters.Deadline;
|
||||
await _receiverService.UpdateAsync(receiver);
|
||||
}
|
||||
else if (receiver.TfaRegDeadline <= DateTime.Now)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status410Gone, new { message = _localizer.WrongAccessCode() });
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
envelopeReceiver.EnvelopeId,
|
||||
envelopeReceiver.Envelope!.Uuid,
|
||||
envelopeReceiver.Receiver!.Signature,
|
||||
receiver.TfaRegDeadline,
|
||||
TotpQR64 = totpQr64
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer.WrongEnvelopeReceiverId());
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs out the envelope receiver from cookie authentication.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost("auth/logout")]
|
||||
public async Task<IActionResult> LogOutAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{message}", ex.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Documentation;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public sealed class AuthProxyDocumentFilter : IDocumentFilter
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="swaggerDoc"></param>
|
||||
/// <param name="context"></param>
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
AddLoginOperation(swaggerDoc, context);
|
||||
AddEnvelopeReceiverLoginOperation(swaggerDoc, context);
|
||||
}
|
||||
|
||||
private static void AddLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
const string path = "/api/auth";
|
||||
|
||||
var loginSchema = context.SchemaGenerator.GenerateSchema(typeof(Login), context.SchemaRepository);
|
||||
var loginExample = new OpenApiObject
|
||||
{
|
||||
["password"] = new OpenApiString(""),
|
||||
["username"] = new OpenApiString("")
|
||||
};
|
||||
|
||||
var operation = new OpenApiOperation
|
||||
{
|
||||
Summary = "Proxy login (auth-hub)",
|
||||
Description = "Proxies the request to the auth service. Add query parameter `cookie=true|false`.",
|
||||
Tags = [new() { Name = "Auth" }],
|
||||
Parameters =
|
||||
{
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = "cookie",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Schema = new OpenApiSchema { Type = "boolean", Default = new OpenApiBoolean(true) },
|
||||
Example = new OpenApiBoolean(true),
|
||||
Description = "If true, auth service sets the auth cookie."
|
||||
}
|
||||
},
|
||||
RequestBody = new OpenApiRequestBody
|
||||
{
|
||||
Required = true,
|
||||
Content =
|
||||
{
|
||||
["application/json"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample },
|
||||
["multipart/form-data"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample }
|
||||
}
|
||||
},
|
||||
Responses =
|
||||
{
|
||||
["200"] = new OpenApiResponse { Description = "OK (proxied response)" },
|
||||
["401"] = new OpenApiResponse { Description = "Unauthorized" }
|
||||
}
|
||||
};
|
||||
|
||||
swaggerDoc.Paths[path] = new OpenApiPathItem
|
||||
{
|
||||
Operations =
|
||||
{
|
||||
[OperationType.Post] = operation
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddEnvelopeReceiverLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
const string path = "/api/Auth/envelope-receiver/{key}";
|
||||
|
||||
var bodySchema = context.SchemaGenerator.GenerateSchema(typeof(EnvelopeReceiverLogin), context.SchemaRepository);
|
||||
|
||||
var operation = new OpenApiOperation
|
||||
{
|
||||
Summary = "Envelope receiver login (auth-hub proxy)",
|
||||
Description = "Proxies the envelope receiver login to the auth service. " +
|
||||
"The `cookie` query parameter is always forwarded as `true` so the auth service sets the per-envelope cookie automatically.",
|
||||
Tags = [new() { Name = "Auth" }],
|
||||
Parameters =
|
||||
{
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = "key",
|
||||
In = ParameterLocation.Path,
|
||||
Required = true,
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Description = "The unique envelope receiver key."
|
||||
}
|
||||
},
|
||||
RequestBody = new OpenApiRequestBody
|
||||
{
|
||||
Required = false,
|
||||
Content =
|
||||
{
|
||||
["multipart/form-data"] = new OpenApiMediaType { Schema = bodySchema }
|
||||
}
|
||||
},
|
||||
Responses =
|
||||
{
|
||||
["200"] = new OpenApiResponse { Description = "OK – per-envelope cookie set by auth service." },
|
||||
["401"] = new OpenApiResponse { Description = "Unauthorized – invalid or missing access code." }
|
||||
}
|
||||
};
|
||||
|
||||
swaggerDoc.Paths[path] = new OpenApiPathItem
|
||||
{
|
||||
Operations =
|
||||
{
|
||||
[OperationType.Post] = operation
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Server.Client\EnvelopeGenerator.Server.Client.csproj" />
|
||||
<ProjectReference Include="..\..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
<ProjectReference Include="..\..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.22" />
|
||||
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||
|
||||
<!-- API Packages from EnvelopeGenerator.Server -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
|
||||
<PackageReference Include="AspNetCore.Scalar" Version="1.1.8" />
|
||||
<PackageReference Include="DigitalData.Auth.Claims" Version="1.0.3" />
|
||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" />
|
||||
<PackageReference Include="itext" Version="8.0.5" />
|
||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||
<PackageReference Include="NLog" Version="5.2.5" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.2.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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="wwwroot\docs\privacy-policy.fr-FR.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\Invoice.pdf" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,96 @@
|
||||
using DigitalData.Auth.Claims;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for working with envelope-specific authentication claims.
|
||||
/// </summary>
|
||||
public static class ReceiverClaimExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="claimType"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private static string GetRequiredClaimValue(this ClaimsPrincipal user, string claimType)
|
||||
{
|
||||
var value = user.FindFirstValue(claimType);
|
||||
if (value is not null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var identity = user.Identity;
|
||||
var principalName = identity?.Name ?? "(anonymous)";
|
||||
var authType = identity?.AuthenticationType ?? "(none)";
|
||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
||||
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
private static string GetRequiredClaimValue(this ClaimsPrincipal user, params string[] claimTypes)
|
||||
{
|
||||
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
|
||||
{
|
||||
var value = user.FindFirstValue(claimType);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
return value;
|
||||
}
|
||||
|
||||
var identity = user.Identity;
|
||||
var principalName = identity?.Name ?? "(anonymous)";
|
||||
var authType = identity?.AuthenticationType ?? "(none)";
|
||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
||||
var message = $"Required claim(s) '{string.Join("', '", claimTypes)}' are missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope UUID from the claims.
|
||||
/// </summary>
|
||||
public static string EnvelopeUuid(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver signature from the claims.
|
||||
/// </summary>
|
||||
public static string ReceiverSignature(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverSignature);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver email address from the claims.
|
||||
/// </summary>
|
||||
public static string ReceiverMail(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope identifier from the claims.
|
||||
/// </summary>
|
||||
public static int EnvelopeId(this ClaimsPrincipal user)
|
||||
{
|
||||
var envIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeId);
|
||||
if (int.TryParse(envIdStr, out var envId))
|
||||
return envId;
|
||||
else
|
||||
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.EnvelopeId}' is not a valid integer.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated receiver identifier from the claims.
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static int ReceiverId(this ClaimsPrincipal user)
|
||||
{
|
||||
var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId);
|
||||
if (int.TryParse(rcvIdStr, out var rcvId))
|
||||
return rcvId;
|
||||
else
|
||||
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
|
||||
/// </summary>
|
||||
public static class SenderClaimExtensions
|
||||
{
|
||||
private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, string claimType)
|
||||
{
|
||||
var value = user.FindFirstValue(claimType);
|
||||
if (value is not null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var identity = user.Identity;
|
||||
var principalName = identity?.Name ?? "(anonymous)";
|
||||
var authType = identity?.AuthenticationType ?? "(none)";
|
||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
||||
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, params string[] claimTypes)
|
||||
{
|
||||
string? value = null;
|
||||
|
||||
foreach (var claimType in claimTypes)
|
||||
{
|
||||
value = user.FindFirstValue(claimType);
|
||||
if (value is not null)
|
||||
return value;
|
||||
}
|
||||
|
||||
var identity = user.Identity;
|
||||
var principalName = identity?.Name ?? "(anonymous)";
|
||||
var authType = identity?.AuthenticationType ?? "(none)";
|
||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
||||
var message = $"Required claim among [{string.Join(", ", claimTypes)}] is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the user's ID from the claims. Throws an exception if the ID is missing or invalid.
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||
/// <returns>The user's ID as an integer.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the user ID claim is missing or invalid.</exception>
|
||||
public static int GetId(this ClaimsPrincipal user)
|
||||
{
|
||||
var idValue = user.GetRequiredClaimOfSender(ClaimTypes.NameIdentifier, "sub");
|
||||
|
||||
if (!int.TryParse(idValue, out var result))
|
||||
{
|
||||
throw new InvalidOperationException("User ID claim is missing or invalid. This may indicate a misconfigured or forged JWT token.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the username from the claims.
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||
/// <returns>The username as a string.</returns>
|
||||
public static string GetUsername(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimOfSender(ClaimTypes.Name);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the user's surname (last name) from the claims.
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||
/// <returns>The surname as a string.</returns>
|
||||
public static string GetName(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimOfSender(ClaimTypes.Surname);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the user's given name (first name) from the claims.
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||
/// <returns>The given name as a string.</returns>
|
||||
public static string GetPrename(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimOfSender(ClaimTypes.GivenName);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the user's email address from the claims.
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
|
||||
/// <returns>The email address as a string.</returns>
|
||||
public static string GetEmail(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimOfSender(ClaimTypes.Email);
|
||||
}
|
||||
}
|
||||
10
EnvelopeGenerator.Server/EnvelopeGenerator.Server/Jenkinsfile
vendored
Normal file
10
EnvelopeGenerator.Server/EnvelopeGenerator.Server/Jenkinsfile
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
pipeline {
|
||||
agent any
|
||||
stages {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'dotnet build'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
namespace EnvelopeGenerator.Server.Middleware;
|
||||
|
||||
using DigitalData.Core.Exceptions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Middleware for handling exceptions globally in the application.
|
||||
/// Captures exceptions thrown during the request pipeline execution,
|
||||
/// logs them, and returns an appropriate HTTP response with a JSON error message.
|
||||
/// </summary>
|
||||
public class ExceptionHandlingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The next middleware in the request pipeline.</param>
|
||||
/// <param name="logger">The logger instance for logging exceptions.</param>
|
||||
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the middleware to handle the HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context of the current request.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context); // Continue down the pipeline
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await HandleExceptionAsync(context, ex, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles exceptions by logging them and writing an appropriate JSON response.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context of the current request.</param>
|
||||
/// <param name="exception">The exception that occurred.</param>
|
||||
/// <param name="logger">The logger instance for logging the exception.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
string message;
|
||||
|
||||
switch (exception)
|
||||
{
|
||||
case BadRequestException badRequestEx:
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
message = badRequestEx.Message;
|
||||
break;
|
||||
|
||||
case NotFoundException notFoundEx:
|
||||
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
message = notFoundEx.Message;
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.LogError(exception, "Unhandled exception occurred.");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
message = "An unexpected error occurred.";
|
||||
break;
|
||||
}
|
||||
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(new
|
||||
{
|
||||
message
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
|
||||
{
|
||||
public bool HasAccessCode => AccessCode is not null;
|
||||
|
||||
public bool HasSmsCode => SmsCode is not null;
|
||||
|
||||
public bool HasAuthenticatorCode => AuthenticatorCode is not null;
|
||||
|
||||
public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1;
|
||||
|
||||
public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the keys and default values used for authentication token handling
|
||||
/// within the Envelope Generator Server.
|
||||
/// </summary>
|
||||
public class AuthTokenKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the cookie used to store the authentication token.
|
||||
/// </summary>
|
||||
public string Cookie { get; init; } = "AuthToken";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the query string parameter used to pass the authentication token.
|
||||
/// </summary>
|
||||
public string QueryString { get; init; } = "AuthToken";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expected issuer value for the authentication token.
|
||||
/// </summary>
|
||||
public string Issuer { get; init; } = "auth.digitaldata.works";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expected audience value for the authentication token.
|
||||
/// </summary>
|
||||
public string Audience { get; init; } = "sign-flow.digitaldata.works";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the database connection string for dependency injection.
|
||||
/// </summary>
|
||||
public class ConnectionString
|
||||
{
|
||||
/// <summary>
|
||||
/// The database connection string value.
|
||||
/// </summary>
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace EnvelopeGenerator.Server.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a hyperlink for contact purposes with various HTML attributes.
|
||||
/// </summary>
|
||||
public class ContactLink
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the label of the hyperlink.
|
||||
/// </summary>
|
||||
public string Label { get; init; } = "Contact";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL that the hyperlink points to.
|
||||
/// </summary>
|
||||
public string Href { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target where the hyperlink should open.
|
||||
/// Commonly used values are "_blank", "_self", "_parent", "_top".
|
||||
/// </summary>
|
||||
public string Target { get; set; } = "_blank";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the relationship of the linked URL as space-separated link types.
|
||||
/// Examples include "nofollow", "noopener", "noreferrer".
|
||||
/// </summary>
|
||||
public string Rel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filename that should be downloaded when clicking the hyperlink.
|
||||
/// This attribute will only have an effect if the href attribute is set.
|
||||
/// </summary>
|
||||
public string Download { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language of the linked resource. Useful when linking to
|
||||
/// content in another language.
|
||||
/// </summary>
|
||||
public string HrefLang { get; set; } = "en";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type of the linked URL. Helps browsers to handle
|
||||
/// the type correctly when the link is clicked.
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional information about the hyperlink, typically viewed
|
||||
/// as a tooltip when the mouse hovers over the link.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an identifier for the hyperlink, unique within the HTML document.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
public class Culture
|
||||
{
|
||||
private string _language = string.Empty;
|
||||
public string Language { get => _language;
|
||||
init {
|
||||
_language = value;
|
||||
Info = new(value);
|
||||
}
|
||||
}
|
||||
public string FIClass { get; init; } = string.Empty;
|
||||
|
||||
public CultureInfo? Info { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
public class Cultures : List<Culture>
|
||||
{
|
||||
public IEnumerable<string> Languages => this.Select(c => c.Language);
|
||||
|
||||
public IEnumerable<string> FIClasses => this.Select(c => c.FIClass);
|
||||
|
||||
public Culture Default => this.First();
|
||||
|
||||
public Culture? this[string? language] => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
public class CustomImages : Dictionary<string, Image>
|
||||
{
|
||||
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Request body for the envelope-receiver login endpoint.
|
||||
/// </summary>
|
||||
/// <param name="AccessCode">The access code sent to the receiver.</param>
|
||||
public record EnvelopeReceiverLogin(string? AccessCode = null);
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
public class ErrorViewModel
|
||||
{
|
||||
public string Title { get; init; } = "404";
|
||||
|
||||
public string Subtitle { get; init; } = "Hmmm...";
|
||||
|
||||
public string Body { get; init; } = "It looks like one of the developers fell asleep";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
public class Image
|
||||
{
|
||||
public string Src { get; init; } = string.Empty;
|
||||
|
||||
public Dictionary<string, string> Classes { get; init; } = new();
|
||||
|
||||
public string GetClassIn(string page) => Classes.TryGetValue(page, out var cls) && cls is not null ? cls : string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername.
|
||||
/// </summary>
|
||||
/// <param name="Password">Das erforderliche Passwort für das Login.</param>
|
||||
/// <param name="UserId">Die optionale ID des Benutzers.</param>
|
||||
/// <param name="Username">Der optionale Benutzername.</param>
|
||||
public record Login([Required] string Password, int? UserId = null, string? Username = null)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
public class MainViewModel
|
||||
{
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
|
||||
public record Annotation : IAnnotation
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
|
||||
#region Bound Annotation
|
||||
[JsonIgnore]
|
||||
public string? HorBoundAnnotName { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string? VerBoundAnnotName { get; init; }
|
||||
#endregion
|
||||
|
||||
#region Layout
|
||||
[JsonIgnore]
|
||||
public double? MarginLeft { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double MarginLeftRatio { get; init; } = 1;
|
||||
|
||||
[JsonIgnore]
|
||||
public double? MarginTop { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double MarginTopRatio { get; init; } = 1;
|
||||
|
||||
public double? Width { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double WidthRatio { get; init; } = 1;
|
||||
|
||||
public double? Height { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double HeightRatio { get; init; } = 1;
|
||||
#endregion
|
||||
|
||||
#region Position
|
||||
public double Left => (MarginLeft ?? 0) + (HorBoundAnnot?.HorBoundary ?? 0);
|
||||
|
||||
public double Top => (MarginTop ?? 0) + (VerBoundAnnot?.VerBoundary ?? 0);
|
||||
#endregion
|
||||
|
||||
#region Boundary
|
||||
[JsonIgnore]
|
||||
public double HorBoundary => Left + (Width ?? 0);
|
||||
|
||||
[JsonIgnore]
|
||||
public double VerBoundary => Top + (Height ?? 0);
|
||||
#endregion
|
||||
|
||||
#region BoundAnnot
|
||||
[JsonIgnore]
|
||||
public Annotation? HorBoundAnnot { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Annotation? VerBoundAnnot { get; set; }
|
||||
#endregion
|
||||
|
||||
public Color? BackgroundColor { get; init; }
|
||||
|
||||
#region Border
|
||||
public Color? BorderColor { get; init; }
|
||||
|
||||
public string? BorderStyle { get; init; }
|
||||
|
||||
public int? BorderWidth { get; set; }
|
||||
#endregion
|
||||
|
||||
[JsonIgnore]
|
||||
internal Annotation Default
|
||||
{
|
||||
set
|
||||
{
|
||||
// To set null value, annotation must have null (0) value but null must has non-null value
|
||||
if (MarginLeft == null && value.MarginLeft != null)
|
||||
MarginLeft = value.MarginLeft * MarginLeftRatio;
|
||||
|
||||
if (MarginTop == null && value.MarginTop != null)
|
||||
MarginTop = value.MarginTop * MarginTopRatio;
|
||||
|
||||
if (Width == null && value.Width != null)
|
||||
Width = value.Width * WidthRatio;
|
||||
|
||||
if (Height == null && value.Height != null)
|
||||
Height = value.Height * HeightRatio;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
|
||||
public class AnnotationParams
|
||||
{
|
||||
public AnnotationParams()
|
||||
{
|
||||
_AnnotationJSObjectInitor = new(CreateAnnotationJSObject);
|
||||
}
|
||||
|
||||
public Background? Background { get; init; }
|
||||
|
||||
#region Annotation
|
||||
[JsonIgnore]
|
||||
public Annotation? DefaultAnnotation { get; init; }
|
||||
|
||||
private readonly List<Annotation> _annots = new List<Annotation>();
|
||||
|
||||
public bool TryGet(string name, out Annotation annotation)
|
||||
{
|
||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||
annotation = _annots.FirstOrDefault(a => a.Name == name);
|
||||
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||
return annotation is not null;
|
||||
}
|
||||
|
||||
public required IEnumerable<Annotation> Annotations
|
||||
{
|
||||
get => _annots;
|
||||
init
|
||||
{
|
||||
_annots = value.ToList();
|
||||
|
||||
if (DefaultAnnotation is not null)
|
||||
foreach (var annot in _annots)
|
||||
annot.Default = DefaultAnnotation;
|
||||
|
||||
for (int i = 0; i < _annots.Count; i++)
|
||||
{
|
||||
#region set bound annotations
|
||||
// horizontal
|
||||
if (_annots[i].HorBoundAnnotName is string horBoundAnnotName)
|
||||
if (TryGet(horBoundAnnotName, out var horBoundAnnot))
|
||||
_annots[i].HorBoundAnnot = horBoundAnnot;
|
||||
else
|
||||
throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined.");
|
||||
|
||||
// vertical
|
||||
if (_annots[i].VerBoundAnnotName is string verBoundAnnotName)
|
||||
if (TryGet(verBoundAnnotName, out var verBoundAnnot))
|
||||
_annots[i].VerBoundAnnot = verBoundAnnot;
|
||||
else
|
||||
throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined.");
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AnnotationJSObject
|
||||
private Dictionary<string, IAnnotation> CreateAnnotationJSObject()
|
||||
{
|
||||
var dict = _annots.ToDictionary(a => a.Name.ToLower(), a => a as IAnnotation);
|
||||
|
||||
if (Background is not null)
|
||||
{
|
||||
Background.Locate(_annots);
|
||||
dict.Add(Background.Name.ToLower(), Background);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
private readonly Lazy<Dictionary<string, IAnnotation>> _AnnotationJSObjectInitor;
|
||||
|
||||
public Dictionary<string, IAnnotation> AnnotationJSObject => _AnnotationJSObjectInitor.Value;
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
|
||||
/// <summary>
|
||||
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
|
||||
/// It is only the first annotation as a background for other annotations.
|
||||
/// </summary>
|
||||
public record Background : IAnnotation
|
||||
{
|
||||
[JsonIgnore]
|
||||
public double Margin { get; init; }
|
||||
|
||||
public string Name { get; } = "Background";
|
||||
|
||||
public double? Width { get; set; }
|
||||
|
||||
public double? Height { get; set; }
|
||||
|
||||
public double Left { get; set; }
|
||||
|
||||
public double Top { get; set; }
|
||||
|
||||
public Color? BackgroundColor { get; init; }
|
||||
|
||||
#region Border
|
||||
public Color? BorderColor { get; init; }
|
||||
|
||||
public string? BorderStyle { get; init; }
|
||||
|
||||
public int? BorderWidth { get; set; }
|
||||
#endregion
|
||||
|
||||
public void Locate(IEnumerable<IAnnotation> annotations)
|
||||
{
|
||||
// set Top
|
||||
if (annotations.MinBy(a => a.Top)?.Top is double minTop)
|
||||
Top = minTop;
|
||||
|
||||
// set Left
|
||||
if (annotations.MinBy(a => a.Left)?.Left is double minLeft)
|
||||
Left = minLeft;
|
||||
|
||||
// set Width
|
||||
if(annotations.MaxBy(a => a.GetRight())?.GetRight() is double maxRight)
|
||||
Width = maxRight - Left;
|
||||
|
||||
// set Height
|
||||
if (annotations.MaxBy(a => a.GetBottom())?.GetBottom() is double maxBottom)
|
||||
Height = maxBottom - Top;
|
||||
|
||||
// add margins
|
||||
Top -= Margin;
|
||||
Left -= Margin;
|
||||
Width += Margin * 2;
|
||||
Height += Margin * 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
|
||||
public record Color
|
||||
{
|
||||
public int R { get; init; } = 0;
|
||||
|
||||
public int G { get; init; } = 0;
|
||||
|
||||
public int B { get; init; } = 0;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static double GetRight(this IAnnotation annotation) => annotation.Left + annotation?.Width ?? 0;
|
||||
|
||||
public static double GetBottom(this IAnnotation annotation) => annotation.Top + annotation?.Height ?? 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation;
|
||||
|
||||
public interface IAnnotation
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
double? Width { get; }
|
||||
|
||||
double? Height { get; }
|
||||
|
||||
double Left { get; }
|
||||
|
||||
double Top { get; }
|
||||
|
||||
Color? BackgroundColor { get; }
|
||||
|
||||
Color? BorderColor { get; }
|
||||
|
||||
string? BorderStyle { get; }
|
||||
|
||||
int? BorderWidth { get; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the parameters for two-factor authentication (2FA) registration.
|
||||
/// </summary>
|
||||
public class TFARegParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum allowed time for completing the registration process.
|
||||
/// </summary>
|
||||
public TimeSpan TimeLimit { get; init; } = new(0, 30, 0);
|
||||
|
||||
/// <summary>
|
||||
/// The deadline for registration, calculated as the current time plus the <see cref="TimeLimit"/>.
|
||||
/// </summary>
|
||||
public DateTime Deadline => DateTime.Now.AddTicks(TimeLimit.Ticks);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace EnvelopeGenerator.Server.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for distributed caching.
|
||||
/// </summary>
|
||||
public sealed class CacheOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name in appsettings.json.
|
||||
/// </summary>
|
||||
public const string SectionName = "Cache";
|
||||
|
||||
/// <summary>
|
||||
/// Signature cache expiration time.
|
||||
/// If null, signatures will not expire automatically.
|
||||
/// </summary>
|
||||
public TimeSpan? SignatureCacheExpiration { get; set; }
|
||||
}
|
||||
375
EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs
Normal file
375
EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs
Normal file
@@ -0,0 +1,375 @@
|
||||
using EnvelopeGenerator.Server.Components;
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using EnvelopeGenerator.Server.Options;
|
||||
using DevExpress.Blazor;
|
||||
using EnvelopeGenerator.Server.Client.Services;
|
||||
using DigitalData.Core.API;
|
||||
using DigitalData.Core.Application;
|
||||
using EnvelopeGenerator.Infrastructure;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using Scalar.AspNetCore;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using DigitalData.UserManager.DependencyInjection;
|
||||
using EnvelopeGenerator.Application;
|
||||
using DigitalData.Auth.Client;
|
||||
using DigitalData.Core.Abstractions;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||
using NLog.Web;
|
||||
using NLog;
|
||||
using DigitalData.Auth.Claims;
|
||||
using EnvelopeGenerator.Server;
|
||||
|
||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||
logger.Info("EnvelopeGenerator.Server logging initialized!");
|
||||
|
||||
try
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
{
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Host.UseNLog();
|
||||
}
|
||||
|
||||
var config = builder.Configuration;
|
||||
|
||||
var deferredProvider = new DeferredServiceProvider();
|
||||
|
||||
// Add Blazor services
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents()
|
||||
.AddInteractiveWebAssemblyComponents();
|
||||
|
||||
// Add API Controllers
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// Named HttpClient for internal API calls (same domain, uses relative paths)
|
||||
builder.Services.AddHttpClient("EnvelopeGenerator.Server");
|
||||
|
||||
// CORS Policy
|
||||
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
||||
throw new InvalidOperationException("AllowedOrigins section is missing in the configuration.");
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowSpecificOriginsPolicy", builder =>
|
||||
{
|
||||
builder.WithOrigins(allowedOrigins)
|
||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
// Swagger/OpenAPI
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Version = "v1",
|
||||
Title = "signFLOW Absender-API",
|
||||
Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschlägen in der signFLOW-Anwendung.",
|
||||
Contact = new OpenApiContact
|
||||
{
|
||||
Name = "Digital Data GmbH",
|
||||
Url = new Uri("https://digitaldata.works/digitale-signatur#kontakt"),
|
||||
Email = "info-flow@digitaldata.works"
|
||||
},
|
||||
});
|
||||
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header,
|
||||
Description = "JWT-Autorisierungs-Header unter Verwendung des Bearer-Schemas.",
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
|
||||
var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
|
||||
foreach (var xmlFile in xmlFiles)
|
||||
{
|
||||
options.IncludeXmlComments(xmlFile);
|
||||
}
|
||||
});
|
||||
|
||||
// Database Context
|
||||
var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration");
|
||||
var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default";
|
||||
var connStr = config.GetConnectionString(cnnStrName)
|
||||
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
|
||||
|
||||
builder.Services.Configure<ConnectionString>(cs => cs.Value = connStr);
|
||||
|
||||
builder.Services.AddDbContext<EGDbContext>(options => options.UseSqlServer(connStr));
|
||||
|
||||
// Authentication - AuthHub
|
||||
builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams"));
|
||||
|
||||
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(AuthScheme.Sender, opt =>
|
||||
{
|
||||
opt.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
||||
{
|
||||
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
||||
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
||||
return [publicKey.SecurityKey];
|
||||
},
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = authTokenKeys.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = authTokenKeys.Audience,
|
||||
};
|
||||
|
||||
opt.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
if (context.Token is null)
|
||||
{
|
||||
if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null)
|
||||
context.Token = cookieToken;
|
||||
else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken))
|
||||
context.Token = queryStrToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
})
|
||||
.AddJwtBearer(AuthScheme.Receiver, opt =>
|
||||
{
|
||||
opt.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
||||
{
|
||||
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
||||
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
||||
return [publicKey.SecurityKey];
|
||||
},
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = authTokenKeys.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = authTokenKeys.Audience,
|
||||
};
|
||||
|
||||
opt.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
var envelopeKey = paths?.LastOrDefault();
|
||||
|
||||
if (envelopeKey is not null)
|
||||
{
|
||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken) && cookieToken is not null)
|
||||
context.Token = cookieToken;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
var envelopeKey = paths?.LastOrDefault();
|
||||
|
||||
var sub = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
||||
?? context.Principal?.FindFirst("sub")?.Value;
|
||||
|
||||
if (envelopeKey is null || sub != envelopeKey)
|
||||
context.Fail("Envelope key in the path does not match the token subject.");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Cookie Authentication
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
options.Cookie.SameSite = SameSiteMode.Strict;
|
||||
options.LoginPath = "/api/auth/login";
|
||||
options.LogoutPath = "/api/auth/logout";
|
||||
options.SlidingExpiration = true;
|
||||
});
|
||||
|
||||
// Authorization Policies
|
||||
builder.Services.AddAuthorizationBuilder()
|
||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
||||
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||
.RequireRole(Role.Sender)
|
||||
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||
.AddPolicy(AuthPolicy.Receiver, policy => policy
|
||||
.AddAuthenticationSchemes(AuthScheme.Receiver)
|
||||
.RequireAuthenticatedUser()
|
||||
.RequireRole(Role.Receiver.Full, "receiver"))
|
||||
.AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA));
|
||||
|
||||
// User Manager
|
||||
#pragma warning disable CS0618
|
||||
builder.Services.AddUserManager<EGDbContext>();
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// LDAP Directory Search
|
||||
builder.ConfigureBySection<DirectorySearchOptions>();
|
||||
builder.Services.AddDirectorySearchService(config.GetSection("DirectorySearchOptions"));
|
||||
|
||||
// Localization
|
||||
builder.Services.AddCookieBasedLocalizer();
|
||||
|
||||
// Cache options
|
||||
builder.Services.Configure<CacheOptions>(config.GetSection(CacheOptions.SectionName));
|
||||
|
||||
// Distributed Cache - SQL Server
|
||||
builder.Services.AddDistributedSqlServerCache(options =>
|
||||
{
|
||||
config.GetSection("Cache:SqlServer").Bind(options);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.ConnectionString))
|
||||
{
|
||||
options.ConnectionString = connStr;
|
||||
}
|
||||
});
|
||||
|
||||
// Envelope Generator Infrastructure & Application Services
|
||||
#pragma warning disable CS0618
|
||||
builder.Services
|
||||
.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
||||
{
|
||||
opt.AddDbTriggerParams(config);
|
||||
opt.AddDbContext((provider, options) =>
|
||||
{
|
||||
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
|
||||
options.UseSqlServer(connStr)
|
||||
.LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace)
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableDetailedErrors();
|
||||
});
|
||||
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
|
||||
})
|
||||
.AddEnvelopeGeneratorServices(config);
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// HttpClient for server-side components (e.g., MainLayout with FontLoader)
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
// Business Services (Server specific)
|
||||
builder.Services.AddScoped<DocumentService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<AnnotationService>();
|
||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||
builder.Services.AddScoped<SignatureService>();
|
||||
builder.Services.AddScoped<SignatureCacheService>();
|
||||
builder.Services.AddSingleton<AppVersionService>();
|
||||
|
||||
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
||||
builder.Services.AddDevExpressBlazor();
|
||||
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
||||
|
||||
// Configuration Options
|
||||
builder.Services.Configure<EnvelopeGenerator.Server.Client.Options.ApiOptions>(
|
||||
builder.Configuration.GetSection("ApiOptions"));
|
||||
builder.Services.Configure<EnvelopeGenerator.Server.Client.Options.PdfViewerOptions>(
|
||||
builder.Configuration.GetSection("PdfViewerOptions"));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
deferredProvider.Factory = () => app.Services;
|
||||
|
||||
// Exception handling middleware for API controllers
|
||||
app.UseMiddleware<EnvelopeGenerator.Server.Middleware.ExceptionHandlingMiddleware>();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseWebAssemblyDebugging();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.MapScalarApiReference();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
// Set CORS policy
|
||||
app.UseCors("AllowSpecificOriginsPolicy");
|
||||
|
||||
// Localization
|
||||
string[] supportedCultureNames = ["de-DE", "en-US"];
|
||||
IList<CultureInfo> list = [.. supportedCultureNames.Select(cn => new CultureInfo(cn))];
|
||||
var cultureInfo = list.FirstOrDefault() ?? throw new InvalidOperationException("There is no supported culture.");
|
||||
var requestLocalizationOptions = new RequestLocalizationOptions
|
||||
{
|
||||
SupportedCultures = list,
|
||||
SupportedUICultures = list
|
||||
};
|
||||
requestLocalizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
|
||||
app.UseRequestLocalization(requestLocalizationOptions);
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// API Controllers (map before Blazor routing)
|
||||
app.MapControllers();
|
||||
|
||||
// Blazor routing
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.AddAdditionalAssemblies(typeof(EnvelopeGenerator.Server.Client._Imports).Assembly);
|
||||
|
||||
app.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Stopped program because of exception");
|
||||
throw;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:5131",
|
||||
"sslPort": 8088
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"https (Blazor UI)": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
|
||||
},
|
||||
"https (Swagger API)": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"http (Development)": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5131",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AuthClientParams": {
|
||||
"Url": "http://172.24.12.39:9090/auth-hub",
|
||||
"PublicKeys": [
|
||||
{
|
||||
"Issuer": "auth.digitaldata.works",
|
||||
"Audience": "sign-flow.digitaldata.works"
|
||||
}
|
||||
],
|
||||
"RetryDelay": "00:00:05"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"UseSwagger": true,
|
||||
"UseDbMigration": false,
|
||||
"DiPMode": true,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AllowedOrigins": [
|
||||
"http://localhost:4200",
|
||||
"http://172.24.12.39:9090",
|
||||
"https://localhost:8088",
|
||||
"http://localhost:5131",
|
||||
"http://localhost:7192"
|
||||
],
|
||||
"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;"
|
||||
},
|
||||
"DirectorySearchOptions": {
|
||||
"ServerName": "DD-VMP01-DC01",
|
||||
"Root": "DC=dd-gan,DC=local,DC=digitaldata,DC=works",
|
||||
"UserCacheExpirationDays": 1,
|
||||
"CustomSearchFilters": {
|
||||
"User": "(&(objectClass=user)(sAMAccountName=*))",
|
||||
"Group": "(&(objectClass=group)(samAccountName=*))"
|
||||
}
|
||||
},
|
||||
"AuthClientParams": {
|
||||
"Url": "http://172.24.12.39:9090/auth-hub",
|
||||
"PublicKeys": [
|
||||
{
|
||||
"Issuer": "auth.digitaldata.works",
|
||||
"Audience": "sign-flow.digitaldata.works"
|
||||
}
|
||||
],
|
||||
"RetryDelay": "00:00:05"
|
||||
},
|
||||
"AuthTokenKeys": {
|
||||
"Cookie": "AuthToken",
|
||||
"QueryString": "AuthToken",
|
||||
"Issuer": "auth.digitaldata.works",
|
||||
"Audience": "sign-flow.digitaldata.works"
|
||||
},
|
||||
"ApiOptions": {
|
||||
"BaseUrl": ""
|
||||
},
|
||||
"PdfViewerOptions": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
"ThumbnailEnableHiDPI": true,
|
||||
"MainCanvasEnableHiDPI": true,
|
||||
"ZoomStepPercentage": 5
|
||||
},
|
||||
"PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==",
|
||||
"Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value.
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'nonce-{0}' 'unsafe-eval'",
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com:*",
|
||||
"img-src 'self' data: https: blob:",
|
||||
"font-src 'self' https://fonts.gstatic.com:*",
|
||||
"connect-src 'self' https://nominatim.openstreetmap.org:* http://localhost:* https://localhost:* ws://localhost:* wss://localhost:* blob:",
|
||||
"frame-src 'self'",
|
||||
"media-src 'self'",
|
||||
"object-src 'self'"
|
||||
],
|
||||
"NLog": {
|
||||
"throwConfigExceptions": true,
|
||||
"variables": {
|
||||
"logDirectory": "E:\\LogFiles\\Digital Data\\signFlow",
|
||||
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Server"
|
||||
},
|
||||
"targets": {
|
||||
"infoLogs": {
|
||||
"type": "File",
|
||||
"fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log",
|
||||
"maxArchiveDays": 30
|
||||
},
|
||||
"errorLogs": {
|
||||
"type": "File",
|
||||
"fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log",
|
||||
"maxArchiveDays": 30
|
||||
},
|
||||
"criticalLogs": {
|
||||
"type": "File",
|
||||
"fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log",
|
||||
"maxArchiveDays": 30
|
||||
}
|
||||
},
|
||||
// Trace, Debug, Info, Warn, Error and *Fatal*
|
||||
"rules": [
|
||||
{
|
||||
"logger": "*",
|
||||
"minLevel": "Info",
|
||||
"maxLevel": "Warn",
|
||||
"writeTo": "infoLogs"
|
||||
},
|
||||
{
|
||||
"logger": "*",
|
||||
"level": "Error",
|
||||
"writeTo": "errorLogs"
|
||||
},
|
||||
{
|
||||
"logger": "*",
|
||||
"level": "Fatal",
|
||||
"writeTo": "criticalLogs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ContactLink": {
|
||||
"Label": "Kontakt",
|
||||
"Href": "https://digitaldata.works/",
|
||||
"HrefLang": "de",
|
||||
"Target": "_blank",
|
||||
"Title": "Digital Data GmbH"
|
||||
},
|
||||
/* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx).
|
||||
To add a new language, first you should write the required resx file.
|
||||
first is the default culture name. */
|
||||
"Cultures": [
|
||||
{
|
||||
"Language": "de-DE",
|
||||
"FIClass": "fi-de"
|
||||
},
|
||||
{
|
||||
"Language": "en-US",
|
||||
"FIClass": "fi-us"
|
||||
}
|
||||
],
|
||||
"DisableMultiLanguage": false,
|
||||
"Regexes": [
|
||||
{
|
||||
"Pattern": "/^\\p{L}+(?:([\\ \\-\\']|(\\.\\ ))\\p{L}+)*$/u",
|
||||
"Name": "City",
|
||||
"Platforms": [ ".NET" ]
|
||||
},
|
||||
{
|
||||
"Pattern": "/^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$/",
|
||||
"Name": "City",
|
||||
"Platforms": [ "javascript" ]
|
||||
}
|
||||
],
|
||||
"CustomImages": {
|
||||
"App": {
|
||||
"Src": "/img/DD_signFLOW_LOGO.png",
|
||||
"Classes": {
|
||||
"Main": "signFlow-logo"
|
||||
}
|
||||
},
|
||||
"Company": {
|
||||
"Src": "/img/digital_data.svg",
|
||||
"Classes": {
|
||||
"Show": "dd-show-logo",
|
||||
"Locked": "dd-locked-logo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DispatcherParams": {
|
||||
"SendingProfile": 1,
|
||||
"AddedWho": "DDEnvelopGenerator",
|
||||
"ReminderTypeId": 202377,
|
||||
"EmailAttmt1": ""
|
||||
},
|
||||
"MailParams": {
|
||||
"Placeholders": {
|
||||
"[NAME_PORTAL]": "signFlow",
|
||||
"[SIGNATURE_TYPE]": "signieren",
|
||||
"[REASON]": ""
|
||||
}
|
||||
},
|
||||
"GtxMessagingParams": {
|
||||
"Uri": "https://rest.gtx-messaging.net",
|
||||
"Path": "smsc/sendsms/f566f7e5-bdf2-4a9a-bf52-ed88215a432e/json",
|
||||
"Headers": {},
|
||||
"QueryParams": {
|
||||
"from": "signFlow"
|
||||
}
|
||||
},
|
||||
"TFARegParams": {
|
||||
"TimeLimit": "00:30:00"
|
||||
},
|
||||
"DbTriggerParams": {
|
||||
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||
"EnvelopeHistory": [ "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" ]
|
||||
},
|
||||
"Cache": {
|
||||
"SignatureCacheExpiration": null,
|
||||
"SqlServer": {
|
||||
"ConnectionString": null,
|
||||
"SchemaName": "dbo",
|
||||
"TableName": "TBDD_CACHE"
|
||||
}
|
||||
},
|
||||
"MainPageTitle": null,
|
||||
"AnnotationParams": {
|
||||
"Background": {
|
||||
"Margin": 0.20,
|
||||
"BackgroundColor": {
|
||||
"R": 222,
|
||||
"G": 220,
|
||||
"B": 215
|
||||
},
|
||||
"BorderColor": {
|
||||
"R": 204,
|
||||
"G": 202,
|
||||
"B": 198
|
||||
},
|
||||
"BorderStyle": "underline",
|
||||
"BorderWidth": 4
|
||||
},
|
||||
"DefaultAnnotation": {
|
||||
"Width": 1,
|
||||
"Height": 0.5,
|
||||
"MarginTop": 1
|
||||
},
|
||||
"Annotations": [
|
||||
{
|
||||
"Name": "Signature",
|
||||
"MarginTop": 0
|
||||
},
|
||||
{
|
||||
"Name": "PositionLabel",
|
||||
"VerBoundAnnotName": "Signature",
|
||||
"WidthRatio": 1.2,
|
||||
"HeightRatio": 0.5,
|
||||
"MarginTopRatio": 0.22
|
||||
},
|
||||
{
|
||||
"Name": "Position",
|
||||
"VerBoundAnnotName": "PositionLabel",
|
||||
"WidthRatio": 1.2,
|
||||
"HeightRatio": 0.5,
|
||||
"MarginTopRatio": -0.05
|
||||
},
|
||||
{
|
||||
"Name": "CityLabel",
|
||||
"VerBoundAnnotName": "Position",
|
||||
"WidthRatio": 1.2,
|
||||
"HeightRatio": 0.5,
|
||||
"MarginTopRatio": 0.05
|
||||
},
|
||||
{
|
||||
"Name": "City",
|
||||
"VerBoundAnnotName": "CityLabel",
|
||||
"WidthRatio": 1.2,
|
||||
"HeightRatio": 0.5,
|
||||
"MarginTopRatio": -0.05
|
||||
},
|
||||
{
|
||||
"Name": "DateLabel",
|
||||
"VerBoundAnnotName": "City",
|
||||
"WidthRatio": 1.55,
|
||||
"HeightRatio": 0.5,
|
||||
"MarginTopRatio": 0.05
|
||||
},
|
||||
{
|
||||
"Name": "Date",
|
||||
"VerBoundAnnotName": "DateLabel",
|
||||
"WidthRatio": 1.55,
|
||||
"HeightRatio": 0.5,
|
||||
"MarginTopRatio": -0.1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user