Compare commits

...

4 Commits

26 changed files with 328 additions and 277 deletions

View File

@@ -40,13 +40,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Update="Resources\Resource.de_DE.resx"> <EmbeddedResource Update="Resources\Resource.de-DE.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.en_US.resx"> <EmbeddedResource Update="Resources\Resource.en-US.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>

View File

@@ -117,10 +117,28 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="de_DE" xml:space="preserve"> <data name="de-DE" xml:space="preserve">
<value>Deutch</value> <value>Deutch</value>
</data> </data>
<data name="en_US" xml:space="preserve"> <data name="en-US" xml:space="preserve">
<value>Englisch</value> <value>Englisch</value>
</data> </data>
<data name="LocakedOpen" xml:space="preserve">
<value>Öffnen</value>
</data>
<data name="LockedAccessCode" xml:space="preserve">
<value>Zugriffscode</value>
</data>
<data name="LockedBody" xml:space="preserve">
<value>Wir haben Ihnen gerade den Zugriffscode an die hinterlegte Email Adresse gesendet. Dies kann evtl. einige Minuten dauern.</value>
</data>
<data name="LockedFooterBody" xml:space="preserve">
<value>Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender bitten, Ihnen den Code auf anderem Wege zukommen zu lassen.</value>
</data>
<data name="LockedFooterTitle" xml:space="preserve">
<value>Sie haben keinen Zugriffscode erhalten?</value>
</data>
<data name="LockedTitle" xml:space="preserve">
<value>Dokument erfordert einen Zugriffscode</value>
</data>
</root> </root>

View File

@@ -117,7 +117,28 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Hello" xml:space="preserve"> <data name="de-DE" xml:space="preserve">
<value>Hallo!!</value> <value>German</value>
</data>
<data name="en-US" xml:space="preserve">
<value>English</value>
</data>
<data name="LocakedOpen" xml:space="preserve">
<value>Open</value>
</data>
<data name="LockedAccessCode" xml:space="preserve">
<value>Access Code</value>
</data>
<data name="LockedBody" xml:space="preserve">
<value>We have just sent you the access code to the email address you provided. This may take a few minutes.</value>
</data>
<data name="LockedFooterBody" xml:space="preserve">
<value>Please check your email inbox including your spam folder. Furthermore, you can also ask the sender to send the code by other means.</value>
</data>
<data name="LockedFooterTitle" xml:space="preserve">
<value>You have not received an access code?</value>
</data>
<data name="LockedTitle" xml:space="preserve">
<value>Document requires an access code</value>
</data> </data>
</root> </root>

View File

@@ -1,123 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Hello" xml:space="preserve">
<value>Hello!!</value>
</data>
</root>

View File

@@ -2,10 +2,10 @@
using DigitalData.Core.Application; using DigitalData.Core.Application;
using EnvelopeGenerator.Application.Contracts; using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@@ -3,11 +3,11 @@ using DigitalData.Core.Application;
using DigitalData.Core.DTO; using DigitalData.Core.DTO;
using EnvelopeGenerator.Application.Contracts; using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@@ -3,11 +3,11 @@ using DigitalData.Core.Application;
using DigitalData.Core.DTO; using DigitalData.Core.DTO;
using EnvelopeGenerator.Application.Contracts; using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure.Contracts; using EnvelopeGenerator.Infrastructure.Contracts;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Application.Services namespace EnvelopeGenerator.Application.Services
{ {

View File

@@ -120,7 +120,9 @@
<Compile Include="frmFinalizePDF.Designer.vb"> <Compile Include="frmFinalizePDF.Designer.vb">
<DependentUpon>frmFinalizePDF.vb</DependentUpon> <DependentUpon>frmFinalizePDF.vb</DependentUpon>
</Compile> </Compile>
<Compile Include="frmFinalizePDF.vb" /> <Compile Include="frmFinalizePDF.vb">
<SubType>Form</SubType>
</Compile>
<Compile Include="frmReportViewer.Designer.vb"> <Compile Include="frmReportViewer.Designer.vb">
<DependentUpon>frmReportViewer.vb</DependentUpon> <DependentUpon>frmReportViewer.vb</DependentUpon>
</Compile> </Compile>

View File

@@ -6,27 +6,14 @@ namespace EnvelopeGenerator.Web.Controllers
{ {
public class BaseController : Controller public class BaseController : Controller
{ {
internal DatabaseService database; protected readonly DatabaseService database;
internal State state;
internal ILogger _logger; protected readonly ILogger _logger;
public BaseController(DatabaseService database, ILogger logger) public BaseController(DatabaseService database, ILogger logger)
{ {
this.database = database; this.database = database;
this.state = database.State;
_logger = logger; _logger = logger;
} }
internal ObjectResult ErrorResponse(Exception e)
{
// Log the detailed error message.
_logger.LogError(e, "An unexpected error occurred.");
return Problem(
statusCode: 500,
detail: e.Message,
type: "ServerError");
}
} }
} }

View File

@@ -41,7 +41,8 @@ namespace EnvelopeGenerator.Web.Controllers
} }
catch(Exception ex) catch(Exception ex)
{ {
return ErrorResponse(ex); _logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
} }
} }
@@ -66,7 +67,8 @@ namespace EnvelopeGenerator.Web.Controllers
} }
catch(Exception ex) catch(Exception ex)
{ {
return ErrorResponse(ex); _logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
} }
} }

