This commit is contained in:
Zeeshaun
2026-03-13 09:11:35 -05:00
parent f004c8205a
commit c2d6bda458
197 changed files with 14970 additions and 0 deletions
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.Configurations;
public class ApiConfiguration
{
public string AppName { get; set; }
public string SeqUrl { get; set; }
public string SeqApiKey { get; set; }
public int FileUploadCutoffHours { get; set; }
public string[] CorsAllowedOrigins { get; set; }
public List<string> MaliciousFileFoundEmailList { get; set; }
public bool IsAksDeploy { get; set; }
}
@@ -0,0 +1,588 @@
using LCU.API.Internal.Document.Services;
using LCU.Library.Internal.ApiSecurity.Attributes;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using LCU.Library.Internal.DocumentModels.Responses;
using LCU.Library.Internal.Exceptions;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using Microsoft.AspNetCore.Authorization;
namespace LCU.API.Internal.Document.Controllers;
[ApiController]
[Route("[controller]")]
public class DocumentController : ControllerBase
{
private readonly IDocumentService _documentService;
private readonly IDocumentAuditService _documentAuditService;
private readonly List<DocumentAction> _allowedExternalActions = new()
{
DocumentAction.Download
};
public DocumentController(IDocumentService documentService,
IDocumentAuditService documentAuditService)
{
_documentService = documentService;
_documentAuditService = documentAuditService;
}
[ApiKey]
[HttpGet("{documentId:guid}")]
[ProducesResponseType(typeof(DocumentResponse), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentById(Guid documentId)
{
try
{
var result = await _documentService.GetDocument(documentId);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("{personOrOrgNumber:long}/{documentId:guid}")]
[ProducesResponseType(typeof(DocumentResponse), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetMemberDocumentById(long personOrOrgNumber, Guid documentId)
{
try
{
var result = await _documentService.GetDocument(personOrOrgNumber, documentId);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Obsolete]
[HttpGet("GetDocumentByBlobId/{documentBlobId:guid}")]
[ProducesResponseType(typeof(byte[]), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentByBlobId(Guid documentBlobId)
{
try
{
var result = await _documentService.GetDocumentByBlobId(documentBlobId);
if (result == null)
{
return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("Content/{documentBlobId:guid}")]
[ProducesResponseType(typeof(byte[]), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentContentByBlobId(Guid documentBlobId)
{
try
{
var result = await _documentService.GetDocumentByBlobId(documentBlobId);
if (result == null)
{
return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("{personOrOrgNumber:long}/Content/{documentBlobId:guid}")]
[ProducesResponseType(typeof(byte[]), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetMemberDocumentContentByBlobId(long personOrOrgNumber, Guid documentBlobId)
{
try
{
var result = await _documentService.GetDocumentByBlobId(personOrOrgNumber, documentBlobId);
if (result == null)
{
return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpPost("Search")]
[ProducesResponseType(typeof(List<DocumentInformationResponse>),200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> PostDocumentInformationByFilter([FromBody] DocumentInformationFilteredSearch documentInformationFilteredSearch)
{
try
{
var result = await _documentService.SearchDocumentInformation(documentInformationFilteredSearch.Filter, documentInformationFilteredSearch.Skip, documentInformationFilteredSearch.Take, documentInformationFilteredSearch.SortDefinition);
if (!result.Any())
{
return NoContent();
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("AuditInformation/{documentId:guid}")]
[ProducesResponseType(typeof(List<DocumentAuditResponse>),200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentAuditInformationById(Guid documentId)
{
try
{
var result = await _documentAuditService.GetDocumentAudits(documentId);
if (!result.Any())
{
return NoContent();
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("AuditInformation")]
[ProducesResponseType(typeof(List<DocumentAuditResponse>), 200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentAuditInformationByIds([FromQuery]Guid[] documentIds)
{
try
{
var result = await _documentAuditService.GetDocumentAudits(documentIds);
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPost("{documentId:guid}/Actions")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> ExternalActionPerformed([FromRoute]Guid documentId, [FromBody] DocumentActionNotificationRequest action)
{
try
{
if (!_allowedExternalActions.Contains(action.Action))
{
throw new BadHttpRequestException($"{action.Action.ToString()} action type not allowed externally.");
}
await _documentAuditService.AuditAction(documentId,
HttpContext.User.Claims.Single(c => c.Type == "name").Value, action.Source, action.Action);
return Ok();
}
catch (BadHttpRequestException ex)
{
return StatusCode(StatusCodes.Status400BadRequest, ex.Message);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> UploadDocument([FromBody] DocumentRequest request)
{
try
{
var result = await _documentService.UploadDocument(request);
return CreatedAtAction(nameof(GetDocumentById), new { documentId = result.DocumentId }, result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPut("{documentId:guid}")]
[ProducesResponseType(typeof(DocumentInformationResponse),200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> UpdateDocument([FromBody] UpdateDocumentRequest request, Guid documentId)
{
try
{
var result = await _documentService.UpdateDocument(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPut("DocumentContent/{documentId:guid}")]
[ProducesResponseType(typeof(DocumentInformationResponse),200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> UpdateDocumentContent([FromBody] UpdateDocumentContentRequest request, Guid documentId)
{
try
{
var result = await _documentService.UpdateDocumentContent(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPut("DocumentInformation/{documentId:guid}")]
[ProducesResponseType(typeof(DocumentInformationResponse),200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> UpdateDocumentInformation([FromBody] UpdateDocumentInformationRequest request,
Guid documentId)
{
try
{
var result = await _documentService.UpdateDocumentInformation(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("DocumentInformation/{documentId:guid}")]
[ProducesResponseType(typeof(DocumentInformationResponse),200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentInformation(Guid documentId)
{
try
{
var result = await _documentService.GetDocumentInformation(documentId);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPut("DocumentScanStatus")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> UpdateDocumentScanStatus([FromBody] UpdateDocumentScanStatusRequest request)
{
try
{
await _documentService.UpdateDocumentScanStatus(request.ScanStatus, request.DocumentBlobIds, HttpContext.User.Claims.Single(c => c.Type == "name").Value);
return Ok();
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// This is currently a developer endpoint, still waiting on guidance on how the 'Delete' Operation
/// should work. This will remove both the documents contents from blob storage and its information from cosmos.
/// </summary>
/// <param name="documentId"></param>
/// <returns></returns>
[ApiKey]
[HttpDelete("{documentId:guid}")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> DeleteDocument(Guid documentId)
{
try
{
await _documentService.DeleteDocument(documentId);
return Ok();
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpDelete("DeleteDocumentContent")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> DeleteDocumentContent([FromBody] DeleteDocumentContentRequest request)
{
try
{
await _documentService.DeleteDocumentContent(request);
return Ok();
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpPost("Count")]
[ProducesResponseType(typeof(long), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> CountDocuments(DocumentInformationFilter filter)
{
try
{
var count = await _documentService.CountDocuments(filter);
return Ok(count);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpPost("GetDocumentsForCleanup")]
[ProducesResponseType(typeof(IEnumerable<Guid>), 200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentsForCleanup([FromBody] GetDocumentCleanupRequest request)
{
try
{
var documents = await _documentService.GetDocumentsForCleanup(request);
if (!documents.Any())
{
return NoContent();
}
return Ok(documents);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,36 @@
using System.Net;
using LCU.API.Internal.Document.Models;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using LCU.Library.Internal.ApiSecurity.Attributes;
namespace LCU.API.Internal.Document.Controllers;
[Route("[controller]")]
[ApiController]
public class StatusController : ControllerBase
{
private readonly IEnvironmentView _environmentView;
public StatusController(IEnvironmentView environmentView)
{
_environmentView = environmentView;
}
[HttpGet]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
Log.Verbose($"{_environmentView.Application}'s Status Endpoint Was Hit");
return Ok(_environmentView);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to return status");
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
}
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.Exceptions;
public class BlobApiServiceException : Exception
{
public BlobApiServiceException(string message) : base(message)
{
}
public BlobApiServiceException(string message, Exception exception) : base(message, exception)
{
}
}
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.Exceptions;
public class DocumentAuditServiceException : Exception
{
public DocumentAuditServiceException(string message) : base(message)
{
}
public DocumentAuditServiceException(string message, Exception exception) : base(message, exception)
{
}
}
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.Exceptions;
public class DocumentInformationServiceException : Exception
{
public DocumentInformationServiceException(string message) : base(message)
{
}
public DocumentInformationServiceException(string message, Exception exception) : base(message, exception)
{
}
}
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702; 8618; 1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702; 8618; 1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
<PackageReference Include="Azure.Identity" Version="1.13.2" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.3.0" />
<PackageReference Include="LCU.Api.Internal.MaidBotLib" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.ApiSecurity" Version="1.0.1" />
<PackageReference Include="LCU.Library.Internal.Exceptions" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.Logging" Version="1.1.0" />
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="2.5.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="LCU.Library.Internal.Configuration" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.API.Internal.DocumentLib\LCU.API.Internal.DocumentLib.csproj" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties appsettings_1staging_1json__JsonSchema="https://github.com/fbecart/zinoma/releases/latest/download/zinoma-schema.json" /></VisualStudio></ProjectExtensions>
<ItemGroup>
<InternalsVisibleTo Include="LCU.Library.Internal.DocumentModelsTest" />
</ItemGroup>
</Project>
@@ -0,0 +1,35 @@
using System.Reflection;
namespace LCU.API.Internal.Document.Models;
public class EnvironmentView : IEnvironmentView
{
private readonly IWebHostEnvironment _env;
public EnvironmentView(IWebHostEnvironment env)
{
_env = env;
}
public string Application => _env.ApplicationName;
public DateTime CurrentDate => DateTime.Now;
public string BuildVersion => AssembleBuildVersion();
public DateTime BuildDate => AssemblyBuildDate();
public string Environment => _env.EnvironmentName;
private static DateTime AssemblyBuildDate()
{
//https://stackoverflow.com/a/804310/266594
var assembly = Assembly.GetExecutingAssembly();
var fileInfo = new FileInfo(assembly.Location);
var lastModified = fileInfo.LastWriteTime;
return lastModified;
}
private static string AssembleBuildVersion()
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
return version == null ? "-.-.-" : version.ToString();
}
}
@@ -0,0 +1,10 @@
namespace LCU.API.Internal.Document.Models;
public interface IEnvironmentView
{
string Application { get; }
DateTime CurrentDate { get; }
string BuildVersion { get; }
DateTime BuildDate { get; }
string Environment { get; }
}
@@ -0,0 +1,263 @@
using System.Text.Json.Serialization;
using Azure.Identity;
using LCU.API.Internal.Document.Configurations;
using LCU.API.Internal.Document.Models;
using LCU.API.Internal.Document.Services;
using LCU.API.Internal.DocumentLib.Configurations;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.DataAccess;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.Library.Internal.Logging;
using LCU.Library.Internal.Logging.Middleware;
using LCU.Library.Internal.Logging.Models;
using Microsoft.OpenApi.Models;
using LCU.Library.Internal.ApiSecurity;
using Serilog;
using LCU.API.Internal.DocumentLib.Utilities;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
using MongoDB.Bson.Serialization;
using LCU.Library.Internal.Configuration;
using Microsoft.FeatureManagement;
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = BuildBuilder(args);
BuildServicesAndConfigurations(builder);
BuildAppAndRun(builder);
WebApplicationBuilder BuildBuilder(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("/app/config/mapped-appsettings/primary.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/documentapi_configuration.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-azure-client-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-azure-tenant-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-client-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-client-secret.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-tenant-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-swagger-oauth-scope.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-seq-api-key.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-blob-storage-connection-string.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-mongo-database-connection-string.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-uploader-api-key.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-retriever-api-key.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-cleanup-api-key.json", optional: true, reloadOnChange: true);
if (!bool.Parse(builder.Configuration.GetSection("Primary")["IsAksDeploy"]))
{
var clientSecretCredentials = new ClientSecretCredential(builder.Configuration["TenantId"], builder.Configuration["ClientId"], builder.Configuration["ClientSecret"]);
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{builder.Configuration["AzureKeyVaultUrl"]}.vault.azure.net/"), clientSecretCredentials);
}
builder.Host.UseSerilog();
return builder;
}
void BuildServicesAndConfigurations(WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd"));
services.AddAuthorization(options =>
{
options.AddPolicy("user_token", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
options.DefaultPolicy = options.GetPolicy("user_token")!;
});
services.AddFeatureManagement();
var documentTypeConfiguration = configuration.Bind<ApiConfiguration>(services, "Primary");
configuration.Bind<BlobStorageConfiguration>(services, "BlobStorage");
configuration.Bind<DatabaseConfiguration>(services, "MongoDatabase");
configuration.Bind<NotificationApiConfiguration>(services);
var loggerConfiguration = new LoggingConfiguration(documentTypeConfiguration.AppName,
documentTypeConfiguration.SeqApiKey,
documentTypeConfiguration.SeqUrl);
var lcuLogging = new LcuLogging(loggerConfiguration);
lcuLogging.UseLcuLogging();
ServiceRegistrations.AddDocumentServices(services);
ServiceRegistrations.AddDocumentDataAccesses(services);
ServiceRegistrations.AddDocumentRepositories(services);
BsonSerializer.RegisterDiscriminatorConvention(typeof(DocumentAudit), new DocumentAuditDiscriminatorConvention());
services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(s =>
{
s.SwaggerDoc("v1", new OpenApiInfo { Title = $"{documentTypeConfiguration.AppName}", Version = "v1" });
s.UseOneOfForPolymorphism();
var authority = $"https://login.microsoftonline.com/{configuration.GetSection("AzureAd")["TenantId"]}";
s.AddSecurityDefinition
(
nameof(SecuritySchemeType.OpenIdConnect),
new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri($"{authority}/oauth2/v2.0/authorize"),
TokenUrl = new Uri($"{authority}/oauth2/v2.0/token")
}
}
//24.10.2022 these SHOULD have enabled OIDC discovery (the discovery info is returned from this URI) but swagger UI is not displaying the auth options
//oidc discovery does work, I have set it up with IdentityServer4 in the past
//Type = SecuritySchemeType.OpenIdConnect,
//OpenIdConnectUrl = new Uri($"https://login.microsoftonline.com/{configuration.GetSection("AzureAd")["TenantId"]}/v2.0/.well-known/openid-configuration")
}
);
s.AddSecurityRequirement
(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = nameof(SecuritySchemeType.OpenIdConnect)
}
},
Array.Empty<string>()
}
}
);
s.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "ApiKey"
});
s.AddSecurityDefinition("ApiClient", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "Client"
});
s.AddSecurityRequirement
(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "ApiKey"
}
},
Array.Empty<string>()
}
}
);
s.AddSecurityRequirement
(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "ApiClient"
}
},
Array.Empty<string>()
}
}
);
});
services.AddApiKeySecurity(configuration);
}
void BuildAppAndRun(WebApplicationBuilder builder)
{
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage();
app.UseSwagger(s =>
{
s.PreSerializeFilters.Add((doc, request) =>
{
doc.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"{request.Scheme}://{request.Host.Value}{request.PathBase}" } };
});
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("v1/swagger.json", "LCU.API.Internal.Document v1");
c.OAuthClientId(app.Configuration.GetSection("AzureAd")["ClientId"]);
c.OAuthUsePkce();
c.OAuthScopes(app.Configuration.GetSection("Swagger")["OAuthScope"]);
});
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest;
});
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseHttpsRedirection();
app.UseAuthentication().UseAuthorization();
app.MapControllers();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
app.Run();
}
public static class ServiceRegistrations
{
public static void AddDocumentServices(IServiceCollection services)
{
services.AddScoped<DocumentService.DocumentIsForMemberStrategy>(p =>
{
const string checkDocumentOwner = "GetDocumentCheckOwnership";
return p.GetRequiredService<IFeatureManager>().IsEnabledAsync(checkDocumentOwner).Result
? DocumentService.Ownership.CheckPersonOrOrgNumber.Strategy
: DocumentService.Ownership.Unchecked.Strategy;
});
services.AddScoped<IDocumentService, DocumentService>();
services.AddScoped<IDocumentInformationService, DocumentInformationService>();
services.AddScoped<IBlobApiService, BlobApiService>();
services.AddScoped<IDocumentAuditService, DocumentAuditService>();
services.AddScoped<IEnvironmentView, EnvironmentView>();
services.AddScoped<INotificationApiService, NotificationApiService>();
}
public static void AddDocumentDataAccesses(IServiceCollection services)
{
services.AddScoped<IBlobStorageClient, BlobStorageClient>();
services.AddScoped<IBlobApiDao, BlobApiDao>();
}
public static void AddDocumentRepositories(IServiceCollection services)
{
services.AddTransient<IDataClient, DataClient>();
services.AddTransient<IMongoCollectionFactory, MongoCollectionFactory>();
services.AddScoped<IDocumentInformationRepository, DocumentInformationRepository>();
services.AddScoped<IDocumentAuditRepository, DocumentAuditRepository>();
services.AddScoped<INotificationApiDao, NotificationApiDao>();
}
}
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:9009",
"sslPort": 44312
}
},
"profiles": {
"LCU.API.Internal.Document": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7037;http://localhost:5037",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,96 @@
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.DocumentLib.DataAccess;
using LCU.Library.Internal.Exceptions;
using Serilog;
namespace LCU.API.Internal.Document.Services;
public class BlobApiService : IBlobApiService
{
private readonly IBlobApiDao _blobApiDao;
public BlobApiService(IBlobApiDao blobApiDao)
{
_blobApiDao = blobApiDao;
}
public async Task<byte[]> GetDocumentContent(Guid? documentBlobId)
{
try
{
if (documentBlobId is null || documentBlobId == Guid.Empty)
{
return Array.Empty<byte>();
}
var result = await _blobApiDao.GetDocumentFromBlob(documentBlobId ?? Guid.Empty);
if (result?.Value == null){
throw new ServiceException($"Could not find the Document Content for DocumentBlobId:{documentBlobId}");
}
await using var memoryStream = new MemoryStream();
await result.Value.Content.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
catch (ServiceException exception){
Log.Error(exception.Message);
throw new BlobApiServiceException(exception.Message);
}
catch (Exception exception){
var message = $"Failure to Retrieve Document Content with DocumentBlobId: {documentBlobId}.";
Log.Error(exception, message);
throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<Guid> UploadDocumentContent(byte[] content)
{
try
{
return await _blobApiDao.SaveDocumentToBlob(content);
}
catch (Exception exception){
const string message = "Failure to Upload Document Content.";
Log.Error(exception, message);
throw new BlobApiServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task UpdateDocumentContent(Guid? documentBlobId, byte[] content)
{
try
{
if (documentBlobId is null || documentBlobId == Guid.Empty)
{
throw new Exception("DocumentBlobId cannot be null.");
}
await _blobApiDao.UpdateDocumentInBlob(documentBlobId ?? Guid.Empty, content);
}
catch (Exception exception){
var message = $"Failure to Update Document with DocumentBlobId: {documentBlobId}";
Log.Error(exception, message);
throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task DeleteDocumentContent(Guid? documentBlobId)
{
try
{
if (documentBlobId is null || documentBlobId == Guid.Empty)
{
throw new Exception("DocumentBlobId cannot be null.");
}
await _blobApiDao.RemoveDocumentFromBlob(documentBlobId ?? Guid.Empty);
}
catch (Exception exception){
var message = $"Failure to Delete Document with DocumentBlobId: {documentBlobId}";
Log.Error(exception, message);
throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}");
}
}
}
@@ -0,0 +1,243 @@
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Responses;
using Serilog;
namespace LCU.API.Internal.Document.Services;
public class DocumentAuditService : IDocumentAuditService
{
private const string NameField = "Name";
private const string TypeField = "Type";
private const string AttributesField = "Attributes";
private const string ScanStatusField = "ScanStatus";
private const string ContentField = "Content";
private readonly IDocumentAuditRepository _documentAuditRepository;
public DocumentAuditService(IDocumentAuditRepository documentAuditRepository)
{
_documentAuditRepository = documentAuditRepository;
}
public async Task<List<DocumentAuditResponse>> GetDocumentAudits(params Guid[] documentIds)
{
try
{
static DocumentAuditResponse ResponseFactory(DocumentAudit audit) => audit switch
{
DocumentActionAudit actionAudit => new DocumentActionAuditResponse
{
Action = actionAudit.Action,
ActionDescription = actionAudit.ActionDescription,
Date = actionAudit.Date,
DocumentId = actionAudit.DocumentId,
Source = actionAudit.Source,
UserId = actionAudit.UserId
},
DocumentFieldAudit fieldAudit => new DocumentFieldAuditResponse
{
Field = fieldAudit.Field,
CurrentValue = fieldAudit.CurrentValue,
PreviousValue = fieldAudit.PreviousValue,
Date = fieldAudit.Date,
DocumentId = fieldAudit.DocumentId,
Source = fieldAudit.Source,
UserId = fieldAudit.UserId
},
_ => throw new ArgumentOutOfRangeException(nameof(audit))
};
return (await _documentAuditRepository.GetDocumentAudits(documentIds))
.Select(ResponseFactory)
.ToList();
}
catch (Exception exception)
{
var message = $"Failure to Retrieve Document Audit Information for DocumentIds: {documentIds}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<IEnumerable<DocumentAudit>> CreateDocumentUploadedAudit(Guid documentId, DateTime uploadDate)
{
try
{
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentId,
"System",
uploadDate,
"System",
DocumentAction.Upload,
"Document Uploaded"
)
};
return await _documentAuditRepository.SaveDocumentAudits(audits);
}
catch (Exception exception)
{
var message = $"Failure to Create Document Audit for DocumentId: {documentId}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<IEnumerable<DocumentAudit>> CreateDocumentDeletedAudit(Guid documentId,
DateTime deleteDate)
{
try
{
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentId,
"System",
deleteDate,
"System",
DocumentAction.Delete,
"Document Deleted"
)
};
return await _documentAuditRepository.SaveDocumentAudits(audits);
}
catch (Exception exception)
{
var message = $"Failure to Create Document Audit for DocumentId: {documentId}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<IEnumerable<DocumentAudit>> CreateDocumentContentDeletedAudit(Guid documentId,
DateTime deleteDate,
string? reason)
{
try
{
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentId,
"System",
deleteDate,
"System",
DocumentAction.DeleteContent,
"DocumentContent Deleted" + (!string.IsNullOrWhiteSpace(reason) ? $". Reason: {reason}" : string.Empty)
)
};
return await _documentAuditRepository.SaveDocumentAudits(audits);
}
catch (Exception exception)
{
var message = $"Failure to Create Document Audit for DocumentId: {documentId}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<IEnumerable<DocumentAudit>> CreateDocumentAudits(DocumentInformation oldDocumentInformation,
DocumentInformation newDocumentInformation, bool wasDocumentContentUpdated, DateTime updateDate, string userId)
{
DocumentFieldAudit AuditLogFactory(string field, object? oldValue, object? newValue) => new
(
newDocumentInformation.DocumentId,
newDocumentInformation.Source,
updateDate,
userId,
field,
oldValue,
newValue
);
var documentAudits = new List<DocumentFieldAudit>();
try
{
if (oldDocumentInformation.Name != newDocumentInformation.Name){
documentAudits.Add(AuditLogFactory(NameField, oldDocumentInformation.Name, newDocumentInformation.Name));
}
if (oldDocumentInformation.Type != newDocumentInformation.Type){
documentAudits.Add(AuditLogFactory(TypeField, oldDocumentInformation.Type, newDocumentInformation.Type));
}
if (oldDocumentInformation.ScanStatus != newDocumentInformation.ScanStatus){
documentAudits.Add(AuditLogFactory(ScanStatusField, oldDocumentInformation.ScanStatus, newDocumentInformation.ScanStatus));
}
if (!AttributesAreEqual(oldDocumentInformation.Attributes, newDocumentInformation.Attributes)){
documentAudits.Add(AuditLogFactory(AttributesField, oldDocumentInformation.Attributes, newDocumentInformation.Attributes));
}
if (wasDocumentContentUpdated){
documentAudits.Add(AuditLogFactory(ContentField, "NOT CAPTURED", "NOT CAPTURED"));
}
if (!documentAudits.Any()){
return new List<DocumentAudit>();
}
return await _documentAuditRepository.SaveDocumentAudits(documentAudits.ToArray());
}
catch (Exception exception){
var message =
$"Failure to Create Document Audit Information for DocumentId: {oldDocumentInformation.DocumentId}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task AuditAction(Guid documentId, string userId, string actionSource, DocumentAction action)
{
try
{
var description = action switch
{
DocumentAction.Download => $"Document downloaded from {actionSource}.",
DocumentAction.Upload => "Document Uploaded",
DocumentAction.Delete => "Document Deleted",
DocumentAction.DeleteContent => "DocumentContent Deleted",
_ => throw new ArgumentOutOfRangeException()
};
await _documentAuditRepository.SaveDocumentAudits(new DocumentActionAudit(documentId, actionSource, DateTime.Now, userId, action, description));
}
catch (Exception exception)
{
var message =
$"Failure to Create Document Audit Information for DocumentId: {documentId} on DocumentAction: {action} from source {actionSource}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message}. Exception Message: {exception.Message}");
}
}
private static bool AttributesAreEqual(Dictionary<string, string>? oldAttributes, Dictionary<string, string>? newAttributes)
{
oldAttributes ??= new Dictionary<string, string>();
newAttributes ??= new Dictionary<string, string>();
if (oldAttributes.Count != newAttributes.Count){
return false;
}
var attributes = oldAttributes
.Where(d2 => newAttributes.Any(d1 => d1.Key == d2.Key && d2.Value.Equals(d1.Value))).ToList();
return attributes.Count == oldAttributes.Count;
}
}
@@ -0,0 +1,164 @@
using LCU.API.Internal.Document.Configurations;
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using Serilog;
namespace LCU.API.Internal.Document.Services;
public class DocumentInformationService : IDocumentInformationService
{
private readonly IDocumentInformationRepository _documentInformationRepository;
private readonly ApiConfiguration _apiConfiguration;
public DocumentInformationService(IDocumentInformationRepository documentInformationRepository,
ApiConfiguration apiConfiguration)
{
_documentInformationRepository = documentInformationRepository;
_apiConfiguration = apiConfiguration;
}
public async Task<DocumentInformation?> GetDocumentInformation(Guid documentId)
{
try
{
var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId);
if (documentInformation == null ||
documentInformation.UploadDate >= DateTime.UtcNow.AddHours(_apiConfiguration.FileUploadCutoffHours) ||
documentInformation.ScanStatus == false)
{
return null;
}
return documentInformation;
}
catch (Exception exception)
{
var message = $"Failure to Retrieve DocumentInformation with DocumentId: {documentId}.";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformation?> GetDocumentInformationByBlobId(Guid? documentBlobId)
{
try
{
if (documentBlobId is null || documentBlobId == Guid.Empty)
{
return null;
}
var documentInformation = await _documentInformationRepository.GetDocumentInformationByBlobId(documentBlobId ?? Guid.Empty);
return documentInformation ?? null;
}
catch (Exception exception)
{
var message = $"Failure to Retrieve DocumentInformation with DocumentBlobId: {documentBlobId}.";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task DeleteDocumentInformation(Guid documentId)
{
try
{
var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return;
}
await _documentInformationRepository.RemoveDocumentInformation(documentId);
}
catch (Exception exception)
{
var message = $"Failure to Delete DocumentInformation with DocumentId: {documentId}";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformation> UploadDocumentInformation(DocumentInformation information)
{
try{
return await _documentInformationRepository.SaveDocumentInformation(information);
}
catch (Exception exception)
{
var message = $"Failure to Upload DocumentInformation with Name: {information.Name} of type: {information.Type} from source: {information.Source}";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformation?> UpdateDocumentInformation(Guid documentId, DocumentInformation information)
{
try
{
var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
return await _documentInformationRepository.UpdateDocumentInformation(documentId, information);
}
catch (Exception exception)
{
var message = $"Failure to Update DocumentInformation with DocumentId: {documentId} and Name: {information.Name} of type: {information.Type}.";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<List<DocumentInformation>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null)
{
try
{
var documentInformationFound = await _documentInformationRepository.SearchDocumentInformation(filter, skip, take, sortDefinition);
return documentInformationFound ?? new List<DocumentInformation>();
}
catch (Exception exception)
{
const string message = "Failure to Search Document Information";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<long> CountDocuments(DocumentInformationFilter filter)
{
try
{
return await _documentInformationRepository.CountDocuments(filter);
}
catch (Exception exception)
{
const string message = "Failure to Count Document Information";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request)
{
try
{
return await _documentInformationRepository.GetDocumentsForCleanup(request);
}
catch (Exception exception)
{
const string message = "Failure to GetDocumentsForCleanup";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
}
@@ -0,0 +1,748 @@
using System.Numerics;
using System.Text.Json;
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using LCU.Library.Internal.DocumentModels.Responses;
using LCU.Library.Internal.Exceptions;
using LCU.Api.Internal.MaidBotLib.Models;
using Serilog;
using LCU.API.Internal.NotificationsApiLib.Models;
using LCU.API.Internal.Document.Configurations;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
namespace LCU.API.Internal.Document.Services;
// TODO: Add hash of content to document information so the content can be compared before updates.
// Any basic hash should be fine since it will only be used as a checksum to the content.
// Example can be found https://stackoverflow.com/questions/10520048/calculate-md5-checksum-for-a-file/10520086#10520086
public class DocumentService : IDocumentService
{
private readonly IBlobApiService _blobApiService;
private readonly IDocumentInformationService _documentInformationService;
private readonly IDocumentAuditService _documentAuditService;
private readonly INotificationApiService _notificationApiService;
private readonly ApiConfiguration _apiConfiguration;
private readonly DocumentIsForMemberStrategy DocumentIsForMember;
public DocumentService
(
IBlobApiService blobApiService,
IDocumentInformationService documentInformationService,
IDocumentAuditService documentAuditService,
INotificationApiService notificationApiService,
ApiConfiguration apiConfiguration,
DocumentIsForMemberStrategy documentIsForMember
)
{
_blobApiService = blobApiService;
_documentInformationService = documentInformationService;
_documentAuditService = documentAuditService;
_notificationApiService = notificationApiService;
_apiConfiguration = apiConfiguration;
DocumentIsForMember = documentIsForMember;
}
public async Task<DocumentResponse?> GetDocument(Guid documentId)
{
try{
var documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
return new DocumentResponse
{
Content = await _blobApiService.GetDocumentContent(documentInformation.DocumentBlobId),
DocumentInformation = ConvertDocumentInformationToDocumentInformationResponse(documentInformation)
};
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
var message =
$"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Retrieve Document with DocumentId: {documentId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentResponse?> GetDocument(long personOrOrgNumber, Guid documentId)
{
try{
var documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation is null)
{
return null;
}
if(!DocumentIsForMember(documentInformation, personOrOrgNumber)) return null;
return new DocumentResponse
{
Content = await _blobApiService.GetDocumentContent(documentInformation.DocumentBlobId),
DocumentInformation = ConvertDocumentInformationToDocumentInformationResponse(documentInformation)
};
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
var message =
$"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Retrieve Document with DocumentId: {documentId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<byte[]?> GetDocumentByBlobId(Guid documentBlobId)
{
try
{
return await _blobApiService.GetDocumentContent(documentBlobId);
}
catch (BlobApiServiceException exception)
{
var message =
$"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception)
{
var message = $"Failure to Retrieve Document with DocumentBlobId: {documentBlobId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<byte[]?> GetDocumentByBlobId(long personOrOrgNumber, Guid documentBlobId)
{
try
{
var document = await _documentInformationService.GetDocumentInformationByBlobId(documentBlobId);
if(document is null) return null;
if(!DocumentIsForMember(document, personOrOrgNumber)) return null;
return await _blobApiService.GetDocumentContent(documentBlobId);
}
catch (BlobApiServiceException exception)
{
var message =
$"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception)
{
var message = $"Failure to Retrieve Document with DocumentBlobId: {documentBlobId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task DeleteDocument(Guid documentId)
{
try{
var deleteDate = DateTime.UtcNow;
var documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return;
}
await _documentInformationService.DeleteDocumentInformation(documentId);
await _blobApiService.DeleteDocumentContent(documentInformation.DocumentBlobId);
await _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Remove Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
var message =
$"Failure to Remove Document Content from Blob Storage, but DocumentInformation was removed. Exception Message: {exception.Message}";
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Delete Document with DocumentId: {documentId}";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task DeleteDocumentContent(DeleteDocumentContentRequest request)
{
try
{
var deleteDate = DateTime.UtcNow;
var documentInformation = await _documentInformationService.GetDocumentInformation(request.DocumentId);
if (documentInformation?.DocumentBlobId is null)
{
return;
}
await _blobApiService.DeleteDocumentContent(documentInformation.DocumentBlobId);
documentInformation.DocumentBlobId = null;
await _documentInformationService.UpdateDocumentInformation(documentInformation.DocumentId, documentInformation);
await _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, request.Reason);
}
catch (DocumentInformationServiceException exception){
var message =
$"Failure to Remove Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
var message =
$"Failure to Remove Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Delete Document Content with DocumentId: {request.DocumentId}";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse> UploadDocument(DocumentRequest request)
{
var docBlobId = Guid.Empty;
try{
docBlobId = await _blobApiService.UploadDocumentContent(request.Content);
var fileSize = CalculateFileSize(request.Content);
var uploadDate = DateTime.UtcNow;
var documentInformation = new DocumentInformation
{
DocumentId = Guid.NewGuid(),
DocumentBlobId = docBlobId,
TraceId = request.TraceId,
Name = request.Name,
Type = request.Type,
Source = request.Source,
DataSource = request.DataSource,
PersonNumbers = request.PersonNumbers,
OrganizationNumbers = request.OrganizationNumbers,
AccountNumber = request.AccountNumber,
Attributes = request.Attributes,
CreatedDate = request.CreatedDate,
FileSize = fileSize,
UploadDate = DateTime.UtcNow,
ScanStatus = null
};
var uploadedDocumentInformation = await _documentInformationService.UploadDocumentInformation(documentInformation);
await _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate);
return ConvertDocumentInformationToDocumentInformationResponse(uploadedDocumentInformation);
}
catch (BlobApiServiceException exception){
var message = $"Failure to Upload Content to Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (DocumentInformationServiceException exception){
await RollBackDocumentContent(docBlobId);
var message = $"Failure to Upload Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message =
$"Failure to Upload Document Name: {request.Name} of type: {request.Type} from source: {request.Source}";
Log.Error(exception, message);
throw new ServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse?> UpdateDocument(UpdateDocumentRequest request, Guid documentId, string userId)
{
DocumentInformation? documentInformation = null;
var modifiedDate = DateTime.UtcNow;
try{
documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
var content = request.Content;
var fileSize = CalculateFileSize(content);
var updatedDocumentInformation = new DocumentInformation
{
DocumentId = documentId,
DocumentBlobId = documentInformation.DocumentBlobId,
TraceId = documentInformation.TraceId,
UploadDate = documentInformation.UploadDate,
Source = documentInformation.Source,
DataSource = documentInformation.DataSource,
CreatedDate = documentInformation.CreatedDate,
Name = request.Name,
Type = request.Type,
PersonNumbers = request.PersonNumbers,
OrganizationNumbers = request.OrganizationNumbers,
AccountNumber = request.AccountNumber,
Attributes = request.Attributes,
LastModifiedDate = modifiedDate,
FileSize = fileSize,
ScanStatus = null
};
var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation);
await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, true, modifiedDate, userId);
await _blobApiService.UpdateDocumentContent(documentInformation.DocumentBlobId, content);
return ConvertDocumentInformationToDocumentInformationResponse(docInfo);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Update Document Information for DocumentId: {documentId}. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (DocumentAuditServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Update Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Update Document with DocumentId: {documentId} Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}.";
Log.Error(exception, message);
throw new ServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse?> GetDocumentInformation(Guid documentId)
{
try{
var documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
return ConvertDocumentInformationToDocumentInformationResponse(documentInformation);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Retrieve Document with DocumentId: {documentId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse?> UpdateDocumentContent(UpdateDocumentContentRequest request, Guid documentId, string userId)
{
DocumentInformation? documentInformation = null;
var modifiedDate = DateTime.UtcNow;
try{
documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
var content = request.Content;
var fileSize = CalculateFileSize(content);
var updatedDocumentInformation = new DocumentInformation
{
DocumentId = documentId,
DocumentBlobId = documentInformation.DocumentBlobId,
TraceId = documentInformation.TraceId,
Name = documentInformation.Name,
Type = documentInformation.Type,
Source = documentInformation.Source,
DataSource = documentInformation.DataSource,
PersonNumbers = documentInformation.PersonNumbers,
OrganizationNumbers = documentInformation.OrganizationNumbers,
AccountNumber = documentInformation.AccountNumber,
Attributes = documentInformation.Attributes,
UploadDate = documentInformation.UploadDate,
CreatedDate = documentInformation.CreatedDate,
LastModifiedDate = modifiedDate,
FileSize = fileSize,
ScanStatus = null
};
var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation);
await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, true, modifiedDate, userId);
await _blobApiService.UpdateDocumentContent(documentInformation.DocumentBlobId, content);
return ConvertDocumentInformationToDocumentInformationResponse(docInfo);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Update Document Information for DocumentId: {documentId}. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (DocumentAuditServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Update Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Update Document Content for DocumentId: {documentId}.";
Log.Error(exception, message);
throw new ServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse?> UpdateDocumentInformation(UpdateDocumentInformationRequest request, Guid documentId, string userId)
{
DocumentInformation? documentInformation = null;
var modifiedDate = DateTime.UtcNow;
try{
documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
var updatedDocumentInformation = new DocumentInformation
{
DocumentId = documentId,
DocumentBlobId = documentInformation.DocumentBlobId,
TraceId = documentInformation.TraceId,
Source = documentInformation.Source,
DataSource = documentInformation.DataSource,
CreatedDate = documentInformation.CreatedDate,
Name = request.Name,
Type = request.Type,
PersonNumbers = request.PersonNumbers,
OrganizationNumbers = request.OrganizationNumbers,
AccountNumber = request.AccountNumber,
Attributes = request.Attributes,
UploadDate = documentInformation.UploadDate,
LastModifiedDate = modifiedDate,
FileSize = documentInformation.FileSize,
ScanStatus = documentInformation.ScanStatus
};
var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation);
await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, false, modifiedDate, userId);
return ConvertDocumentInformationToDocumentInformationResponse(docInfo);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Update Document Information for DocumentId: {documentId} with Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (DocumentAuditServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Update Document Information for DocumentId: {documentId} with Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<List<DocumentInformationResponse>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null)
{
try{
var documentInformationFound = await _documentInformationService.SearchDocumentInformation(filter, skip, take, sortDefinition);
return documentInformationFound.Select(ConvertDocumentInformationToDocumentInformationResponse).ToList();
}
catch (DocumentInformationServiceException exception){
Log.Error(exception, exception.Message);
throw new ServiceException(exception.Message);
}
catch (Exception exception){
const string message = "Failure to Search Document Information";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task UpdateDocumentScanStatus(bool status, List<Guid> documentBlobIds, string userId)
{
foreach (var documentBlobId in documentBlobIds){
DocumentInformation? documentInformation = null;
var modifiedDate = DateTime.UtcNow;
try{
documentInformation = await _documentInformationService.GetDocumentInformationByBlobId(documentBlobId);
if (documentInformation == null)
{
continue;
}
var updatedDocumentInformation = new DocumentInformation
{
DocumentBlobId = documentBlobId,
DocumentId = documentInformation.DocumentId,
Source = documentInformation.Source,
DataSource = documentInformation.DataSource,
TraceId = documentInformation.TraceId,
Name = documentInformation.Name,
Type = documentInformation.Type,
PersonNumbers = documentInformation.PersonNumbers,
OrganizationNumbers = documentInformation.OrganizationNumbers,
AccountNumber = documentInformation.AccountNumber,
Attributes = documentInformation.Attributes,
UploadDate = documentInformation.UploadDate,
CreatedDate = documentInformation.CreatedDate,
FileSize = documentInformation.FileSize,
ScanStatus = status,
LastModifiedDate = modifiedDate
};
await _documentInformationService.UpdateDocumentInformation(updatedDocumentInformation.DocumentId,
updatedDocumentInformation);
await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, false,
modifiedDate, userId);
if (!status)
{
await NotifyAboutMaliciousFile(documentInformation);
}
}
catch (DocumentInformationServiceException exception){
Log.Error(exception, $"Failure to Update Document Scan Status for DocumentBlobId: {documentBlobId}");
}
catch (DocumentAuditServiceException exception){
await RollbackDocumentInformation(documentInformation.DocumentId, documentInformation);
var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}";
Log.Error(exception, message);
}
catch (Exception exception){
Log.Error(exception,$"Failure to Update Document Scan Status for DocumentBlobId: {documentBlobId}, caused by unexpected exception");
}
}
}
public async Task<long> CountDocuments(DocumentInformationFilter filter)
{
try
{
return await _documentInformationService.CountDocuments(filter);
}
catch (DocumentInformationServiceException exception)
{
Log.Error(exception, exception.Message);
throw new ServiceException(exception.Message);
}
catch (Exception exception)
{
const string message = "Failure to Count Document Information";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request)
{
try
{
return await _documentInformationService.GetDocumentsForCleanup(request);
}
catch (DocumentInformationServiceException exception)
{
Log.Error(exception, exception.Message);
throw new ServiceException(exception.Message);
}
catch (Exception exception)
{
const string message = "Failure GetDocumentsForCleanup";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
private async Task RollBackDocumentContent(Guid docBlobId)
{
try{
Log.Information($"Removing {docBlobId} from Blob Storage, as part of a rollback transaction.");
await _blobApiService.DeleteDocumentContent(docBlobId);
}
catch (Exception inception){
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment))
.Error(inception,
$"Failed to remove {docBlobId} from Blob Storage, an inconsistency in data now exists between DocumentInformation and Content. Exception Message: {inception.Message}");
}
}
private async Task RollbackDocumentInformation(Guid documentId, DocumentInformation documentInformation)
{
try{
Log.Information(
$"Reverting Document Information for DocumentId:{documentId}, as part of a rollback transaction.");
await _documentInformationService.UpdateDocumentInformation(documentId, documentInformation);
}
catch (Exception inception){
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment))
.Error($"Failed to update document information for DocumentId:{documentId}, an inconsistency in data now exists between DocumentInformation. DocumentInformation should be updated to: {JsonSerializer.Serialize(documentInformation)} Exception Message: {inception.Message}");
}
}
private static BigInteger CalculateFileSize(byte[] bytes)
{
var fileBase64Value = Convert.ToBase64String(bytes);
var size = fileBase64Value.Length / 4 * 3;
if (fileBase64Value.EndsWith("=="))
size -= 2;
else if (fileBase64Value.EndsWith("=")) size -= 1;
return size;
}
private static DocumentInformationResponse ConvertDocumentInformationToDocumentInformationResponse(
DocumentInformation updatedDocumentInformation)
{
return new DocumentInformationResponse
{
DocumentId = updatedDocumentInformation.DocumentId,
DocumentBlobId = updatedDocumentInformation.DocumentBlobId,
TraceId = updatedDocumentInformation.TraceId,
Name = updatedDocumentInformation.Name,
Type = updatedDocumentInformation.Type,
Source = updatedDocumentInformation.Source,
DataSource = updatedDocumentInformation.DataSource,
FileSize = updatedDocumentInformation.FileSize,
ScanStatus = updatedDocumentInformation.ScanStatus,
PersonNumbers = updatedDocumentInformation.PersonNumbers,
OrganizationNumbers = updatedDocumentInformation.OrganizationNumbers,
AccountNumber = updatedDocumentInformation.AccountNumber,
Attributes = updatedDocumentInformation.Attributes,
UploadDate = updatedDocumentInformation.UploadDate,
LastModifiedDate = updatedDocumentInformation.LastModifiedDate,
CreatedDate = updatedDocumentInformation.CreatedDate
};
}
private async Task NotifyAboutMaliciousFile(DocumentInformation documentInformation)
{
try
{
var options = new JsonSerializerOptions { WriteIndented = true };
var documentAttributesSerialized =
JsonSerializer.Serialize(
documentInformation.Attributes,
new JsonSerializerOptions { WriteIndented = true }
);
var emailRequest = new EmailRequest()
{
Subject = "DocumentApi: Malicious File Reported",
Recipient = _apiConfiguration.MaliciousFileFoundEmailList,
Body = "This is an auto generated message. <br>" +
"<br>" +
"Azure Defender has returned a malicious file from scan<br>" +
"<br>" +
"Document Attributes:<br>" +
$"{documentAttributesSerialized}"
};
await _notificationApiService.SendEmailAsync(emailRequest);
}
catch (Exception ex)
{
var message = $"Failure to send email. Azure Defender returned a file as malicious. DocumentInformation: {JsonSerializer.Serialize(documentInformation)}. Exception Message: {ex.Message}";
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(ex, message);
throw new ServiceException(message);
}
}
public delegate bool DocumentIsForMemberStrategy(DocumentInformation document, long personOrOrgNumber);
public static class Ownership
{
public static class Unchecked
{
public static DocumentIsForMemberStrategy Strategy => DocumentIsForMember;
private static bool DocumentIsForMember(DocumentInformation document, long personOrOrgNumber)
{
var personOrOrgNumbers = (document.PersonNumbers ?? Array.Empty<string>())
.Concat(document.OrganizationNumbers ?? Array.Empty<string>())
.Select(n => long.TryParse(n, out var number) ? number : -1);
Log.Information("checking {personOrOrgNumber} on {document.DocumentId} in {personOrOrgNumbers} ignored", personOrOrgNumber, document.DocumentId, JsonSerializer.Serialize(personOrOrgNumbers));
return true;
}
}
public static class CheckPersonOrOrgNumber
{
public static DocumentIsForMemberStrategy Strategy => DocumentIsForMember;
private static bool DocumentIsForMember(DocumentInformation document, long personOrOrgNumber) => (document.PersonNumbers ?? Array.Empty<string>())
.Concat(document.OrganizationNumbers ?? Array.Empty<string>())
.Select(n => long.TryParse(n, out var number) ? number : -1)
.Contains(personOrOrgNumber);
}
}
}
@@ -0,0 +1,9 @@
namespace LCU.API.Internal.Document.Services;
public interface IBlobApiService
{
Task<byte[]> GetDocumentContent(Guid? documentBlobId);
Task<Guid> UploadDocumentContent(byte[] content);
Task UpdateDocumentContent(Guid? documentBlobId, byte[] content);
Task DeleteDocumentContent(Guid? documentBlobId);
}
@@ -0,0 +1,15 @@
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Responses;
namespace LCU.API.Internal.Document.Services;
public interface IDocumentAuditService
{
Task<List<DocumentAuditResponse>> GetDocumentAudits(params Guid[] documentIds);
Task<IEnumerable<DocumentAudit>> CreateDocumentAudits(DocumentInformation oldDocumentInformation, DocumentInformation newDocumentInformation, bool wasDocumentContentUpdated, DateTime updateDate, string userId);
public Task<IEnumerable<DocumentAudit>> CreateDocumentUploadedAudit(Guid documentId, DateTime uploadDate);
public Task<IEnumerable<DocumentAudit>> CreateDocumentDeletedAudit(Guid documentId, DateTime deleteDate);
public Task<IEnumerable<DocumentAudit>> CreateDocumentContentDeletedAudit(Guid documentId, DateTime deleteDate, string? reason);
Task AuditAction(Guid documentId, string userId, string actionSource, DocumentAction action);
}
@@ -0,0 +1,17 @@
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.DocumentModels.Requests.Search;
namespace LCU.API.Internal.Document.Services;
public interface IDocumentInformationService
{
Task<DocumentInformation?> GetDocumentInformation(Guid documentId);
Task<DocumentInformation?> GetDocumentInformationByBlobId(Guid? documentBlobId);
Task DeleteDocumentInformation(Guid documentId);
Task<DocumentInformation> UploadDocumentInformation(DocumentInformation information);
Task<DocumentInformation?> UpdateDocumentInformation(Guid documentId, DocumentInformation information);
Task<List<DocumentInformation>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null);
Task<long> CountDocuments(DocumentInformationFilter filter);
Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request);
}
@@ -0,0 +1,25 @@
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using LCU.Library.Internal.DocumentModels.Responses;
namespace LCU.API.Internal.Document.Services;
public interface IDocumentService
{
Task<DocumentResponse?> GetDocument(Guid documentId);
Task<DocumentResponse?> GetDocument(long personOrOrgNumber, Guid documentId);
Task<byte[]?> GetDocumentByBlobId(Guid documentBlobId);
Task<byte[]?> GetDocumentByBlobId(long personOrOrgNumber, Guid documentBlobId);
Task<List<DocumentInformationResponse>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null);
Task DeleteDocument(Guid documentId);
Task DeleteDocumentContent(DeleteDocumentContentRequest request);
Task<DocumentInformationResponse> UploadDocument(DocumentRequest request);
Task<DocumentInformationResponse?> GetDocumentInformation(Guid documentId);
Task<DocumentInformationResponse?> UpdateDocument(UpdateDocumentRequest request, Guid documentId, string userId);
Task<DocumentInformationResponse?> UpdateDocumentContent(UpdateDocumentContentRequest request, Guid documentId, string userId);
Task<DocumentInformationResponse?> UpdateDocumentInformation(UpdateDocumentInformationRequest request, Guid documentId, string userId);
Task UpdateDocumentScanStatus(bool status, List<Guid> documentBlobIds, string userId);
Task<long> CountDocuments(DocumentInformationFilter filter);
Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request);
}
@@ -0,0 +1,9 @@
using LCU.API.Internal.NotificationsApiLib.Models;
namespace LCU.API.Internal.Document.Services
{
public interface INotificationApiService
{
Task SendEmailAsync(EmailRequest emailRequest);
}
}
@@ -0,0 +1,30 @@
using LCU.API.Internal.DocumentLib.DataAccess;
using LCU.API.Internal.NotificationsApiLib.Models;
using LCU.Library.Internal.Exceptions;
using Serilog;
namespace LCU.API.Internal.Document.Services
{
public class NotificationApiService : INotificationApiService
{
private readonly INotificationApiDao _notificationServiceApiDao;
public NotificationApiService(INotificationApiDao notificationApiServiceDao)
{
_notificationServiceApiDao = notificationApiServiceDao;
}
public async Task SendEmailAsync(EmailRequest emailRequest)
{
try
{
await _notificationServiceApiDao.SendEmailAsync(emailRequest);
}
catch (Exception exception)
{
Log.Error(exception, $"Failure to send email. Subject: {emailRequest.Subject}; Recipients: {String.Join(", ", emailRequest.Recipient)}");
throw new ServiceException($"Failure to send email. Subject: {emailRequest.Subject}; Recipients: {String.Join(", ", emailRequest.Recipient)}; Exception Message: {exception.Message}");
}
}
}
}
@@ -0,0 +1,58 @@
{
"AzureAd": {
"ClientId": "c92072ab-987f-4b67-9923-730e06b2766f",
"Instance": "https://login.microsoftonline.com/",
"Domain": "landmarkcu.onmicrosoft.com",
"TenantId": "63cd1554-2783-4a86-ae50-b9e6c21c9499",
"CallbackPath": "/signin-oidc"
},
"AzureKeyVaultUrl": "qa-DocumentApi-kv",
"ClientId": "c92072ab-987f-4b67-9923-730e06b2766f",
"ClientSecret": "OjS8Q~TuByD~pylYMu5pbw9UGNG5fldT6eP9YcBI",
"TenantId": "63cd1554-2783-4a86-ae50-b9e6c21c9499",
"Swagger": {
"OAuthScope": "api://c92072ab-987f-4b67-9923-730e06b2766f/user.groups"
},
"Primary": {
"AppName": "Document Api",
"SeqUrl": "https://qa-seq.lcu-internal.com",
"SeqApiKey": "ftd9RlWVEI1ctg3dqusa",
"FileUploadCutoffHours": -2,
"IsAksDeploy": "false",
"CorsAllowedOrigins": [
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://qa-api.lcu-internal.com"
],
"MaliciousFileFoundEmailList": [
"[email protected]",
"[email protected]"
]
},
"BlobStorage": {
"connectionString": "VALUE-IN-KEYVAULT",
"containerName": "documents"
},
"MongoDatabase": {
"connectionString": "VALUE-IN-KEYVAULT",
"databaseName": "DocumentsApi",
"documentContainerName": "DocumentsInformation",
"auditContainerName": "AuditInformation"
},
"ApiSecurityConfiguration": {
"ApiKeyAuthenticationFailureBehavior": "Error",
"ExternalApiKeySet": {
"DocumentTypeUploader": "VALUE-IN-KEYVAULT",
"DocumentTypeRetriever": "VALUE-IN-KEYVAULT",
"DocumentTypeCleanup": "VALUE-IN-KEYVAULT"
},
"InternalApiKeySet": {}
},
"NotificationApiConfiguration": {
"Url": "https://qa-notificationsapi.lcu-internal.com",
"Email": "/api/Email"
},
"FeatureManagement": {
"GetDocumentCheckOwnership": true
}
}