diff --git a/Altinn.Broker.sln b/Altinn.Broker.sln index ef4f6ae3..0440390d 100644 --- a/Altinn.Broker.sln +++ b/Altinn.Broker.sln @@ -17,7 +17,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Dockerfile = Dockerfile EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Broker.Tests", "Test\Altinn.Broker.Tests\Altinn.Broker.Tests.csproj", "{DDFFAD18-D87F-459C-9CEF-9F11B541C627}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Broker.Tests", "Test\Altinn.Broker.Tests\Altinn.Broker.Tests.csproj", "{DDFFAD18-D87F-459C-9CEF-9F11B541C627}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Broker.Application", "src\Altinn.Broker.Application\Altinn.Broker.Application.csproj", "{32DD52AF-A024-4359-9983-698AF514BB31}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,6 +47,10 @@ Global {DDFFAD18-D87F-459C-9CEF-9F11B541C627}.Debug|Any CPU.Build.0 = Debug|Any CPU {DDFFAD18-D87F-459C-9CEF-9F11B541C627}.Release|Any CPU.ActiveCfg = Release|Any CPU {DDFFAD18-D87F-459C-9CEF-9F11B541C627}.Release|Any CPU.Build.0 = Release|Any CPU + {32DD52AF-A024-4359-9983-698AF514BB31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32DD52AF-A024-4359-9983-698AF514BB31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32DD52AF-A024-4359-9983-698AF514BB31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32DD52AF-A024-4359-9983-698AF514BB31}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 612c303b..273b0e4d 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,14 @@ If you need to re-initialize the database during local development, you can dele ### Authorization -To get access to the Broker API, a consumer needs to use a Maskinporten token. Recipients should use the scope altinn:broker.read and senders should use the scope altinn:broker.write. Tokens with both scopes also work. You can create a Maskinporten integration here: +To get access to the Broker API in staging/production, a consumer needs to use a Maskinporten integration. Recipients should use the scope altinn:broker.read and senders should use the scope altinn:broker.write. Tokens with both scopes also work. You can create a Maskinporten integration here: https://selvbetjening-samarbeid-ver2.difi.no/integrations For more on Maskinporten tokens see: https://docs.digdir.no/docs/Maskinporten/maskinporten_guide_apikonsument +When running locally for development, you can use any Maskinporten token. It is not validated. + ### Formatting Formatting of the code base is handled by Dotnet format. [See how to configure it to format-on-save in Visual Studio here.](https://learn.microsoft.com/en-us/community/content/how-to-enforce-dotnet-format-using-editorconfig-github-actions#3---formatting-your-code-locally) diff --git a/Test/Altinn.Broker.Tests/Factories/FileInitializeExtTestFactory.cs b/Test/Altinn.Broker.Tests/Factories/FileInitializeExtTestFactory.cs new file mode 100644 index 00000000..9612ddd3 --- /dev/null +++ b/Test/Altinn.Broker.Tests/Factories/FileInitializeExtTestFactory.cs @@ -0,0 +1,15 @@ +using Altinn.Broker.Models; + +namespace Altinn.Broker.Tests.Factories; +internal static class FileInitializeExtTestFactory +{ + internal static FileInitalizeExt BasicFile() => new FileInitalizeExt() + { + Checksum = null, + FileName = "input.txt", + PropertyList = [], + Recipients = new List { "0192:986252932" }, + Sender = "0192:991825827", + SendersFileReference = "test-data" + }; +} diff --git a/Test/Altinn.Broker.Tests/FileControllerTests.cs b/Test/Altinn.Broker.Tests/FileControllerTests.cs index d2a75526..053c41d2 100644 --- a/Test/Altinn.Broker.Tests/FileControllerTests.cs +++ b/Test/Altinn.Broker.Tests/FileControllerTests.cs @@ -6,10 +6,10 @@ using Altinn.Broker.Core.Models; using Altinn.Broker.Enums; using Altinn.Broker.Models; +using Altinn.Broker.Tests.Factories; using Altinn.Broker.Tests.Helpers; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Identity.Web; using Xunit; @@ -21,12 +21,6 @@ public class FileControllerTests : IClassFixture private readonly HttpClient _recipientClient; private readonly JsonSerializerOptions _responseSerializerOptions; - /** - * Inject a mock bearer configuration that does not verify anything. - * Generate our own JWT with correct scope, expiry and issuer. - * Set it as default request header - * */ - public FileControllerTests(CustomWebApplicationFactory factory) { _factory = factory; @@ -44,23 +38,17 @@ public FileControllerTests(CustomWebApplicationFactory factory) [Fact] - public async Task WhenAllIsOk_NormalFlow_Success() + public async Task NormalFlow_WhenAllIsOK_Success() { - var initializeFileResponse = await _senderClient.PostAsJsonAsync("broker/api/v1/file", new FileInitalizeExt() - { - Checksum = null, - FileName = "input.txt", - PropertyList = [], - Recipients = new List { "0192:986252932" }, - Sender = "0192:991825827", - SendersFileReference = "test-data" - }); + // Initialize + var initializeFileResponse = await _senderClient.PostAsJsonAsync("broker/api/v1/file", FileInitializeExtTestFactory.BasicFile()); + Assert.Equal(System.Net.HttpStatusCode.OK, initializeFileResponse.StatusCode); var fileId = await initializeFileResponse.Content.ReadAsStringAsync(); + var fileAfterInitialize = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}", _responseSerializerOptions); + Assert.NotNull(fileAfterInitialize); + Assert.True(fileAfterInitialize.FileStatus == FileStatusExt.Initialized); - var initializedFile = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}", _responseSerializerOptions); - Assert.NotNull(initializedFile); - Assert.True(initializedFile.FileStatus == FileStatusExt.Initialized); - + // Upload var uploadedFileBytes = Encoding.UTF8.GetBytes("This is the contents of the uploaded file"); using (var content = new ByteArrayContent(uploadedFileBytes)) { @@ -68,25 +56,62 @@ public async Task WhenAllIsOk_NormalFlow_Success() var uploadResponse = await _senderClient.PostAsync($"broker/api/v1/file/{fileId}/upload", content); Assert.True(uploadResponse.IsSuccessStatusCode); } + var fileAfterUpload = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}", _responseSerializerOptions); + Assert.NotNull(fileAfterUpload); + Assert.True(fileAfterUpload.FileStatus == FileStatusExt.Published); // When running integration test this happens instantly as of now. - var uploadedFile = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}", _responseSerializerOptions); - Assert.NotNull(uploadedFile); - Assert.True(uploadedFile.FileStatus == FileStatusExt.Published); // When running integration test this happens instantly as of now. - + // Download var downloadedFile = await _recipientClient.GetAsync($"broker/api/v1/file/{fileId}/download"); var downloadedFileBytes = await downloadedFile.Content.ReadAsByteArrayAsync(); Assert.Equal(uploadedFileBytes, downloadedFileBytes); + // Details var downloadedFileDetails = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}/details", _responseSerializerOptions); Assert.NotNull(downloadedFileDetails); Assert.True(downloadedFileDetails.FileStatus == FileStatusExt.Published); Assert.Contains(downloadedFileDetails.RecipientFileStatusHistory, recipient => recipient.RecipientFileStatusCode == RecipientFileStatusExt.DownloadStarted); + // Confirm await _recipientClient.PostAsync($"broker/api/v1/file/{fileId}/confirmdownload", null); - var confirmedFileDetails = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}/details", _responseSerializerOptions); Assert.NotNull(confirmedFileDetails); Assert.True(confirmedFileDetails.FileStatus == FileStatusExt.AllConfirmedDownloaded); Assert.Contains(confirmedFileDetails.RecipientFileStatusHistory, recipient => recipient.RecipientFileStatusCode == RecipientFileStatusExt.DownloadConfirmed); } + + [Fact] + public async Task DownloadFile_WhenFileDownloadsTwice_ShowLastOccurenceInOverview() + { + // Arrange + var initializeFileResponse = await _senderClient.PostAsJsonAsync("broker/api/v1/file", FileInitializeExtTestFactory.BasicFile()); + var fileId = await initializeFileResponse.Content.ReadAsStringAsync(); + var initializedFile = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}", _responseSerializerOptions); + Assert.NotNull(initializedFile); + var uploadedFileBytes = Encoding.UTF8.GetBytes("This is the contents of the uploaded file"); + using (var content = new ByteArrayContent(uploadedFileBytes)) + { + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + var uploadResponse = await _senderClient.PostAsync($"broker/api/v1/file/{fileId}/upload", content); + Assert.True(uploadResponse.IsSuccessStatusCode); + } + var uploadedFile = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}", _responseSerializerOptions); + + // Act + var downloadedFile1 = await _recipientClient.GetAsync($"broker/api/v1/file/{fileId}/download"); + await Task.Delay(1000); + var downloadedFile2 = await _recipientClient.GetAsync($"broker/api/v1/file/{fileId}/download"); + var downloadedFile1Bytes = await downloadedFile1.Content.ReadAsByteArrayAsync(); + var downloadedFile2Bytes = await downloadedFile2.Content.ReadAsByteArrayAsync(); + Assert.Equal(downloadedFile1Bytes, downloadedFile2Bytes); + + // Assert + var downloadedFileDetails = await _senderClient.GetFromJsonAsync($"broker/api/v1/file/{fileId}/details", _responseSerializerOptions); + Assert.NotNull(downloadedFileDetails); + Assert.Contains(downloadedFileDetails.RecipientFileStatusHistory, recipient => recipient.RecipientFileStatusCode == RecipientFileStatusExt.DownloadStarted); + var downloadStartedEvents = downloadedFileDetails.RecipientFileStatusHistory.Where(recipientFileStatus => recipientFileStatus.RecipientFileStatusCode == RecipientFileStatusExt.DownloadStarted); + Assert.NotNull(downloadStartedEvents); + Assert.Equal(2, downloadStartedEvents.Count()); + var lastEvent = downloadStartedEvents.OrderBy(recipientFileStatus => recipientFileStatus.RecipientFileStatusChanged).Last(); + Assert.Equal(lastEvent.RecipientFileStatusChanged, downloadedFileDetails.Recipients.FirstOrDefault(recipient => recipient.Recipient == lastEvent.Recipient)?.CurrentRecipientFileStatusChanged); + } } diff --git a/src/Altinn.Broker.Application/Altinn.Broker.Application.csproj b/src/Altinn.Broker.Application/Altinn.Broker.Application.csproj new file mode 100644 index 00000000..7735c2a2 --- /dev/null +++ b/src/Altinn.Broker.Application/Altinn.Broker.Application.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandHandler.cs b/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandHandler.cs new file mode 100644 index 00000000..ad50c5e6 --- /dev/null +++ b/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandHandler.cs @@ -0,0 +1,55 @@ +using Altinn.Broker.Application; +using Altinn.Broker.Application.ConfirmDownloadCommand; +using Altinn.Broker.Core.Application; +using Altinn.Broker.Core.Domain.Enums; +using Altinn.Broker.Core.Repositories; + +using Microsoft.Extensions.Logging; + +using OneOf; + +public class ConfirmDownloadCommandHandler : IHandler +{ + private readonly IServiceOwnerRepository _serviceOwnerRepository; + private readonly IFileRepository _fileRepository; + private readonly ILogger _logger; + + public ConfirmDownloadCommandHandler(IServiceOwnerRepository serviceOwnerRepository, IFileRepository fileRepository, ILogger logger) + { + _serviceOwnerRepository = serviceOwnerRepository; + _fileRepository = fileRepository; + _logger = logger; + } + public async Task> Process(ConfirmDownloadCommandRequest request) + { + var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(request.Supplier); + if (serviceOwner is null) + { + return Errors.ServiceOwnerNotConfigured; + }; + var file = await _fileRepository.GetFile(request.FileId); + if (file is null) + { + return Errors.FileNotFound; + } + if (!file.ActorEvents.Any(actorEvent => actorEvent.Actor.ActorExternalId == request.Consumer)) + { + return Errors.FileNotFound; + } + if (string.IsNullOrWhiteSpace(file?.FileLocation)) + { + return Errors.NoFileUploaded; + } + + await _fileRepository.AddReceipt(request.FileId, ActorFileStatus.DownloadConfirmed, request.Consumer); + var recipientStatuses = file.ActorEvents + .Where(actorEvent => actorEvent.Actor.ActorExternalId != file.Sender && actorEvent.Actor.ActorExternalId != request.Consumer) + .GroupBy(actorEvent => actorEvent.Actor.ActorExternalId) + .Select(group => group.Max(statusEvent => statusEvent.Status)) + .ToList(); + bool shouldConfirmAll = recipientStatuses.All(status => status >= ActorFileStatus.DownloadConfirmed); + await _fileRepository.InsertFileStatus(request.FileId, FileStatus.AllConfirmedDownloaded); + + return new ConfirmDownloadCommandResponse(); + } +} diff --git a/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandRequest.cs b/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandRequest.cs new file mode 100644 index 00000000..376b9c47 --- /dev/null +++ b/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandRequest.cs @@ -0,0 +1,8 @@ + +namespace Altinn.Broker.Application.ConfirmDownloadCommand; +public class ConfirmDownloadCommandRequest +{ + public Guid FileId { get; set; } + public string Consumer { get; set; } + public string Supplier { get; set; } +} diff --git a/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandResponse.cs b/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandResponse.cs new file mode 100644 index 00000000..19c1c4f8 --- /dev/null +++ b/src/Altinn.Broker.Application/ConfirmDownloadCommand/ConfirmDownloadCommandResponse.cs @@ -0,0 +1,6 @@ +namespace Altinn.Broker.Application.ConfirmDownloadCommand; + +public class ConfirmDownloadCommandResponse +{ + +} diff --git a/src/Altinn.Broker.Application/DependencyInjection.cs b/src/Altinn.Broker.Application/DependencyInjection.cs new file mode 100644 index 00000000..b12c5393 --- /dev/null +++ b/src/Altinn.Broker.Application/DependencyInjection.cs @@ -0,0 +1,21 @@ +using Altinn.Broker.Application.DownloadFileQuery; +using Altinn.Broker.Application.GetFileDetailsQuery; +using Altinn.Broker.Application.GetFileOverviewQuery; +using Altinn.Broker.Application.InitializeFileCommand; +using Altinn.Broker.Application.UploadFileCommand; + +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.Broker.Application; +public static class DependencyInjection +{ + public static void AddApplicationHandlers(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryHandler.cs b/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryHandler.cs new file mode 100644 index 00000000..2e00d6d4 --- /dev/null +++ b/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryHandler.cs @@ -0,0 +1,53 @@ +using Altinn.Broker.Core.Application; +using Altinn.Broker.Core.Domain.Enums; +using Altinn.Broker.Core.Repositories; + +using Microsoft.Extensions.Logging; + +using OneOf; + +namespace Altinn.Broker.Application.DownloadFileQuery; +public class DownloadFileQueryHandler : IHandler +{ + private readonly IServiceOwnerRepository _serviceOwnerRepository; + private readonly IFileRepository _fileRepository; + private readonly IBrokerStorageService _brokerStorageService; + private readonly ILogger _logger; + + public DownloadFileQueryHandler(IServiceOwnerRepository serviceOwnerRepository, IFileRepository fileRepository, IBrokerStorageService brokerStorageService, ILogger logger) + { + _serviceOwnerRepository = serviceOwnerRepository; + _fileRepository = fileRepository; + _brokerStorageService = brokerStorageService; + _logger = logger; + } + + public async Task> Process(DownloadFileQueryRequest request) + { + var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(request.Supplier); + if (serviceOwner is null) + { + return Errors.ServiceOwnerNotConfigured; + }; + var file = await _fileRepository.GetFile(request.FileId); + if (file is null) + { + return Errors.FileNotFound; + } + if (!file.ActorEvents.Any(actorEvent => actorEvent.Actor.ActorExternalId == request.Consumer)) + { + return Errors.FileNotFound; + } + if (string.IsNullOrWhiteSpace(file?.FileLocation)) + { + return Errors.NoFileUploaded; + } + var downloadStream = await _brokerStorageService.DownloadFile(serviceOwner, file); + await _fileRepository.AddReceipt(request.FileId, ActorFileStatus.DownloadStarted, request.Consumer); + return new DownloadFileQueryResponse() + { + Filename = file.Filename, + Stream = downloadStream + }; + } +} diff --git a/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryRequest.cs b/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryRequest.cs new file mode 100644 index 00000000..53d590ac --- /dev/null +++ b/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryRequest.cs @@ -0,0 +1,8 @@ + +namespace Altinn.Broker.Application.DownloadFileQuery; +public class DownloadFileQueryRequest +{ + public Guid FileId { get; set; } + public string Supplier { get; set; } + public string Consumer { get; set; } +} diff --git a/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryResponse.cs b/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryResponse.cs new file mode 100644 index 00000000..5cda7cec --- /dev/null +++ b/src/Altinn.Broker.Application/DownloadFileQuery/DownloadFileQueryResponse.cs @@ -0,0 +1,6 @@ +namespace Altinn.Broker.Application.DownloadFileQuery; +public class DownloadFileQueryResponse +{ + public string Filename { get; set; } + public Stream Stream { get; set; } +} diff --git a/src/Altinn.Broker.Application/Errors.cs b/src/Altinn.Broker.Application/Errors.cs new file mode 100644 index 00000000..c1793acd --- /dev/null +++ b/src/Altinn.Broker.Application/Errors.cs @@ -0,0 +1,14 @@ +using System.Net; + +namespace Altinn.Broker.Application; + +public record Error(int ErrorCode, string Message, HttpStatusCode StatusCode); + +internal static class Errors +{ + public static Error FileNotFound = new Error(1, "The requested file was not found", HttpStatusCode.NotFound); + public static Error WrongTokenForSender = new Error(2, "You must use a bearer token that belongs to the sender", HttpStatusCode.Unauthorized); + public static Error ServiceOwnerNotConfigured = new Error(3, "Service owner needs to be configured to use the broker API", HttpStatusCode.BadRequest); + public static Error ServiceOwnerNotReadyInfrastructure = new Error(4, "Service owner infrastructure is not ready.", HttpStatusCode.UnprocessableEntity); + public static Error NoFileUploaded = new Error(5, "No file uploaded yet", HttpStatusCode.BadRequest); +} diff --git a/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryHandler.cs b/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryHandler.cs new file mode 100644 index 00000000..eba5c3f0 --- /dev/null +++ b/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryHandler.cs @@ -0,0 +1,44 @@ +using Altinn.Broker.Core.Application; +using Altinn.Broker.Core.Repositories; + +using OneOf; + +namespace Altinn.Broker.Application.GetFileDetailsQuery; + +public class GetFileDetailsQueryHandler : IHandler +{ + private readonly IFileRepository _fileRepository; + private readonly IServiceOwnerRepository _serviceOwnerRepository; + + public GetFileDetailsQueryHandler(IFileRepository fileRepository, IServiceOwnerRepository serviceOwnerRepository) + { + _fileRepository = fileRepository; + _serviceOwnerRepository = serviceOwnerRepository; + } + + public async Task> Process(GetFileDetailsQueryRequest request) + { + var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(request.Supplier); + if (serviceOwner is null) + { + return Errors.ServiceOwnerNotConfigured; + }; + var file = await _fileRepository.GetFile(request.FileId); + if (file is null) + { + return Errors.FileNotFound; + } + if (!file.ActorEvents.Any(actorEvent => actorEvent.Actor.ActorExternalId == request.Consumer)) + { + return Errors.FileNotFound; + } + var fileEvents = await _fileRepository.GetFileStatusHistory(request.FileId); + var actorEvents = await _fileRepository.GetActorEvents(request.FileId); + return new GetFileDetailsQueryResponse() + { + File = file, + FileEvents = fileEvents, + ActorEvents = actorEvents + }; + } +} diff --git a/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryRequest.cs b/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryRequest.cs new file mode 100644 index 00000000..aece3d89 --- /dev/null +++ b/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryRequest.cs @@ -0,0 +1,9 @@ + +namespace Altinn.Broker.Application.GetFileDetailsQuery; + +public class GetFileDetailsQueryRequest +{ + public Guid FileId { get; set; } + public string Supplier { get; set; } + public string Consumer { get; set; } +} diff --git a/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryResponse.cs b/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryResponse.cs new file mode 100644 index 00000000..c39f78ed --- /dev/null +++ b/src/Altinn.Broker.Application/GetFileDetailsQuery/GetFileDetailsQueryResponse.cs @@ -0,0 +1,10 @@ +using Altinn.Broker.Core.Domain; + +namespace Altinn.Broker.Application.GetFileDetailsQuery; + +public class GetFileDetailsQueryResponse +{ + public List ActorEvents { get; set; } + public List FileEvents { get; set; } + public FileEntity File { get; internal set; } +} diff --git a/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryHandler.cs b/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryHandler.cs new file mode 100644 index 00000000..22a46302 --- /dev/null +++ b/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryHandler.cs @@ -0,0 +1,45 @@ +using Altinn.Broker.Core.Application; +using Altinn.Broker.Core.Repositories; +using Altinn.Broker.Core.Services; + +using Microsoft.Extensions.Logging; + +using OneOf; + +namespace Altinn.Broker.Application.GetFileOverviewQuery; + +public class GetFileOverviewQueryHandler : IHandler +{ + private readonly IServiceOwnerRepository _serviceOwnerRepository; + private readonly IFileRepository _fileRepository; + private readonly ILogger _logger; + + public GetFileOverviewQueryHandler(IServiceOwnerRepository serviceOwnerRepository, IFileRepository fileRepository, IResourceManager resourceManager, ILogger logger) + { + _serviceOwnerRepository = serviceOwnerRepository; + _fileRepository = fileRepository; + _logger = logger; + } + + public async Task> Process(GetFileOverviewQueryRequest request) + { + var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(request.Supplier); + if (serviceOwner is null) + { + return Errors.ServiceOwnerNotConfigured; + }; + var file = await _fileRepository.GetFile(request.FileId); + if (file is null) + { + return Errors.FileNotFound; + } + if (!file.ActorEvents.Any(actorEvent => actorEvent.Actor.ActorExternalId == request.Consumer)) + { + return Errors.FileNotFound; + } + return new GetFileOverviewQueryResponse() + { + File = file + }; + } +} diff --git a/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryRequest.cs b/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryRequest.cs new file mode 100644 index 00000000..e80a2f26 --- /dev/null +++ b/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryRequest.cs @@ -0,0 +1,9 @@ +namespace Altinn.Broker.Application.GetFileOverviewQuery; + +public class GetFileOverviewQueryRequest +{ + public Guid FileId { get; set; } + public string Supplier { get; set; } + public string Consumer { get; set; } + +} diff --git a/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryResponse.cs b/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryResponse.cs new file mode 100644 index 00000000..cd63fd85 --- /dev/null +++ b/src/Altinn.Broker.Application/GetFileOverviewQuery/GetFileOverviewQueryResponse.cs @@ -0,0 +1,8 @@ +using Altinn.Broker.Core.Domain; + +namespace Altinn.Broker.Application.GetFileOverviewQuery; + +public class GetFileOverviewQueryResponse +{ + public FileEntity File { get; set; } +} diff --git a/src/Altinn.Broker.Application/GetFilesQuery/GetFilesQueryHandler.cs b/src/Altinn.Broker.Application/GetFilesQuery/GetFilesQueryHandler.cs new file mode 100644 index 00000000..0a18b4f1 --- /dev/null +++ b/src/Altinn.Broker.Application/GetFilesQuery/GetFilesQueryHandler.cs @@ -0,0 +1,40 @@ +using Altinn.Broker.Core.Application; +using Altinn.Broker.Core.Repositories; + +using Microsoft.Extensions.Logging; + +using OneOf; + +namespace Altinn.Broker.Application.GetFilesQuery; + +public class GetFilesQueryHandler : IHandler> +{ + private readonly IServiceOwnerRepository _serviceOwnerRepository; + private readonly IFileRepository _fileRepository; + private readonly IActorRepository _actorRepository; + private readonly ILogger _logger; + + public GetFilesQueryHandler(IServiceOwnerRepository serviceOwnerRepository, IFileRepository fileRepository, IActorRepository actorRepository, ILogger logger) + { + _serviceOwnerRepository = serviceOwnerRepository; + _fileRepository = fileRepository; + _actorRepository = actorRepository; + _logger = logger; + } + + public async Task, Error>> Process(GetFilesQueryRequest request) + { + var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(request.Supplier); + if (serviceOwner is null) + { + return Errors.ServiceOwnerNotConfigured; + }; + var callingActor = await _actorRepository.GetActorAsync(request.Consumer); + if (callingActor is null) + { + return new List(); + } + var files = await _fileRepository.GetFilesAssociatedWithActor(callingActor); + return files; + } +} diff --git a/src/Altinn.Broker.Application/GetFilesQuery/GetFilesQueryRequest.cs b/src/Altinn.Broker.Application/GetFilesQuery/GetFilesQueryRequest.cs new file mode 100644 index 00000000..6271ede4 --- /dev/null +++ b/src/Altinn.Broker.Application/GetFilesQuery/GetFilesQueryRequest.cs @@ -0,0 +1,12 @@ +using Altinn.Broker.Core.Domain.Enums; + +namespace Altinn.Broker.Application.GetFilesQuery; + +public class GetFilesQueryRequest +{ + public string Consumer { get; set; } + public string Supplier { get; set; } + public FileStatus? Status { get; set; } + public DateTimeOffset? From { get; set; } + public DateTimeOffset? To { get; set; } +} diff --git a/src/Altinn.Broker.Application/IHandler.cs b/src/Altinn.Broker.Application/IHandler.cs new file mode 100644 index 00000000..8f65caa0 --- /dev/null +++ b/src/Altinn.Broker.Application/IHandler.cs @@ -0,0 +1,11 @@ +using Altinn.Broker.Application; + +using OneOf; + +namespace Altinn.Broker.Core.Application; +internal interface IHandler +{ + Task> Process(TRequest request); +} + +// Return OneOf instead in order to handle errors without exception0 diff --git a/src/Altinn.Broker.Application/InitializeFileCommand/InitializeFileCommandHandler.cs b/src/Altinn.Broker.Application/InitializeFileCommand/InitializeFileCommandHandler.cs new file mode 100644 index 00000000..961a9302 --- /dev/null +++ b/src/Altinn.Broker.Application/InitializeFileCommand/InitializeFileCommandHandler.cs @@ -0,0 +1,44 @@ +using Altinn.Broker.Core.Application; +using Altinn.Broker.Core.Repositories; +using Altinn.Broker.Core.Services; + +using Microsoft.Extensions.Logging; + +using OneOf; + +namespace Altinn.Broker.Application.InitializeFileCommand; +public class InitializeFileCommandHandler : IHandler +{ + private readonly IServiceOwnerRepository _serviceOwnerRepository; + private readonly IFileRepository _fileRepository; + private readonly IResourceManager _resourceManager; + private readonly ILogger _logger; + + public InitializeFileCommandHandler(IServiceOwnerRepository serviceOwnerRepository, IFileRepository fileRepository, IResourceManager resourceManager, ILogger logger) + { + _serviceOwnerRepository = serviceOwnerRepository; + _fileRepository = fileRepository; + _resourceManager = resourceManager; + _logger = logger; + } + + public async Task> Process(InitializeFileCommandRequest request) + { + if (request.Consumer != request.SenderExternalId) + { + return Errors.WrongTokenForSender; + } + var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(request.Supplier); + if (serviceOwner is null) + { + return Errors.ServiceOwnerNotConfigured; + } + var deploymentStatus = await _resourceManager.GetDeploymentStatus(serviceOwner); + if (deploymentStatus != DeploymentStatus.Ready) + { + return Errors.ServiceOwnerNotReadyInfrastructure; + } + var fileId = await _fileRepository.AddFile(serviceOwner, request.Filename, request.SendersFileReference, request.SenderExternalId, request.RecipientExternalIds, request.PropertyList, request.Checksum); + return fileId; + } +} diff --git a/src/Altinn.Broker.Application/InitializeFileCommand/InitializeFileCommandRequest.cs b/src/Altinn.Broker.Application/InitializeFileCommand/InitializeFileCommandRequest.cs new file mode 100644 index 00000000..be4f07a8 --- /dev/null +++ b/src/Altinn.Broker.Application/InitializeFileCommand/InitializeFileCommandRequest.cs @@ -0,0 +1,13 @@ + +namespace Altinn.Broker.Application.InitializeFileCommand; +public class InitializeFileCommandRequest +{ + public string Consumer { get; set; } + public string Supplier { get; set; } + public string Filename { get; set; } + public string SendersFileReference { get; set; } + public string SenderExternalId { get; set; } + public List RecipientExternalIds { get; set; } + public Dictionary PropertyList { get; set; } + public string? Checksum { get; set; } +} diff --git a/src/Altinn.Broker.Application/UploadFileCommand/UploadFileCommandHandler.cs b/src/Altinn.Broker.Application/UploadFileCommand/UploadFileCommandHandler.cs new file mode 100644 index 00000000..0337d178 --- /dev/null +++ b/src/Altinn.Broker.Application/UploadFileCommand/UploadFileCommandHandler.cs @@ -0,0 +1,59 @@ +using Altinn.Broker.Core.Application; +using Altinn.Broker.Core.Domain.Enums; +using Altinn.Broker.Core.Repositories; +using Altinn.Broker.Core.Services; + +using Microsoft.Extensions.Logging; + +using OneOf; + +namespace Altinn.Broker.Application.UploadFileCommand; + +public class UploadFileCommandHandler : IHandler +{ + private readonly IServiceOwnerRepository _serviceOwnerRepository; + private readonly IFileRepository _fileRepository; + private readonly IResourceManager _resourceManager; + private readonly IBrokerStorageService _brokerStorageService; + private readonly ILogger _logger; + + public UploadFileCommandHandler(IServiceOwnerRepository serviceOwnerRepository, IFileRepository fileRepository, IResourceManager resourceMananger, IBrokerStorageService brokerStorageService, ILogger logger) + { + _serviceOwnerRepository = serviceOwnerRepository; + _fileRepository = fileRepository; + _resourceManager = resourceMananger; + _brokerStorageService = brokerStorageService; + _logger = logger; + } + + public async Task> Process(UploadFileCommandRequest request) + { + var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(request.Supplier); + if (serviceOwner?.StorageProvider is null) + { + return Errors.ServiceOwnerNotConfigured; + }; + var deploymentStatus = await _resourceManager.GetDeploymentStatus(serviceOwner); + if (deploymentStatus != DeploymentStatus.Ready) + { + return Errors.ServiceOwnerNotReadyInfrastructure; + } + var file = await _fileRepository.GetFile(request.FileId); + if (file is null) + { + return Errors.FileNotFound; + } + if (request.Consumer != file.Sender) + { + return Errors.FileNotFound; + } + + await _fileRepository.InsertFileStatus(request.FileId, FileStatus.UploadStarted); + await _brokerStorageService.UploadFile(serviceOwner, file, request.Filestream); + await _fileRepository.SetStorageReference(request.FileId, serviceOwner.StorageProvider.Id, request.FileId.ToString()); + await _fileRepository.InsertFileStatus(request.FileId, FileStatus.UploadProcessing); + // TODO, async jobs + await _fileRepository.InsertFileStatus(request.FileId, FileStatus.Published); + return file.FileId; + } +} diff --git a/src/Altinn.Broker.Application/UploadFileCommand/UploadFileCommandRequest.cs b/src/Altinn.Broker.Application/UploadFileCommand/UploadFileCommandRequest.cs new file mode 100644 index 00000000..b36e6aa5 --- /dev/null +++ b/src/Altinn.Broker.Application/UploadFileCommand/UploadFileCommandRequest.cs @@ -0,0 +1,10 @@ + +namespace Altinn.Broker.Application.UploadFileCommand; + +public class UploadFileCommandRequest +{ + public Guid FileId { get; set; } + public string Supplier { get; set; } + public Stream Filestream { get; set; } + public string Consumer { get; set; } +} diff --git a/src/Altinn.Broker.Core/Repositories/IFileRepository.cs b/src/Altinn.Broker.Core/Repositories/IFileRepository.cs index 03513749..5e63570c 100644 --- a/src/Altinn.Broker.Core/Repositories/IFileRepository.cs +++ b/src/Altinn.Broker.Core/Repositories/IFileRepository.cs @@ -5,23 +5,27 @@ namespace Altinn.Broker.Core.Repositories; public interface IFileRepository { - Task AddFileAsync(FileEntity file, ServiceOwnerEntity serviceOwner); - Task AddReceiptAsync(ActorFileStatusEntity receipt); - Task GetFileAsync(Guid fileId); - Task> GetFilesAvailableForCaller(string actorExernalReference); - + Task AddFile( + ServiceOwnerEntity serviceOwner, + string filename, + string sendersFileReference, + string senderExternalId, + List recipientIds, + Dictionary propertyList, + string? checksum); + Task GetFile(Guid fileId); + Task> GetFilesAssociatedWithActor(ActorEntity actor); Task AddReceipt( Guid fileId, Domain.Enums.ActorFileStatus status, string actorExternalReference ); - Task SetStorageReference( Guid fileId, long storageProviderId, string fileLocation ); - Task> GetFileStatusHistoryAsync(Guid fileId); + Task> GetFileStatusHistory(Guid fileId); Task> GetActorEvents(Guid fileId); Task InsertFileStatus(Guid fileId, FileStatus status); } diff --git a/src/Altinn.Broker.Integrations/Maskinporten/MaskinportenOptions.cs b/src/Altinn.Broker.Integrations/Maskinporten/MaskinportenOptions.cs deleted file mode 100644 index 66d0fa04..00000000 --- a/src/Altinn.Broker.Integrations/Maskinporten/MaskinportenOptions.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Altinn.Broker.Integrations.Maskinporten; -internal class MaskinportenOptions -{ -} diff --git a/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenConsumer.cs b/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenConsumer.cs deleted file mode 100644 index 810c48e7..00000000 --- a/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenConsumer.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Altinn.Broker.Integrations.Maskinporten.Models; - -public class MaskinportenConsumer -{ - [JsonConstructor] - public MaskinportenConsumer( - string authority, - string id - ) - { - Authority = authority; - ID = id; - } - - [JsonPropertyName("authority")] - public string Authority { get; } - - [JsonPropertyName("ID")] - public string ID { get; } -} diff --git a/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenSupplier.cs b/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenSupplier.cs deleted file mode 100644 index cdf6fa51..00000000 --- a/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenSupplier.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Altinn.Broker.Integrations.Maskinporten.Models; - -public class MaskinportenSupplier -{ - [JsonConstructor] - public MaskinportenSupplier( - string authority, - string id - ) - { - Authority = authority; - ID = id; - } - - [JsonPropertyName("authority")] - public string Authority { get; } - - [JsonPropertyName("ID")] - public string ID { get; } -} - diff --git a/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenToken.cs b/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenToken.cs deleted file mode 100644 index 018ab1b5..00000000 --- a/src/Altinn.Broker.Integrations/Maskinporten/Models/MaskinportenToken.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Altinn.Broker.Integrations.Maskinporten.Models; - -public class MaskinportenToken -{ - public MaskinportenToken( - string scope, - string consumer, - string supplier - ) - { - Scope = scope; - Consumer = consumer; - Supplier = supplier; - } - - public string Scope { get; } - - public string Consumer { get; } - - public string Supplier { get; } -} diff --git a/src/Altinn.Broker.Persistence/Repositories/FileRepository.cs b/src/Altinn.Broker.Persistence/Repositories/FileRepository.cs index 3011dc77..8fd5f3b7 100644 --- a/src/Altinn.Broker.Persistence/Repositories/FileRepository.cs +++ b/src/Altinn.Broker.Persistence/Repositories/FileRepository.cs @@ -21,7 +21,7 @@ public FileRepository(DatabaseConnectionProvider connectionProvider, IActorRepos _actorRepository = actorRepository; } - public async Task GetFileAsync(Guid fileId) + public async Task GetFile(Guid fileId) { var connection = await _connectionProvider.GetConnectionAsync(); @@ -95,37 +95,19 @@ public FileRepository(DatabaseConnectionProvider connectionProvider, IActorRepos return file; } - public async Task AddReceiptAsync(ActorFileStatusEntity receipt) + public async Task AddFile(ServiceOwnerEntity serviceOwner, string filename, string sendersFileReference, string senderExternalId, List recipientIds, Dictionary propertyList, string? checksum) { - var connection = await _connectionProvider.GetConnectionAsync(); - - var actorId = receipt.Actor.ActorId; - if (actorId == 0) - { - actorId = await _actorRepository.AddActorAsync(receipt.Actor); - } - - using (var command = new NpgsqlCommand( - "INSERT INTO broker.actor_file_status (actor_id_fk, file_id_fk, actor_file_status_id_fk, actor_file_status_date) " + - "VALUES (@actorId, @fileId, @actorFileStatusId, NOW())", connection)) + if (serviceOwner.StorageProvider is null) { - command.Parameters.AddWithValue("@actorId", actorId); - command.Parameters.AddWithValue("@fileId", receipt.FileId); - command.Parameters.AddWithValue("@actorFileStatusId", (int)receipt.Status); - var commandText = command.CommandText; - command.ExecuteNonQuery(); + throw new ArgumentNullException("Storage provider must be set"); } - } - - public async Task AddFileAsync(FileEntity file, ServiceOwnerEntity serviceOwner) - { long actorId; - var actor = await _actorRepository.GetActorAsync(file.Sender); + var actor = await _actorRepository.GetActorAsync(senderExternalId); if (actor is null) { actorId = await _actorRepository.AddActorAsync(new ActorEntity() { - ActorExternalId = file.Sender + ActorExternalId = senderExternalId }); } else @@ -134,53 +116,49 @@ public async Task AddFileAsync(FileEntity file, ServiceOwnerEntity service } var connection = await _connectionProvider.GetConnectionAsync(); - var fileId = Guid.NewGuid(); NpgsqlCommand command = new NpgsqlCommand( "INSERT INTO broker.file (file_id_pk, service_owner_id_fk, filename, checksum, external_file_reference, sender_actor_id_fk, created, storage_provider_id_fk) " + "VALUES (@fileId, @serviceOwnerId, @filename, @checksum, @externalFileReference, @senderActorId, @created, @storageProviderId)", connection); + var fileId = Guid.NewGuid(); command.Parameters.AddWithValue("@fileId", fileId); command.Parameters.AddWithValue("@serviceOwnerId", serviceOwner.Id); - command.Parameters.AddWithValue("@filename", file.Filename); - command.Parameters.AddWithValue("@checksum", file.Checksum is null ? DBNull.Value : file.Checksum); + command.Parameters.AddWithValue("@filename", filename); + command.Parameters.AddWithValue("@checksum", checksum is null ? DBNull.Value : checksum); command.Parameters.AddWithValue("@senderActorId", actorId); - command.Parameters.AddWithValue("@externalFileReference", file.SendersFileReference); - command.Parameters.AddWithValue("@fileStatusId", (int)file.FileStatus); + command.Parameters.AddWithValue("@externalFileReference", sendersFileReference); + command.Parameters.AddWithValue("@fileStatusId", (int)FileStatus.Initialized); // TODO, remove? command.Parameters.AddWithValue("@created", DateTime.UtcNow); - command.Parameters.AddWithValue("@storageProviderId", serviceOwner.StorageProvider!.Id); + command.Parameters.AddWithValue("@storageProviderId", serviceOwner.StorageProvider.Id); command.ExecuteNonQuery(); - var addActorTasks = file.ActorEvents.Select(actorEvent => AddReceipt(fileId, ActorFileStatus.Initialized, actorEvent.Actor.ActorExternalId)); + var addRecipientEventTasks = recipientIds.Concat([senderExternalId]).Select(recipientId => AddReceipt(fileId, ActorFileStatus.Initialized, recipientId)); try { - await Task.WhenAll(addActorTasks); + await Task.WhenAll(addRecipientEventTasks); } catch (Exception ex) { Console.WriteLine($"An error occurred: {ex.Message}"); } - await SetMetadata(fileId, file.PropertyList); + await SetMetadata(fileId, propertyList); await InsertFileStatus(fileId, FileStatus.Initialized); return fileId; } - public async Task> GetFilesAvailableForCaller(string actorExernalReference) + public async Task> GetFilesAssociatedWithActor(ActorEntity actor) { var connection = await _connectionProvider.GetConnectionAsync(); using (var command = new NpgsqlCommand( "SELECT DISTINCT afs.file_id_fk, 'Recipient' " + "FROM broker.actor_file_status afs " + - "WHERE afs.actor_id_fk = ( " + - " SELECT a.actor_id_pk " + - " FROM broker.actor a " + - " WHERE a.actor_external_id = @actorExternalId" + - ")" + + "WHERE afs.actor_id_fk = @actorId " + "UNION " + "SELECT f.file_id_pk, 'Sender' " + "FROM broker.file f " + @@ -191,7 +169,8 @@ public async Task> GetFilesAvailableForCaller(string actorExernalRefe "FROM broker.file f " + "WHERE f.service_owner_id_fk = @actorExternalId", connection)) { - command.Parameters.AddWithValue("@actorExternalid", actorExernalReference); + command.Parameters.AddWithValue("@actorId", actor.ActorId); + command.Parameters.AddWithValue("@actorExternalId", actor.ActorExternalId); var files = new List(); using (var reader = await command.ExecuteReaderAsync()) @@ -206,7 +185,7 @@ public async Task> GetFilesAvailableForCaller(string actorExernalRefe } } - public async Task AddReceipt(Guid fileId, Core.Domain.Enums.ActorFileStatus status, string actorExternalReference) + public async Task AddReceipt(Guid fileId, ActorFileStatus status, string actorExternalReference) { var connection = await _connectionProvider.GetConnectionAsync(); @@ -254,7 +233,7 @@ public async Task SetStorageReference(Guid fileId, long storageProviderId, strin } } - public async Task> GetFileStatusHistoryAsync(Guid fileId) + public async Task> GetFileStatusHistory(Guid fileId) { var connection = await _connectionProvider.GetConnectionAsync(); diff --git a/src/Altinn.Broker/Altinn.Broker.csproj b/src/Altinn.Broker/Altinn.Broker.csproj index c0af0e32..d189eb51 100644 --- a/src/Altinn.Broker/Altinn.Broker.csproj +++ b/src/Altinn.Broker/Altinn.Broker.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Altinn.Broker/Controllers/FileController.cs b/src/Altinn.Broker/Controllers/FileController.cs index 4f98a853..93ab96b8 100644 --- a/src/Altinn.Broker/Controllers/FileController.cs +++ b/src/Altinn.Broker/Controllers/FileController.cs @@ -1,7 +1,13 @@ +using Altinn.Broker.Application.ConfirmDownloadCommand; +using Altinn.Broker.Application.DownloadFileQuery; +using Altinn.Broker.Application.GetFileDetailsQuery; +using Altinn.Broker.Application.GetFileOverviewQuery; +using Altinn.Broker.Application.GetFilesQuery; +using Altinn.Broker.Application.InitializeFileCommand; +using Altinn.Broker.Application.UploadFileCommand; using Altinn.Broker.Core.Domain.Enums; using Altinn.Broker.Core.Models; -using Altinn.Broker.Core.Repositories; -using Altinn.Broker.Core.Services; +using Altinn.Broker.Enums; using Altinn.Broker.Mappers; using Altinn.Broker.Middlewares; using Altinn.Broker.Models; @@ -18,18 +24,10 @@ namespace Altinn.Broker.Controllers [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public class FileController : Controller { - private readonly IFileRepository _fileRepository; - private readonly IServiceOwnerRepository _serviceOwnerRepository; - private readonly IBrokerStorageService _brokerStorageService; - private readonly IResourceManager _resourceManager; private readonly ILogger _logger; - public FileController(IFileRepository fileRepository, IServiceOwnerRepository serviceOwnerRepository, IBrokerStorageService brokerStorageService, IResourceManager resourceManager, ILogger logger) + public FileController(ILogger logger) { - _fileRepository = fileRepository; - _serviceOwnerRepository = serviceOwnerRepository; - _brokerStorageService = brokerStorageService; - _resourceManager = resourceManager; _logger = logger; } @@ -39,18 +37,14 @@ public FileController(IFileRepository fileRepository, IServiceOwnerRepository se /// [HttpPost] [Authorize(Policy = "Sender")] - public async Task> InitializeFile(FileInitalizeExt initializeExt, [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token) + public async Task> InitializeFile(FileInitalizeExt initializeExt, [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token, [FromServices] InitializeFileCommandHandler handler) { - if (token.Consumer != initializeExt.Sender) - { - return Unauthorized("You must use a bearer token that belongs to the sender"); - } - var file = FileInitializeExtMapper.MapToDomain(initializeExt, token.Supplier); - var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(token.Supplier); - - var fileId = await _fileRepository.AddFileAsync(file, serviceOwner); - - return Ok(fileId.ToString()); + var commandRequest = InitializeFileMapper.MapToRequest(initializeExt, token); + var commandResult = await handler.Process(commandRequest); + return commandResult.Match( + fileId => Ok(fileId.ToString()), + error => error.ToActionResult() + ); } /// @@ -62,37 +56,23 @@ public async Task> InitializeFile(FileInitalizeExt initialize [Consumes("application/octet-stream")] [Authorize(Policy = "Sender")] public async Task UploadFileStreamed( - Guid fileId, [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token) + Guid fileId, + [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token, + [FromServices] UploadFileCommandHandler handler + ) { - var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(token.Supplier); - if (serviceOwner is null) - { - return Unauthorized("Service owner not configured for the broker service"); - }; - var deploymentStatus = await _resourceManager.GetDeploymentStatus(serviceOwner); - if (deploymentStatus != DeploymentStatus.Ready) - { - return UnprocessableEntity($"Service owner infrastructure is not ready. Status is: ${nameof(deploymentStatus)}"); - } - var file = await _fileRepository.GetFileAsync(fileId); - if (file is null) - { - return BadRequest(); - } - if (token.Consumer != file.Sender) - { - return Unauthorized("You must use a bearer token that belongs to the sender"); - } - Request.EnableBuffering(); - await _fileRepository.InsertFileStatus(fileId, FileStatus.UploadStarted); - await _brokerStorageService.UploadFile(serviceOwner, file, Request.Body); - await _fileRepository.SetStorageReference(fileId, serviceOwner.StorageProvider.Id, fileId.ToString()); - // TODO, async jobs - await _fileRepository.InsertFileStatus(fileId, FileStatus.UploadProcessing); - await _fileRepository.InsertFileStatus(fileId, FileStatus.Published); - - return Ok(fileId.ToString()); + var commandResult = await handler.Process(new UploadFileCommandRequest() + { + FileId = fileId, + Consumer = token.Consumer, + Supplier = token.Supplier, + Filestream = Request.Body + }); + return commandResult.Match( + fileId => Ok(fileId), + error => error.ToActionResult() + ); } /// @@ -105,33 +85,31 @@ public async Task UploadFileStreamed( [Authorize(Policy = "Sender")] public async Task InitializeAndUpload( [FromForm] FileInitializeAndUploadExt form, - [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token + [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token, + [FromServices] InitializeFileCommandHandler initializeFileCommandHandler, + [FromServices] UploadFileCommandHandler uploadFileCommandHandler ) { - if (token.Consumer != form.Metadata.Sender) - { - return Unauthorized("You must use a bearer token that belongs to the sender"); - } - var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(token.Supplier); - if (serviceOwner is null) - { - return Unauthorized("Service owner not configured for the broker service"); - }; - var deploymentStatus = await _resourceManager.GetDeploymentStatus(serviceOwner); - if (deploymentStatus != DeploymentStatus.Ready) + var initializeRequest = InitializeFileMapper.MapToRequest(form.Metadata, token); + var initializeResult = await initializeFileCommandHandler.Process(initializeRequest); + if (initializeResult.IsT1) { - return UnprocessableEntity($"Service owner infrastructure is not ready. Status is: ${nameof(deploymentStatus)}"); + return initializeResult.AsT1.ToActionResult(); } + var fileId = initializeResult.AsT0; - var file = FileInitializeExtMapper.MapToDomain(form.Metadata, token.Supplier); - var fileId = await _fileRepository.AddFileAsync(file, serviceOwner); - await _fileRepository.InsertFileStatus(fileId, FileStatus.UploadStarted); - await _brokerStorageService.UploadFile(serviceOwner, file, form.File.OpenReadStream()); - await _fileRepository.SetStorageReference(fileId, serviceOwner.StorageProvider.Id, fileId.ToString()); - // TODO, async jobs - await _fileRepository.InsertFileStatus(fileId, FileStatus.UploadProcessing); - await _fileRepository.InsertFileStatus(fileId, FileStatus.Published); - return Ok(fileId.ToString()); + Request.EnableBuffering(); + var uploadResult = await uploadFileCommandHandler.Process(new UploadFileCommandRequest() + { + FileId = fileId, + Supplier = token.Supplier, + Consumer = token.Consumer, + Filestream = Request.Body + }); + return uploadResult.Match( + fileId => Ok(fileId), + error => error.ToActionResult() + ); } /// @@ -141,26 +119,21 @@ public async Task InitializeAndUpload( [HttpGet] [Route("{fileId}")] [Authorize(Policy = "Sender")] - public async Task> GetFileStatus(Guid fileId, [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token) + public async Task> GetFileOverview( + Guid fileId, + [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token, + [FromServices] GetFileOverviewQueryHandler handler) { - var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(token.Supplier); - if (serviceOwner is null) + var queryResult = await handler.Process(new GetFileOverviewQueryRequest() { - return Unauthorized("Service owner not configured for the broker service"); - }; - var file = await _fileRepository.GetFileAsync(fileId); - if (file is null) - { - return NotFound(); - } - if (!file.ActorEvents.Any(actorEvent => actorEvent.Actor.ActorExternalId == token.Consumer)) - { - return NotFound(); - } - - var fileView = FileStatusOverviewExtMapper.MapToExternalModel(file); - - return Ok(fileView); + FileId = fileId, + Consumer = token.Consumer, + Supplier = token.Supplier + }); + return queryResult.Match( + result => Ok(FileStatusOverviewExtMapper.MapToExternalModel(result.File)), + error => error.ToActionResult() + ); } /// @@ -170,42 +143,21 @@ public async Task> GetFileStatus(Guid fileId, [Mod [HttpGet] [Route("{fileId}/details")] [Authorize(Policy = "Sender")] - public async Task> GetFileDetails(Guid fileId, [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token) + public async Task> GetFileDetails( + Guid fileId, + [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token, + [FromServices] GetFileDetailsQueryHandler handler) { - var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(token.Supplier); - if (serviceOwner is null) + var queryResult = await handler.Process(new GetFileDetailsQueryRequest() { - return Unauthorized("Service owner not configured for the broker service"); - }; - var file = await _fileRepository.GetFileAsync(fileId); - if (file is null) - { - return NotFound(); - } - if (!file.ActorEvents.Any(actorEvent => actorEvent.Actor.ActorExternalId == token.Consumer)) - { - return NotFound(); - } - - var fileHistory = await _fileRepository.GetFileStatusHistoryAsync(fileId); - var actorEvents = await _fileRepository.GetActorEvents(fileId); - var fileOverview = FileStatusOverviewExtMapper.MapToExternalModel(file); - - return new FileStatusDetailsExt() - { - Checksum = fileOverview.Checksum, FileId = fileId, - FileName = fileOverview.FileName, - Sender = fileOverview.Sender, - FileStatus = fileOverview.FileStatus, - FileStatusChanged = fileOverview.FileStatusChanged, - FileStatusText = fileOverview.FileStatusText, - PropertyList = fileOverview.PropertyList, - Recipients = fileOverview.Recipients, - SendersFileReference = fileOverview.SendersFileReference, - FileStatusHistory = FileStatusOverviewExtMapper.MapToFileStatusHistoryExt(fileHistory), - RecipientFileStatusHistory = FileStatusOverviewExtMapper.MapToRecipientEvents(actorEvents.Where(actorEvents => actorEvents.Actor.ActorExternalId != file.Sender).ToList()) - }; + Consumer = token.Consumer, + Supplier = token.Supplier + }); + return queryResult.Match( + result => Ok(FileStatusDetailsExtMapper.MapToExternalModel(result.File, result.FileEvents, result.ActorEvents)), + error => error.ToActionResult() + ); } /// @@ -214,17 +166,25 @@ public async Task> GetFileDetails(Guid fileId /// [HttpGet] [Authorize(Policy = "Sender")] - public async Task>> GetFiles([FromQuery] FileStatus? status, [FromQuery] DateTimeOffset? from, [FromQuery] DateTimeOffset? to, [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token) + public async Task>> GetFiles( + [FromQuery] FileStatusExt? status, + [FromQuery] DateTimeOffset? from, + [FromQuery] DateTimeOffset? to, + [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token, + [FromServices] GetFilesQueryHandler handler) { - var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(token.Supplier); - if (serviceOwner is null) - { - return Unauthorized("Service owner not configured for the broker service"); - }; - - var files = await _fileRepository.GetFilesAvailableForCaller(token.Consumer); - - return Ok(files); + var queryResult = await handler.Process(new GetFilesQueryRequest() + { + Consumer = token.Consumer, + Supplier = token.Supplier, + Status = status is not null ? (FileStatus)status : null, + From = from, + To = to + }); + return queryResult.Match( + Ok, + error => error.ToActionResult() + ); } /// @@ -234,31 +194,21 @@ public async Task>> GetFiles([FromQuery] FileStatus? sta [HttpGet] [Route("{fileId}/download")] [Authorize(Policy = "Recipient")] - public async Task> DownloadFile(Guid fileId, [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token) + public async Task> DownloadFile( + Guid fileId, + [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token, + [FromServices] DownloadFileQueryHandler handler) { - var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(token.Supplier); - if (serviceOwner is null) - { - return Unauthorized("Service owner not configured for the broker service"); - }; - var file = await _fileRepository.GetFileAsync(fileId); - if (file is null) - { - return NotFound(); - } - if (!file.ActorEvents.Any(actorEvent => actorEvent.Actor.ActorExternalId == token.Consumer)) - { - return NotFound(); - } - if (string.IsNullOrWhiteSpace(file?.FileLocation)) + var queryResult = await handler.Process(new DownloadFileQueryRequest() { - return BadRequest("No file uploaded yet"); - } - - var downloadStream = await _brokerStorageService.DownloadFile(serviceOwner, file); - await _fileRepository.AddReceipt(fileId, ActorFileStatus.DownloadStarted, token.Consumer); - - return File(downloadStream, "application/force-download", file.Filename); + FileId = fileId, + Consumer = token.Consumer, + Supplier = token.Supplier + }); + return queryResult.Match( + result => File(result.Stream, "application/octet-stream", result.Filename), + error => error.ToActionResult() + ); } /// @@ -268,37 +218,21 @@ public async Task> DownloadFile(Guid fileId, [ModelBinder(t [HttpPost] [Route("{fileId}/confirmdownload")] [Authorize(Policy = "Recipient")] - public async Task ConfirmDownload(Guid fileId, [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token) + public async Task ConfirmDownload( + Guid fileId, + [ModelBinder(typeof(MaskinportenModelBinder))] MaskinportenToken token, + [FromServices] ConfirmDownloadCommandHandler handler) { - var serviceOwner = await _serviceOwnerRepository.GetServiceOwner(token.Supplier); - if (serviceOwner is null) - { - return Unauthorized("Service owner not configured for the broker service"); - }; - var file = await _fileRepository.GetFileAsync(fileId); - if (file is null) - { - return NotFound(); - } - if (!file.ActorEvents.Any(actorEvent => actorEvent.Actor.ActorExternalId == token.Consumer)) - { - return NotFound(); - } - if (string.IsNullOrWhiteSpace(file?.FileLocation)) + var commandResult = await handler.Process(new ConfirmDownloadCommandRequest() { - return BadRequest("No file uploaded yet"); - } - - await _fileRepository.AddReceipt(fileId, ActorFileStatus.DownloadConfirmed, token.Consumer); - var recipientStatuses = file.ActorEvents - .Where(actorEvent => actorEvent.Actor.ActorExternalId != file.Sender && actorEvent.Actor.ActorExternalId != token.Consumer) - .GroupBy(actorEvent => actorEvent.Actor.ActorExternalId) - .Select(group => group.Max(statusEvent => statusEvent.Status)) - .ToList(); - bool shouldConfirmAll = recipientStatuses.All(status => status >= ActorFileStatus.DownloadConfirmed); - await _fileRepository.InsertFileStatus(fileId, FileStatus.AllConfirmedDownloaded); - - return Ok(); + FileId = fileId, + Supplier = token.Supplier, + Consumer = token.Consumer + }); + return commandResult.Match( + Ok, + error => error.ToActionResult() + ); } } } diff --git a/src/Altinn.Broker/Mappers/ErrorMapper.cs b/src/Altinn.Broker/Mappers/ErrorMapper.cs new file mode 100644 index 00000000..781c9d6b --- /dev/null +++ b/src/Altinn.Broker/Mappers/ErrorMapper.cs @@ -0,0 +1,15 @@ +using Altinn.Broker.Application; + +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.Broker.Mappers; + +public static class ErrorMapper +{ + public static ActionResult ToActionResult(this Error error) => new ContentResult() + { + StatusCode = (int)error.StatusCode, + Content = error.Message, + ContentType = "text/plain; charset=utf-8" + }; +} diff --git a/src/Altinn.Broker/Mappers/FileInitializeExtMapper.cs b/src/Altinn.Broker/Mappers/FileInitializeExtMapper.cs index ade9724d..0ba26f34 100644 --- a/src/Altinn.Broker/Mappers/FileInitializeExtMapper.cs +++ b/src/Altinn.Broker/Mappers/FileInitializeExtMapper.cs @@ -4,10 +4,10 @@ using Altinn.Broker.Core.Domain; using Altinn.Broker.Models; -public static class FileInitializeExtMapper +internal static class FileInitializeExtMapper { - public static FileEntity MapToDomain(FileInitalizeExt initializeExt, string caller) + internal static FileEntity MapToDomain(FileInitalizeExt initializeExt, string caller) { return new FileEntity() { diff --git a/src/Altinn.Broker/Mappers/FileStatusDetailsExtMapper.cs b/src/Altinn.Broker/Mappers/FileStatusDetailsExtMapper.cs new file mode 100644 index 00000000..507dcae2 --- /dev/null +++ b/src/Altinn.Broker/Mappers/FileStatusDetailsExtMapper.cs @@ -0,0 +1,47 @@ +using Altinn.Broker.Core.Domain; +using Altinn.Broker.Core.Models; +using Altinn.Broker.Models; + +namespace Altinn.Broker.Mappers; + +internal static class FileStatusDetailsExtMapper +{ + internal static FileStatusDetailsExt MapToExternalModel(FileEntity file, List fileEvents, List actorEvents) + { + var fileOverview = FileStatusOverviewExtMapper.MapToExternalModel(file); + return new FileStatusDetailsExt() + { + Checksum = fileOverview.Checksum, + FileId = file.FileId, + FileName = fileOverview.FileName, + Sender = fileOverview.Sender, + FileStatus = fileOverview.FileStatus, + FileStatusChanged = fileOverview.FileStatusChanged, + FileStatusText = fileOverview.FileStatusText, + PropertyList = fileOverview.PropertyList, + Recipients = fileOverview.Recipients, + SendersFileReference = fileOverview.SendersFileReference, + FileStatusHistory = MapToFileStatusHistoryExt(fileEvents), + RecipientFileStatusHistory = MapToRecipientEvents(actorEvents.Where(actorEvents => actorEvents.Actor.ActorExternalId != file.Sender).ToList()) + }; + + } + + public static List MapToFileStatusHistoryExt(List fileHistory) => fileHistory.Select(entity => new FileStatusEventExt() + { + FileStatus = FileStatusOverviewExtMapper.MapToExternalEnum(entity.Status), + FileStatusChanged = entity.Date, + FileStatusText = FileStatusOverviewExtMapper.MapToFileStatusText(entity.Status) + }).ToList(); + + private static List MapToRecipientEvents(List actorEvents) + { + return actorEvents.Select(actorEvent => new RecipientFileStatusEventExt() + { + Recipient = actorEvent.Actor.ActorExternalId, + RecipientFileStatusChanged = actorEvent.Date, + RecipientFileStatusCode = FileStatusOverviewExtMapper.MapToExternalRecipientStatus(actorEvent.Status), + RecipientFileStatusText = FileStatusOverviewExtMapper.MapToRecipientStatusText(actorEvent.Status) + }).ToList(); + } +} diff --git a/src/Altinn.Broker/Mappers/FileStatusOverviewExtMapper.cs b/src/Altinn.Broker/Mappers/FileStatusOverviewExtMapper.cs index ea8304dc..43987232 100644 --- a/src/Altinn.Broker/Mappers/FileStatusOverviewExtMapper.cs +++ b/src/Altinn.Broker/Mappers/FileStatusOverviewExtMapper.cs @@ -8,9 +8,9 @@ namespace Altinn.Broker.Mappers; -public static class FileStatusOverviewExtMapper +internal static class FileStatusOverviewExtMapper { - public static FileOverviewExt MapToExternalModel(FileEntity file) + internal static FileOverviewExt MapToExternalModel(FileEntity file) { return new FileOverviewExt() { @@ -27,7 +27,7 @@ public static FileOverviewExt MapToExternalModel(FileEntity file) }; } - public static FileStatusExt MapToExternalEnum(FileStatus domainEnum) + internal static FileStatusExt MapToExternalEnum(FileStatus domainEnum) { return domainEnum switch { @@ -43,7 +43,7 @@ public static FileStatusExt MapToExternalEnum(FileStatus domainEnum) }; } - public static string MapToFileStatusText(FileStatus domainEnum) + internal static string MapToFileStatusText(FileStatus domainEnum) { return domainEnum switch { @@ -59,14 +59,7 @@ public static string MapToFileStatusText(FileStatus domainEnum) }; } - public static List MapToFileStatusHistoryExt(List fileHistory) => fileHistory.Select(entity => new FileStatusEventExt() - { - FileStatus = MapToExternalEnum(entity.Status), - FileStatusChanged = entity.Date, - FileStatusText = MapToFileStatusText(entity.Status) - }).ToList(); - - public static List MapToRecipients(List actorEvents, string sender) + internal static List MapToRecipients(List actorEvents, string sender) { var recipientEvents = actorEvents.Where(actorEvent => actorEvent.Actor.ActorExternalId != sender); var lastStatusForEveryRecipient = recipientEvents @@ -83,7 +76,7 @@ public static List MapToRecipients(List MapToRecipientEvents(List actorEvents) - { - return actorEvents.Select(actorEvent => new RecipientFileStatusEventExt() - { - Recipient = actorEvent.Actor.ActorExternalId, - RecipientFileStatusChanged = actorEvent.Date, - RecipientFileStatusCode = MapToExternalRecipientStatus(actorEvent.Status), - RecipientFileStatusText = MapToRecipientStatusText(actorEvent.Status) - }).ToList(); - } } diff --git a/src/Altinn.Broker/Mappers/InitializeFileMapper.cs b/src/Altinn.Broker/Mappers/InitializeFileMapper.cs new file mode 100644 index 00000000..c6953a7a --- /dev/null +++ b/src/Altinn.Broker/Mappers/InitializeFileMapper.cs @@ -0,0 +1,23 @@ +using Altinn.Broker.Application.InitializeFileCommand; +using Altinn.Broker.Models; +using Altinn.Broker.Models.Maskinporten; + +namespace Altinn.Broker.Mappers; + +internal static class InitializeFileMapper +{ + internal static InitializeFileCommandRequest MapToRequest(FileInitalizeExt fileInitializeExt, MaskinportenToken token) + { + return new InitializeFileCommandRequest() + { + Consumer = token.Consumer, + Supplier = token.Supplier, + Filename = fileInitializeExt.FileName, + SenderExternalId = fileInitializeExt.Sender, + SendersFileReference = fileInitializeExt.SendersFileReference, + PropertyList = fileInitializeExt.PropertyList, + RecipientExternalIds = fileInitializeExt.Recipients, + Checksum = fileInitializeExt.Checksum, + }; + } +} diff --git a/src/Altinn.Broker/Program.cs b/src/Altinn.Broker/Program.cs index 28f70686..4a7b8a6c 100644 --- a/src/Altinn.Broker/Program.cs +++ b/src/Altinn.Broker/Program.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; +using Altinn.Broker.Application; using Altinn.Broker.Core.Repositories; using Altinn.Broker.Core.Services; using Altinn.Broker.Integrations.Azure; @@ -16,7 +17,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.Identity.Web; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); @@ -34,7 +35,7 @@ builder.Configuration .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true); -ConfigureServices(builder.Services, builder.Configuration); +ConfigureServices(builder.Services, builder.Configuration, builder.Environment); var app = builder.Build(); app.UseMiddleware(); @@ -50,15 +51,16 @@ app.Run(); -void ConfigureServices(IServiceCollection services, IConfiguration config) +void ConfigureServices(IServiceCollection services, IConfiguration config, IHostEnvironment hostEnvironment) { - services.AddSingleton(); + services.AddApplicationHandlers(); services.Configure(config.GetSection(key: nameof(DatabaseOptions))); services.Configure(config.GetSection(key: nameof(AzureResourceManagerOptions))); services.Configure(config.GetSection(key: nameof(MaskinportenOptions))); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -78,18 +80,36 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) { var maskinportenOptions = new MaskinportenOptions(); config.GetSection(nameof(MaskinportenOptions)).Bind(maskinportenOptions); - options.RequireHttpsMetadata = false; options.SaveToken = true; options.MetadataAddress = $"{maskinportenOptions.Issuer}.well-known/oauth-authorization-server"; - options.TokenValidationParameters = new TokenValidationParameters + if (hostEnvironment.IsDevelopment()) + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + RequireExpirationTime = false, + RequireSignedTokens = false, + SignatureValidator = delegate (string token, TokenValidationParameters parameters) + { + var jwt = new JsonWebToken(token); + return jwt; + } + }; + } + else { - ValidIssuer = maskinportenOptions.Issuer, - ValidateIssuer = true, - ValidateAudience = false, - ValidateLifetime = true, - RequireExpirationTime = true, - RequireSignedTokens = true - }; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = maskinportenOptions.Issuer, + ValidateIssuer = true, + ValidateAudience = false, + ValidateLifetime = true, + RequireExpirationTime = true, + RequireSignedTokens = true + }; + } }); services.AddAuthorization(options =>