View File

@@ -43,7 +43,8 @@ namespace EnvelopeGenerator.Web.Controllers
} }
catch (Exception e) catch (Exception e)
{ {
return ErrorResponse(e); _logger.LogError(e, "{Message}", e.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
} }
} }
@@ -88,7 +89,8 @@ namespace EnvelopeGenerator.Web.Controllers
} }
catch (Exception e) catch (Exception e)
{ {
return ErrorResponse(e); _logger.LogError(e, "{Message}", e.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
} }
} }
} }

View File

@@ -11,10 +11,11 @@ using DigitalData.Core.API;
using EnvelopeGenerator.Application; using EnvelopeGenerator.Application;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using DigitalData.Core.DTO; using DigitalData.Core.DTO;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Application.DTOs; using EnvelopeGenerator.Application.DTOs;
using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Localization;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using EnvelopeGenerator.Web.Models;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.Web.Controllers namespace EnvelopeGenerator.Web.Controllers
{ {
@@ -26,8 +27,9 @@ namespace EnvelopeGenerator.Web.Controllers
private readonly IStringLocalizer<Resource> _localizer; private readonly IStringLocalizer<Resource> _localizer;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly UrlEncoder _urlEncoder; private readonly UrlEncoder _urlEncoder;
private readonly Cultures _cultures;
public HomeController(DatabaseService databaseService, EnvelopeOldService envelopeOldService, ILogger<HomeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, UrlEncoder urlEncoder) : base(databaseService, logger) public HomeController(DatabaseService databaseService, EnvelopeOldService envelopeOldService, ILogger<HomeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, UrlEncoder urlEncoder, Cultures cultures) : base(databaseService, logger)
{ {
this.envelopeOldService = envelopeOldService; this.envelopeOldService = envelopeOldService;
_envRcvService = envelopeReceiverService; _envRcvService = envelopeReceiverService;
@@ -35,6 +37,7 @@ namespace EnvelopeGenerator.Web.Controllers
_localizer = localizer; _localizer = localizer;
_configuration = configuration; _configuration = configuration;
_urlEncoder = urlEncoder; _urlEncoder = urlEncoder;
_cultures = cultures;
} }
[HttpGet("EnvelopeKey/{envelopeReceiverId}")] [HttpGet("EnvelopeKey/{envelopeReceiverId}")]
@@ -68,18 +71,30 @@ namespace EnvelopeGenerator.Web.Controllers
} }
catch(Exception ex) catch(Exception ex)
{ {
_logger.LogEnvelopeError(envelopeEeceiverId: envelopeReceiverId, exception:ex, message: _localizer[MessageKey.UnexpectedError]); _logger.LogEnvelopeError(envelopeEeceiverId: envelopeReceiverId, exception:ex, message: _localizer[WebKey.UnexpectedError]);
return this.ViewInnerServiceError(); return this.ViewInnerServiceError();
} }
} }
[HttpGet("EnvelopeKey/{envelopeReceiverId}/Locked")] [HttpGet("EnvelopeKey/{envelopeReceiverId}/Locked")]
public async Task<IActionResult> EnvelopeLocked([FromRoute] string envelopeReceiverId) public async Task<IActionResult> EnvelopeLocked([FromRoute] string envelopeReceiverId, [FromQuery] string? culture)
{ {
try try
{ {
culture = culture is not null ? _urlEncoder.Encode(culture) : null;
envelopeReceiverId = _urlEncoder.Encode(envelopeReceiverId); envelopeReceiverId = _urlEncoder.Encode(envelopeReceiverId);
ViewData["Languages"] = _configuration.GetSection("Languages").Get<string[]>()!;
if (UserLanguage is null && culture is null)
{
UserLanguage = _cultures.Default.Language;
return Redirect($"{Request.Headers["Referer"]}?culture={_cultures.Default.Language}");
}
if (UserLanguage is not null && culture is not null)
{
return Redirect($"Locked");
}
ViewData["UserLanguage"] = UserLanguage; ViewData["UserLanguage"] = UserLanguage;
return await _envRcvService.IsExisting(envelopeReceiverId: envelopeReceiverId).ThenAsync( return await _envRcvService.IsExisting(envelopeReceiverId: envelopeReceiverId).ThenAsync(
@@ -107,7 +122,7 @@ namespace EnvelopeGenerator.Web.Controllers
if(uuid is null || signature is null) if(uuid is null || signature is null)
{ {
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer[MessageKey.WrongEnvelopeReceiverId]); _logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer[WebKey.WrongEnvelopeReceiverId]);
return Unauthorized(); return Unauthorized();
} }
@@ -226,50 +241,60 @@ namespace EnvelopeGenerator.Web.Controllers
return Ok(new { EnvelopeUuid = envelopeUuid, ReceiverSignature = receiverSignature }); return Ok(new { EnvelopeUuid = envelopeUuid, ReceiverSignature = receiverSignature });
} }
[NonAction] [HttpPost("lang/{language}")]
public IActionResult GetLanguage() => Ok(UserLanguage); public IActionResult SetLanguage([FromRoute] string language)
[HttpPost("lang")]
public IActionResult SetLanguage([FromForm] string language)
{ {
try try
{ {
language = _urlEncoder.Encode(language); language = _urlEncoder.Encode(language);
var cookieOptions = new CookieOptions() if (!_cultures.Languages.Contains(language))
{ return BadRequest();
Expires = DateTimeOffset.UtcNow.AddYears(1),
Secure = false,
Path = "/",
SameSite = SameSiteMode.Strict,
HttpOnly = true
};
Response.Cookies.Append( UserLanguage = language;
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(language)),
cookieOptions);
return Redirect(Request.Headers["Referer"].ToString()); return Redirect(Request.Headers["Referer"].ToString());
} }
catch(Exception ex) catch(Exception ex)
{ {
_logger.LogError(ex, ex.Message); _logger.LogError(ex, "{Message}", ex.Message);
return this.ViewEnvelopeNotFound(); return StatusCode(statusCode: StatusCodes.Status500InternalServerError);
} }
} }
private string UserLanguage [HttpGet("lang")]
public IActionResult GetLanguages() => Ok(_cultures.Languages);
private string? UserLanguage
{ {
get get
{ {
return Request.Cookies[CookieRequestCultureProvider.DefaultCookieName] ?? _configuration.GetSection("Languages").Get<string[]>()![0]; var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
if (string.IsNullOrEmpty(cookieValue))
return null;
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
return culture?.Value ?? null;
} }
set set
{ {
Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)), new CookieOptions() if(value is null)
Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName);
else
{ {
Expires = DateTimeOffset.UtcNow.AddYears(1), 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);
}
} }
} }

