Enhance authentication and configuration setup
Introduced a new `AuthScheme` class for JWT authentication schemes. Added `ExceptionHandlingMiddleware` for global exception handling. Updated `Program.cs` to refactor service registrations, including Blazor, API controllers, CORS, and Swagger setup. Removed YARP reverse proxy and added a more comprehensive configuration for authentication and caching. Updated `appsettings.json` and `appsettings.Development.json` with new sections for authentication, logging, and various application-specific settings. Added new classes for handling authentication tokens, connection strings, and cache options.
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI;
|
||||||
|
|
||||||
|
/// <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.WebUI.ReceiverJWT";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scheme name used for per-envelope sender JWT authentication.
|
||||||
|
/// </summary>
|
||||||
|
public const string Sender = "EnvelopeGenerator.WebUI.SenderJWT";
|
||||||
|
}
|
||||||
@@ -8,11 +8,40 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.WebUI.Client\EnvelopeGenerator.WebUI.Client.csproj" />
|
<ProjectReference Include="..\EnvelopeGenerator.WebUI.Client\EnvelopeGenerator.WebUI.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="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.22" />
|
||||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
|
|
||||||
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||||
|
|
||||||
|
<!-- API Packages from EnvelopeGenerator.API -->
|
||||||
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -22,9 +51,6 @@
|
|||||||
<Content Update="wwwroot\docs\privacy-policy.fr-FR.html">
|
<Content Update="wwwroot\docs\privacy-policy.fr-FR.html">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Update="yarp.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.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,28 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the keys and default values used for authentication token handling
|
||||||
|
/// within the Envelope Generator WebUI.
|
||||||
|
/// </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.WebUI.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,18 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.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; }
|
||||||
|
}
|
||||||
@@ -1,35 +1,310 @@
|
|||||||
using EnvelopeGenerator.WebUI.Components;
|
using EnvelopeGenerator.WebUI.Components;
|
||||||
|
using EnvelopeGenerator.WebUI.Models;
|
||||||
|
using EnvelopeGenerator.WebUI.Options;
|
||||||
using DevExpress.Blazor;
|
using DevExpress.Blazor;
|
||||||
using EnvelopeGenerator.WebUI.Client.Services;
|
using EnvelopeGenerator.WebUI.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.WebUI;
|
||||||
|
|
||||||
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
|
logger.Info("EnvelopeGenerator.WebUI logging initialized!");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Load YARP configuration
|
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||||
builder.Configuration.AddJsonFile("yarp.json", optional: false, reloadOnChange: true);
|
|
||||||
|
|
||||||
// Add services to the container.
|
if (!builder.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Host.UseNLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = builder.Configuration;
|
||||||
|
|
||||||
|
var deferredProvider = new DeferredServiceProvider();
|
||||||
|
|
||||||
|
// Add Blazor services
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents()
|
.AddInteractiveServerComponents()
|
||||||
.AddInteractiveWebAssemblyComponents();
|
.AddInteractiveWebAssemblyComponents();
|
||||||
|
|
||||||
|
// Add API Controllers
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// 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)
|
// HttpClient for server-side components (e.g., MainLayout with FontLoader)
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddScoped<HttpClient>(sp => {
|
builder.Services.AddScoped<HttpClient>(sp =>
|
||||||
|
{
|
||||||
var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
|
var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
|
||||||
var request = httpContextAccessor.HttpContext?.Request;
|
var request = httpContextAccessor.HttpContext?.Request;
|
||||||
|
|
||||||
var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
|
var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
|
||||||
|
|
||||||
if (request != null) {
|
if (request != null)
|
||||||
// Set base address to current host (e.g., https://localhost:5131)
|
{
|
||||||
httpClient.BaseAddress = new Uri($"{request.Scheme}://{request.Host}");
|
httpClient.BaseAddress = new Uri($"{request.Scheme}://{request.Host}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return httpClient;
|
return httpClient;
|
||||||
});
|
});
|
||||||
builder.Services.AddHttpClient();
|
|
||||||
|
|
||||||
// Business Services
|
// Business Services (WebUI specific)
|
||||||
builder.Services.AddScoped<DocumentService>();
|
builder.Services.AddScoped<DocumentService>();
|
||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
builder.Services.AddScoped<AnnotationService>();
|
builder.Services.AddScoped<AnnotationService>();
|
||||||
@@ -38,10 +313,6 @@ builder.Services.AddScoped<SignatureService>();
|
|||||||
builder.Services.AddScoped<SignatureCacheService>();
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
|
|
||||||
// YARP Reverse Proxy
|
|
||||||
builder.Services.AddReverseProxy()
|
|
||||||
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
|
||||||
|
|
||||||
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
||||||
builder.Services.AddDevExpressBlazor();
|
builder.Services.AddDevExpressBlazor();
|
||||||
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
||||||
@@ -54,30 +325,62 @@ builder.Services.Configure<EnvelopeGenerator.WebUI.Client.Options.PdfViewerOptio
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
deferredProvider.Factory = () => app.Services;
|
||||||
|
|
||||||
|
// Exception handling middleware for API controllers
|
||||||
|
app.UseMiddleware<EnvelopeGenerator.WebUI.Middleware.ExceptionHandlingMiddleware>();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseWebAssemblyDebugging();
|
app.UseWebAssemblyDebugging();
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
app.MapScalarApiReference();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
|
||||||
app.UseHsts();
|
app.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.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseAntiforgery();
|
app.UseAntiforgery();
|
||||||
|
|
||||||
// Blazor routing (BEFORE YARP - important order!)
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// API Controllers (map before Blazor routing)
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Blazor routing
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode()
|
.AddInteractiveServerRenderMode()
|
||||||
.AddInteractiveWebAssemblyRenderMode()
|
.AddInteractiveWebAssemblyRenderMode()
|
||||||
.AddAdditionalAssemblies(typeof(EnvelopeGenerator.WebUI.Client._Imports).Assembly);
|
.AddAdditionalAssemblies(typeof(EnvelopeGenerator.WebUI.Client._Imports).Assembly);
|
||||||
|
|
||||||
// YARP proxy (AFTER Blazor - catch-all for /api/*)
|
|
||||||
app.MapReverseProxy();
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Stopped program because of exception");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,5 +4,15 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"UseSwagger": true,
|
||||||
|
"UseDbMigration": false,
|
||||||
|
"DiPMode": true,
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
@@ -6,6 +9,42 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"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": {
|
"ApiOptions": {
|
||||||
"BaseUrl": ""
|
"BaseUrl": ""
|
||||||
},
|
},
|
||||||
@@ -14,5 +53,215 @@
|
|||||||
"ThumbnailEnableHiDPI": true,
|
"ThumbnailEnableHiDPI": true,
|
||||||
"MainCanvasEnableHiDPI": true,
|
"MainCanvasEnableHiDPI": true,
|
||||||
"ZoomStepPercentage": 5
|
"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": [
|
||||||
|
"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.WebUI"
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user