View File

@@ -1,5 +1,6 @@
using EnvelopeGenerator.Application; using AngleSharp.Common;
using EnvelopeGenerator.Application.Resources; using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Web.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@@ -10,10 +11,21 @@ namespace EnvelopeGenerator.Web.Controllers.Test
public class TestLocalizerController : ControllerBase public class TestLocalizerController : ControllerBase
{ {
private readonly IStringLocalizer _localizer; private readonly IStringLocalizer _localizer;
private readonly Cultures _cultures;
public TestLocalizerController(IStringLocalizer<Resource> localizer) => _localizer = localizer; public TestLocalizerController(IStringLocalizer<Resource> localizer, Cultures cultures)
{
_localizer = localizer;
_cultures = cultures;
}
[HttpGet] [HttpGet]
public IActionResult Localize([FromQuery] string key = "Hello") => Ok(_localizer[key]); public IActionResult Localize([FromQuery] string key = "de_DE") => Ok(_localizer[key]);
[HttpGet("fi-class")]
public IActionResult GetFIClass(string? lang = null) => lang is null ? Ok(_cultures.FIClasses) : Ok(_cultures.FIClassOf(lang));
[HttpGet("culture")]
public IActionResult GetCultures(string? lang = null) => lang is null ? Ok(_cultures) : Ok(_cultures.CultureOf(lang));
} }
} }

View File

@@ -1,12 +0,0 @@
namespace EnvelopeGenerator.Web
{
public static class MessageKey
{
public static readonly string ServiceOutputNullError = "ServiceOutputNullError";
public static readonly string UnexpectedError = "UnexpectedError";
public static readonly string FailedToSendAccessCode = "FailedToSendAccessCode";
public static readonly string WrongEnvelopeReceiverId = "WrongEnvelopeReceiverId";
public static readonly string DataIntegrityError = "DataIntegrityError";
public static readonly string NonDecodableEnvelopeReceiverId = "NonDecodableEnvelopeReceiverId";
}
}

View File

@@ -0,0 +1,8 @@
namespace EnvelopeGenerator.Web.Models
{
public class Culture
{
public string Language { get; init; } = string.Empty;
public string FIClass { get; init; } = string.Empty;
}
}

View File

@@ -0,0 +1,15 @@
namespace EnvelopeGenerator.Web.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? CultureOf(string? language) => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault();
public Culture Default => this.First();
public string FIClassOf(string? language) => language is null ? string.Empty : CultureOf(language)?.FIClass ?? string.Empty;
}
}

View File

@@ -16,7 +16,7 @@ using EnvelopeGenerator.Web.Models;
using DigitalData.Core.DTO; using DigitalData.Core.DTO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Ganss.Xss; using Ganss.Xss;
using EnvelopeGenerator.Web; using Microsoft.Extensions.Options;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!"); logger.Info("Logging initialized!");
@@ -71,8 +71,7 @@ try
//AddEF Core dbcontext //AddEF Core dbcontext
var connStr = config["Config:ConnectionString"]; var connStr = config["Config:ConnectionString"];
builder.Services.AddDbContext<EGDbContext>(options => builder.Services.AddDbContext<EGDbContext>(options => options.UseSqlServer(connStr));
options.UseSqlServer(connStr));
//Inject CRUD Service and repositoriesad //Inject CRUD Service and repositoriesad
builder.Services.AddScoped<IConfigRepository, ConfigRepository>(); builder.Services.AddScoped<IConfigRepository, ConfigRepository>();
@@ -157,7 +156,7 @@ try
builder.Services.AddCookieConsentSettings(); builder.Services.AddCookieConsentSettings();
builder.Services.AddCookieBasedLocalizer(); builder.Services.AddCookieBasedLocalizer();
builder.Services.AddSingleton(HtmlEncoder.Default); builder.Services.AddSingleton(HtmlEncoder.Default);
builder.Services.AddSingleton(UrlEncoder.Default); builder.Services.AddSingleton(UrlEncoder.Default);
builder.Services.AddSingleton(_ => builder.Services.AddSingleton(_ =>
@@ -167,6 +166,10 @@ try
return sanitizer; return sanitizer;
}); });
// Register the FlagIconCssClass instance as a singleton
builder.Services.Configure<Cultures>(builder.Configuration.GetSection("Cultures"));
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<Cultures>>().Value);
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
@@ -177,6 +180,14 @@ try
app.UseHsts(); app.UseHsts();
} }
//Content-Security-Policy
if (config.GetValue<bool>("TestCSP") || !app.Environment.IsDevelopment())
{
var csp_list = config.GetSection("Content-Security-Policy").Get<string[]>();
if (csp_list is not null)
app.UseCSPMiddleware($"{string.Join("; ", csp_list)};");
}
if (config.GetValue<bool>("EnableSwagger")) if (config.GetValue<bool>("EnableSwagger"))
{ {
app.UseSwagger(); app.UseSwagger();
@@ -185,26 +196,33 @@ try
app.UseHttpsRedirection(); app.UseHttpsRedirection();
var csp = config["Content-Security-Policy"];
if(csp is not null)
app.UseCSPMiddleware(csp);
app.UseStaticFiles(); app.UseStaticFiles();
app.UseCookiePolicy(); //app.UseCookiePolicy();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
var languages = config.GetSection("Languages").Get<string[]>() ?? var cultures = app.Services.GetRequiredService<Cultures>();
throw new InvalidOperationException("Languages section is missing in the configuration."); if(!cultures.Any())
if(languages.Length == 0) throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
throw new InvalidOperationException("There is no languages in languages section."); Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
""Cultures"": [
{
""Language"": ""de-DE"",
""FIClass"": ""fi-de""
},
{
""Language"": ""en-US"",
""FIClass"": ""fi-us""
}
]");
if(!config.GetValue<bool>("DisableMultiLanguage")) if(!config.GetValue<bool>("DisableMultiLanguage"))
app.UseCookieBasedLocalizer(languages); app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
app.UseCors("SameOriginPolicy"); app.UseCors("SameOriginPolicy");

View File

@@ -4,7 +4,6 @@
@{ @{
ViewData["Title"] = "Dokument geschützt"; ViewData["Title"] = "Dokument geschützt";
var userLanguage = ViewData["UserLanguage"] as string; var userLanguage = ViewData["UserLanguage"] as string;
var languages = ViewData["Languages"] as string[];
} }
<div class="page container py-5 px-2"> <div class="page container py-5 px-2">
<header class="text-center"> <header class="text-center">
@@ -14,72 +13,60 @@
<path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415" /> <path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415" />
</svg> </svg>
</div> </div>
<h1>Dokument erfordert einen Zugriffscode</h1> <h1>@_localizer[WebKey.LockedTitle]</h1>
</header> </header>
<section class="text-center"> <section class="text-center">
<p>Wir haben Ihnen gerade den Zugriffscode an die hinterlegte Email Adresse gesendet. Dies kann evtl. einige Minuten dauern.</p> <p>@_localizer[WebKey.LockedBody]</p>
</section> </section>
<div class="row m-0 p-0"> <div class="row m-0 p-0 justify-content-center">
<div class="col-8"> <div class="col-8">
<form id="form-access-code" class="form ms-5" method="post"> <form id="form-access-code" class="form ms-5" method="post">
<div class="input"> <div class="input">
<label class="visually-hidden" for="access_code">Zugriffscode</label> <label class="visually-hidden" for="access_code">@_localizer[WebKey.LockedTitle]</label>
<input type="password" id="access_code" class="form-control" name="access_code" placeholder="Zugriffscode" required="required"> <input type="password" id="access_code" class="form-control" name="access_code" placeholder="@_localizer[WebKey.LockedAccessCode]" required="required">
</div> </div>
<div class="button"> <div class="button">
<button type="submit" class="btn btn-primary">Öffnen</button> <button type="submit" class="btn btn-primary">@_localizer[WebKey.LocakedOpen]</button>
</div> </div>
</form> </form>
</div> </div>
<div class="col-4"> <div class="col-4 d-flex justify-content-center align-items-center">
<form class="form ps-4" method="post" action="/lang"> <div class="dropdown">
<div class="dropdown dropdown-flag"> <button class="btn btn-outline-secondary dropdown-toggle" type="button" id="langDropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
<select class="form-select select-flag" name="language" onchange="this.form.submit()"> <span class="fi @_cultures.FIClassOf(userLanguage).TrySanitize(_sanitizer) me-2" id="selectedFlag"></span><span id="selectedLanguage"></span>
@if (languages is not null) </button>
foreach (var lang in languages) <ul class="dropdown-menu" aria-labelledby="langDropdownMenuButton">
{ @foreach(var lang in _cultures.Languages)
<option class="select-option option-flag" value="@lang.TrySanitize(_sanitizer)">@_localizer[lang].Value.TrySanitize(_sanitizer)</option> {
} <li>
</select> <a class="dropdown-item" data-language="@lang.TrySanitize(_sanitizer)" data-flag="@_cultures.FIClassOf(lang).TrySanitize(_sanitizer)">
</div> <span class="fi @_cultures.FIClassOf(lang).TrySanitize(_sanitizer) me-2"></span>@_localizer[lang].Value.TrySanitize(_sanitizer)
</form> </a>
</li>
}
</ul>
</div>
</div> </div>
</div> </div>
<section class="text-center"> <section class="text-center">
<details> <details>
<summary>Sie haben keinen Zugriffscode erhalten?</summary> <summary>@_localizer[WebKey.LockedFooterTitle]</summary>
<p>Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender bitten, Ihnen den Code auf anderem Wege zukommen zu lassen.</p> <p>@_localizer[WebKey.LockedFooterBody]</p>
</details> </details>
</section> </section>
</div> </div>
<footer class="container" id="page-footer">&copy; SignFlow 2023-2024 <a href="https://digitaldata.works">Digital Data GmbH</a></footer> <footer class="container" id="page-footer">&copy; SignFlow 2023-2024 <a href="https://digitaldata.works">Digital Data GmbH</a></footer>
<script nonce="@nonce"> <script nonce="@nonce">
$(document).ready(function () { document.addEventListener('DOMContentLoaded', function () {
$('.select-flag').select2({ var dropdownItems = document.querySelectorAll('.dropdown-item');
templateResult: formatResult, dropdownItems.forEach(function (item) {
templateSelection: formatSelection item.addEventListener('click', async function(event) {
event.preventDefault();
var language = this.getAttribute('data-language');
var flagCode = this.getAttribute('data-flag');
document.getElementById('selectedFlag').className = 'fi ' + flagCode + ' me-2';
await setLanguage(language);
});
}); });
}); });
</script>
function formatResult(state) {
if (!state.id) {
return state.text;
}
var baseUrl = "/img/flags";
var $state = $(
`<span style="font-size: 0.85rem;"><img src="${baseUrl}/${state.element.value}.png" class="img-flag me-3" />${state.text}</span>`
);
return $state;
};
function formatSelection(state) {
if (!state.id) {
return state.text;
}
var baseUrl = "/img/flags";
var $state = $(
`<span class="d-flex justify-content-center align-items-center"><img src="${baseUrl}/${state.element.value}.png" class="img-flag pt-1"/></span>`
);
return $state;
};
</script>

View File

@@ -85,4 +85,4 @@
}) })
</script> </script>
} }
<div id='app' style='background: gray; width: 100vw; height: 100vh; margin: 0 auto;'></div> <div id='app'></div>

View File

@@ -8,6 +8,7 @@
<link rel="stylesheet" href="~/lib/sweetalert2/sweetalert2.min.css" /> <link rel="stylesheet" href="~/lib/sweetalert2/sweetalert2.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/EnvelopeGenerator.Web.styles.css" asp-append-version="true"/> <link rel="stylesheet" href="~/EnvelopeGenerator.Web.styles.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/lib/flag-icons-main/css/flag-icons.min.css" asp-append-version="true" />
<link href="~/lib/select2/dist/css/select2.min.css" rel="stylesheet"/> <link href="~/lib/select2/dist/css/select2.min.css" rel="stylesheet"/>
</head> </head>
<body> <body>

View File

@@ -1,9 +1,10 @@
@using EnvelopeGenerator.Web @using EnvelopeGenerator.Web
@using EnvelopeGenerator.Web.Models @using EnvelopeGenerator.Web.Models
@using Microsoft.Extensions.Localization; @using Microsoft.Extensions.Localization
@using EnvelopeGenerator.Application.Resources; @using EnvelopeGenerator.Application.Resources
@inject IStringLocalizer<Resource> _localizer; @inject IStringLocalizer<Resource> _localizer
@inject System.Text.Encodings.Web.UrlEncoder _encoder @inject System.Text.Encodings.Web.UrlEncoder _encoder
@inject Ganss.Xss.HtmlSanitizer _sanitizer @inject Ganss.Xss.HtmlSanitizer _sanitizer
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor _accessor @inject Microsoft.AspNetCore.Http.IHttpContextAccessor _accessor
@inject Cultures _cultures
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,20 @@
namespace EnvelopeGenerator.Web
{
public static class WebKey
{
public static readonly string ServiceOutputNullError = nameof(ServiceOutputNullError);
public static readonly string UnexpectedError = nameof(UnexpectedError);
public static readonly string FailedToSendAccessCode = nameof(FailedToSendAccessCode);
public static readonly string WrongEnvelopeReceiverId = nameof(WrongEnvelopeReceiverId);
public static readonly string DataIntegrityError = nameof(DataIntegrityError);
public static readonly string NonDecodableEnvelopeReceiverId = nameof(NonDecodableEnvelopeReceiverId);
public static readonly string de_DE = nameof(de_DE).Replace("_", "-");
public static readonly string en_US = nameof(en_US).Replace("_", "-");
public static readonly string LockedTitle = nameof(LockedTitle);
public static readonly string LockedBody = nameof(LockedBody);
public static readonly string LocakedOpen = nameof(LocakedOpen);
public static readonly string LockedAccessCode = nameof(LockedAccessCode);
public static readonly string LockedFooterTitle = nameof(LockedFooterTitle);
public static readonly string LockedFooterBody = nameof(LockedFooterBody);
}
}

View File

@@ -5,13 +5,5 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"Config": {
"ConnectionString": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
//preivous connection string without Encrypt=false and TrustServerCertificate=True -> "Server=sDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;"
"LogPath": "E:\\EnvelopeGenerator\\Logs",
"LogDebug": true,
"LogJson": true,
"AdminPassword": "dd"
} }
} }

View File

@@ -10,9 +10,27 @@
"Microsoft.AspNetCore.Hosting.Diagnostics": "Warning" "Microsoft.AspNetCore.Hosting.Diagnostics": "Warning"
} }
}, },
"Config": {
"ConnectionString": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
"LogPath": "E:\\EnvelopeGenerator\\Logs",
"LogDebug": true,
"LogJson": true,
"AdminPassword": "dd"
},
"PSPDFKitLicenseKey": null, "PSPDFKitLicenseKey": null,
/* The first format parameter {0} will be replaced by the nonce value. */ /* The first format parameter {0} will be replaced by the nonce value. */
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'nonce-{0}'; style-src 'self' 'nonce-{0}'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' wss://localhost:44385 ws://localhost:61446; frame-src 'self'; media-src 'self'; object-src 'self';", "TestCSP": false,
"Content-Security-Policy": [
"default-src 'self'",
"script-src 'self' 'nonce-{0}'",
"style-src 'self' 'nonce-{0}'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' http://localhost:* https://localhost:* ws://localhost:* wss://localhost:*",
"frame-src 'self'",
"media-src 'self'",
"object-src 'self'"
],
"AdminPassword": "dd", "AdminPassword": "dd",
"AllowedOrigins": [ "https://localhost:7202", "https://digitale.unterschrift.wisag.de/" ], "AllowedOrigins": [ "https://localhost:7202", "https://digitale.unterschrift.wisag.de/" ],
"NLog": { "NLog": {
@@ -84,6 +102,15 @@
/* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx). /* 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. To add a new language, first you should write the required resx file.
first is the default culture name. */ first is the default culture name. */
"Languages": [ "de_DE", "en_US" ], "Cultures": [
"DisableMultiLanguage": true {
"Language": "de-DE",
"FIClass": "fi-de"
},
{
"Language": "en-US",
"FIClass": "fi-us"
}
],
"DisableMultiLanguage": false
} }

View File

@@ -5,6 +5,13 @@
/* Toolbar Buttons */ /* Toolbar Buttons */
#app {
background: gray;
width: 100vw;
height: 100vh;
margin: 0 auto;
}
.button-finish { .button-finish {
transition: background-color linear 300ms; transition: background-color linear 300ms;
background-color: #059669; /* emerald-600 */ background-color: #059669; /* emerald-600 */
@@ -198,4 +205,12 @@ footer#page-footer a:focus {
.select2-search__field { .select2-search__field {
display:none display:none
}
.lang-item {
font-size: 0.85rem;
}
#langDropdownMenuButton{
min-width: 4vw;
} }

View File

@@ -159,3 +159,39 @@ class WrappedResponse {
this.fatal = (data === null && error === null) this.fatal = (data === null && error === null)
} }
} }
async function setLangAsync(language, flagCode) {
document.getElementById('selectedFlag').className = 'fi ' + flagCode + ' me-2';
await fetch(`/lang/${language}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
}
async function setLanguage(language) {
const hasLang = await fetch('/lang', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(langs => langs.includes(language))
.catch(err => false);
if(hasLang)
return await fetch(`/lang/${language}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => {
if (response.redirected)
window.location.href = response.url;
else if (!response.ok)
return Promise.reject('Failed to set language');
});
}