From 67c3d04d04d5430d292d90b3389f152c2ff661f0 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Tue, 10 Oct 2023 14:15:06 +0200 Subject: [PATCH 1/5] Fix bug where GetTextResources failed if localtest is started with dotnet run locally (#55) --- src/Services/LocalApp/Implementation/LocalAppFile.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Services/LocalApp/Implementation/LocalAppFile.cs b/src/Services/LocalApp/Implementation/LocalAppFile.cs index 2d985cc0..e934ac91 100644 --- a/src/Services/LocalApp/Implementation/LocalAppFile.cs +++ b/src/Services/LocalApp/Implementation/LocalAppFile.cs @@ -24,7 +24,7 @@ public LocalAppFile(IOptions localPlatformSettings) public async Task GetXACMLPolicy(string appId) { - return await File.ReadAllTextAsync(Path.Join(GetAppPath(appId), $"App/config/authorization/policy.xml")); + return await File.ReadAllTextAsync(Path.Join(GetAppConfigFolderPath(appId), "authorization","policy.xml")); } public async Task GetApplicationMetadata(string? appId) @@ -34,7 +34,7 @@ public LocalAppFile(IOptions localPlatformSettings) throw new ArgumentNullException(nameof(appId), "AppMode = file does not support null as appId"); } - var filename = Path.Join(GetAppPath(appId), $"App/config/applicationmetadata.json"); + var filename = Path.Join(GetAppConfigFolderPath(appId), "applicationmetadata.json"); if (File.Exists(filename)) { var content = await File.ReadAllTextAsync(filename, Encoding.UTF8); @@ -93,7 +93,7 @@ public async Task> GetApplications() public async Task GetTextResource(string org, string app, string language) { - string path = Path.Join(GetAppPath(app), "config", "texts", $"resource.{language.AsFileName()}.json"); + string path = Path.Join(GetAppConfigFolderPath(app), "texts", $"resource.{language.AsFileName()}.json"); if (File.Exists(path)) { @@ -116,9 +116,9 @@ public async Task> GetApplications() throw new NotImplementedException(); } - private string GetAppPath(string appId) + private string GetAppConfigFolderPath(string appId) { - return Path.Join(_localPlatformSettings.AppRepositoryBasePath, appId.Split('/').Last()); + return Path.Join(_localPlatformSettings.AppRepositoryBasePath, appId.Split('/').Last(), "App", "config"); } private async Task GetAccessManagment() From acdcb84d60f8cea19ed0bef8ee2eb9761f97145e Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Tue, 10 Oct 2023 14:24:41 +0200 Subject: [PATCH 2/5] Various updates to app-localtest (#60) * Add vscode debug files * add support for proxying /_framework/aspnetcore-browser-refresh.js calls through the loadbalancer by rewriting the both the url and the script reference. * Fix a few C# formatting isuses --- .vscode/launch.json | 33 +++++++++++++++++++++ .vscode/tasks.json | 41 ++++++++++++++++++++++++++ loadbalancer/templates/nginx.conf.conf | 6 ++++ src/Controllers/HomeController.cs | 6 ++-- 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..779ff0bb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/bin/Debug/net6.0/LocalTest.dll", + "args": [], + "cwd": "${workspaceFolder}/src", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..a3a0dfe9 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/LocalTest.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/LocalTest.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/LocalTest.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/loadbalancer/templates/nginx.conf.conf b/loadbalancer/templates/nginx.conf.conf index 1aedd49a..451c463d 100644 --- a/loadbalancer/templates/nginx.conf.conf +++ b/loadbalancer/templates/nginx.conf.conf @@ -70,6 +70,7 @@ http { location = / { proxy_pass http://localtest/Home/; + sub_filter '' ''; } location / { @@ -81,8 +82,13 @@ http { proxy_cookie_domain altinn3local.no local.altinn.cloud; } + location /Home/_framework/ { + proxy_pass http://localtest/_framework/; + } + location /Home/ { proxy_pass http://localtest/Home/; + sub_filter '' ''; } location /receipt/ { diff --git a/src/Controllers/HomeController.cs b/src/Controllers/HomeController.cs index 4e7f32f1..0fa01b82 100644 --- a/src/Controllers/HomeController.cs +++ b/src/Controllers/HomeController.cs @@ -240,7 +240,7 @@ public async Task GetTestOrgToken(string id, [FromQuery] string or public static readonly string FRONTEND_URL_COOKIE_NAME = "frontendVersion"; [HttpGet] - public async Task FrontendVersion([FromServices]HttpClient client) + public async Task FrontendVersion([FromServices] HttpClient client) { var versionFromCookie = HttpContext.Request.Cookies[FRONTEND_URL_COOKIE_NAME]; @@ -315,7 +315,7 @@ private async Task> GetTestUsersForList() var userParties = await _partiesService.GetParties(properProfile.UserId); - if (userParties.Count == 1) + if (userParties.Count == 1 && userParties.First().PartyId == properProfile.PartyId) { // Don't add singe party users to a group var party = userParties.First(); @@ -349,7 +349,7 @@ private async Task> GetTestUsersForList() private async Task GetAppAuthLevel(bool isHttp, IEnumerable testApps) { - if(!isHttp) + if (!isHttp) { return 2; } From fc95a47561d2c743ec51fdff066916c8d93ed036 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Thu, 2 Nov 2023 07:58:34 +0100 Subject: [PATCH 3/5] Add page for uploading json files from tenor to configure persons and orgs (#61) * Add page for uploading json files from tenor to configure persons and orgs Now you can open a separate tab on the localtest site and upload tenor kildedata to localtest internal storage. If there are relevant tenor files in storage, those will replace Ola Nordmann and Sofie Salt as the localtest users. You can also select a subset of the files to downoad testData.json to be used as app local users. In addtition to the user management page, I have included the following changes. * Cleanup of login page and use the menu on top instead of links scattered all over the login screen * Use latest bootstrap (from CDN, per getboostrap.com instructions) * Split HomeController into multiple smaller controllers. * Move `Reauthenticate` button besides the login button and make it a less visible color. * Loadbalancer redirects "/" and /Home/* to localtest, so all new controllers got a `/Home/[controller]/[action]` prefix * use only spaces (not mixed with tabs) in nginx.conf.conf * Fix wrong JsonPropertyName * Small UI improvments * Fix code review issues The main fix is that `GetTenorStorageDirectory` will create the directory if it does not exist. * Improve Iframe behaviour of LocalPlatformStorage * Don't let dotnet watch run add script to /LocalPlatformStorage * Mark user adminsitration as preview fix table layout --- loadbalancer/templates/nginx.conf.conf | 43 ++- src/Configuration/LocalPlatformSettings.cs | 2 + src/Controllers/DebugUsersController.cs | 65 ++++ src/Controllers/FrontendVersionController.cs | 87 +++++ src/Controllers/HomeController.cs | 109 +----- src/Controllers/StorageExplorerController.cs | 14 + src/Controllers/TenorUsersController.cs | 73 ++++ src/Models/Authentication/CustomClaim.cs | 7 +- src/Models/Authorization/Role.cs | 6 +- src/Models/TenorViewModel.cs | 19 + src/Services/Tenor/Models/BrregErFr.cs | 365 ++++++++++++++++++ src/Services/Tenor/Models/Freg.cs | 342 ++++++++++++++++ src/Services/Tenor/TenorDataReader.cs | 199 ++++++++++ src/Services/TestData/AppTestDataModel.cs | 55 ++- src/Services/TestData/TestDataService.cs | 16 +- src/Startup.cs | 1 + .../Index.cshtml} | 6 +- src/Views/Home/Index.cshtml | 20 +- src/Views/Shared/_Layout.cshtml | 32 +- src/Views/StorageExplorer/Index.cshtml | 39 ++ src/Views/TenorUsers/Index.cshtml | 106 +++++ 21 files changed, 1445 insertions(+), 161 deletions(-) create mode 100644 src/Controllers/DebugUsersController.cs create mode 100644 src/Controllers/FrontendVersionController.cs create mode 100644 src/Controllers/StorageExplorerController.cs create mode 100644 src/Controllers/TenorUsersController.cs create mode 100644 src/Models/TenorViewModel.cs create mode 100644 src/Services/Tenor/Models/BrregErFr.cs create mode 100644 src/Services/Tenor/Models/Freg.cs create mode 100644 src/Services/Tenor/TenorDataReader.cs rename src/Views/{Home/FrontendVersion.cshtml => FrontendVersion/Index.cshtml} (66%) create mode 100644 src/Views/StorageExplorer/Index.cshtml create mode 100644 src/Views/TenorUsers/Index.cshtml diff --git a/loadbalancer/templates/nginx.conf.conf b/loadbalancer/templates/nginx.conf.conf index 451c463d..7e26c95d 100644 --- a/loadbalancer/templates/nginx.conf.conf +++ b/loadbalancer/templates/nginx.conf.conf @@ -31,7 +31,7 @@ http { sendfile on; - upstream localtest { + upstream localtest { server host.docker.internal:5101; } @@ -58,7 +58,7 @@ http { } server { - listen 80 default_server; + listen 80 default_server; server_name ${TEST_DOMAIN}; proxy_redirect off; @@ -68,52 +68,53 @@ http { error_page 502 /502LocalTest.html; - location = / { + location = / { proxy_pass http://localtest/Home/; sub_filter '' ''; - } + } - location / { + location / { #Support using Local js, when a cookie value is set sub_filter_once off; sub_filter 'https://altinncdn.no/toolkits/altinn-app-frontend/3/' $LOCAL_SUB_FILTER; proxy_pass http://app/; error_page 502 /502App.html; proxy_cookie_domain altinn3local.no local.altinn.cloud; - } + } location /Home/_framework/ { - proxy_pass http://localtest/_framework/; + proxy_pass http://localtest/_framework/; } - location /Home/ { - proxy_pass http://localtest/Home/; + location /Home/ { + proxy_pass http://localtest/Home/; sub_filter '' ''; - } + } location /receipt/ { - proxy_pass http://receiptcomp/receipt/; - error_page 502 /502Receipt.html; - } + proxy_pass http://receiptcomp/receipt/; + error_page 502 /502Receipt.html; + } location /accessmanagement/ { - proxy_pass http://accessmanagementcomp/accessmanagement/; + proxy_pass http://accessmanagementcomp/accessmanagement/; error_page 502 /502Accessmanagement.html; - } + } location /storage/ { - proxy_pass http://localtest/storage/; - } + proxy_pass http://localtest/storage/; + } location /pdfservice/ { proxy_pass http://pdfservice/; } location /localtestresources/ { - proxy_pass http://localtest/localtestresources/; - } + proxy_pass http://localtest/localtestresources/; + } location /LocalPlatformStorage/ { - proxy_pass http://localtest/LocalPlatformStorage/; + proxy_pass http://localtest/LocalPlatformStorage/; + sub_filter '' ''; } location /502LocalTest.html { root /www; @@ -128,5 +129,5 @@ http { root /www; } - } + } } diff --git a/src/Configuration/LocalPlatformSettings.cs b/src/Configuration/LocalPlatformSettings.cs index 6f5c5231..8b3aadcb 100644 --- a/src/Configuration/LocalPlatformSettings.cs +++ b/src/Configuration/LocalPlatformSettings.cs @@ -68,5 +68,7 @@ public string LocalTestingStaticTestDataPath { public string RolesFolder { get; set; } = "roles/"; public string ClaimsFolder { get; set; } = "claims/"; + + public string TenorDataFolder { get; set; } = "tenorUsers"; } } diff --git a/src/Controllers/DebugUsersController.cs b/src/Controllers/DebugUsersController.cs new file mode 100644 index 00000000..d31012bc --- /dev/null +++ b/src/Controllers/DebugUsersController.cs @@ -0,0 +1,65 @@ +#nullable enable + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + + +using LocalTest.Configuration; + +using LocalTest.Services.TestData; +using Microsoft.AspNetCore.Authorization; + +namespace LocalTest.Controllers; + +[Route("Home/[controller]/[action]")] +public class DebugUsersController : Controller +{ + private readonly LocalPlatformSettings _localPlatformSettings; + private readonly TenorDataRepository _tenorDataRepository; + + public DebugUsersController( + IOptions localPlatformSettings, + TenorDataRepository tenorDataRepository) + { + _localPlatformSettings = localPlatformSettings.Value; + _tenorDataRepository = tenorDataRepository; + } + + // Debugging endpoint + [AllowAnonymous] + public async Task LocalTestUsersRaw() + { + var localData = await TestDataDiskReader.ReadFromDisk(_localPlatformSettings.LocalTestingStaticTestDataPath); + + return Json(localData); + } + + //Debugging endpoint + [AllowAnonymous] + public async Task LocalTestUsers() + { + var localData = await TestDataDiskReader.ReadFromDisk(_localPlatformSettings.LocalTestingStaticTestDataPath); + var constructedAppData = AppTestDataModel.FromTestDataModel(localData); + + return Json(constructedAppData); + } + + // Debugging endpoint + [AllowAnonymous] + public async Task LocalTestUsersRoundTrip() + { + var localData = await TestDataDiskReader.ReadFromDisk(_localPlatformSettings.LocalTestingStaticTestDataPath); + var constructedAppData = AppTestDataModel.FromTestDataModel(localData); + + return Json(constructedAppData.GetTestDataModel()); + } + + // Debugging endpoint + [AllowAnonymous] + public async Task ShowTenorUsers() + { + var localData = await _tenorDataRepository.GetAppTestDataModel(); + + return Json(localData); + } +} \ No newline at end of file diff --git a/src/Controllers/FrontendVersionController.cs b/src/Controllers/FrontendVersionController.cs new file mode 100644 index 00000000..74db638f --- /dev/null +++ b/src/Controllers/FrontendVersionController.cs @@ -0,0 +1,87 @@ +#nullable enable +using System.Text.Json; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +using LocalTest.Configuration; +using LocalTest.Models; +using LocalTest.Services.LocalApp.Interface; + +using LocalTest.Services.TestData; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace LocalTest.Controllers; + +[Route("Home/[controller]/[action]")] +public class FrontendVersionController : Controller +{ + /// + /// See src\development\loadbalancer\nginx.conf + /// + public static readonly string FRONTEND_URL_COOKIE_NAME = "frontendVersion"; + + [HttpGet] + public async Task Index([FromServices] HttpClient client) + { + var versionFromCookie = HttpContext.Request.Cookies[FRONTEND_URL_COOKIE_NAME]; + + var frontendVersion = new FrontendVersion() + { + Version = versionFromCookie, + Versions = new List() + { + new () + { + Text = "Keep as is", + Value = "", + }, + new () + { + Text = "localhost:8080 (local dev)", + Value = "http://localhost:8080/" + } + } + }; + var cdnVersionsString = await client.GetStringAsync("https://altinncdn.no/toolkits/altinn-app-frontend/index.json"); + var groupCdnVersions = new SelectListGroup() { Name = "Specific version from cdn" }; + var versions = JsonSerializer.Deserialize>(cdnVersionsString)!; + versions.Reverse(); + versions.ForEach(version => + { + frontendVersion.Versions.Add(new() + { + Text = version, + Value = $"https://altinncdn.no/toolkits/altinn-app-frontend/{version}/", + Group = groupCdnVersions + }); + }); + + return View(frontendVersion); + } + public ActionResult Index(FrontendVersion frontendVersion) + { + var options = new CookieOptions + { + Expires = DateTime.MaxValue, + HttpOnly = true, + }; + ICookieManager cookieManager = new ChunkingCookieManager(); + if (string.IsNullOrWhiteSpace(frontendVersion.Version)) + { + cookieManager.DeleteCookie(HttpContext, FRONTEND_URL_COOKIE_NAME, options); + } + else + { + cookieManager.AppendResponseCookie( + HttpContext, + FRONTEND_URL_COOKIE_NAME, + frontendVersion.Version, + options + ); + } + + return RedirectToAction("Index", "Home"); + } +} \ No newline at end of file diff --git a/src/Controllers/HomeController.cs b/src/Controllers/HomeController.cs index 0fa01b82..945b881c 100644 --- a/src/Controllers/HomeController.cs +++ b/src/Controllers/HomeController.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Text.Json; using System.Xml; using Microsoft.AspNetCore.Authentication; @@ -24,6 +23,7 @@ namespace LocalTest.Controllers { + [Route("/Home/[action]")] public class HomeController : Controller { private readonly GeneralSettings _generalSettings; @@ -32,7 +32,7 @@ public class HomeController : Controller private readonly IAuthentication _authenticationService; private readonly IApplicationRepository _applicationRepository; private readonly IParties _partiesService; - private readonly IClaims _claimsService; + private readonly ILocalApp _localApp; private readonly TestDataService _testDataService; @@ -43,7 +43,6 @@ public HomeController( IAuthentication authenticationService, IApplicationRepository applicationRepository, IParties partiesService, - IClaims claimsService, ILocalApp localApp, TestDataService testDataService) { @@ -53,40 +52,14 @@ public HomeController( _authenticationService = authenticationService; _applicationRepository = applicationRepository; _partiesService = partiesService; - _claimsService = claimsService; _localApp = localApp; _testDataService = testDataService; } [AllowAnonymous] - public async Task LocalTestUsersRaw() - { - var localData = await TestDataDiskReader.ReadFromDisk(_localPlatformSettings.LocalTestingStaticTestDataPath); - - return Json(localData); - } - - //Debugging endpoint - [AllowAnonymous] - public async Task LocalTestUsers() - { - var localData = await TestDataDiskReader.ReadFromDisk(_localPlatformSettings.LocalTestingStaticTestDataPath); - var constructedAppData = AppTestDataModel.FromTestDataModel(localData); - - return Json(constructedAppData); - } - - // Debugging endpoint - [AllowAnonymous] - public async Task LocalTestUsersRoundTrip() - { - var localData = await TestDataDiskReader.ReadFromDisk(_localPlatformSettings.LocalTestingStaticTestDataPath); - var constructedAppData = AppTestDataModel.FromTestDataModel(localData); - - return Json(constructedAppData.GetTestDataModel()); - } - - [AllowAnonymous] + [HttpGet("/")] + [HttpGet("/Home")] + [HttpGet("/Home/Index")] public async Task Index() { StartAppModel model = new StartAppModel() @@ -95,7 +68,7 @@ public async Task Index() AppPath = _localPlatformSettings.AppRepositoryBasePath, StaticTestDataPath = _localPlatformSettings.LocalTestingStaticTestDataPath, LocalAppUrl = _localPlatformSettings.LocalAppUrl, - LocalFrontendUrl = HttpContext.Request.Cookies[FRONTEND_URL_COOKIE_NAME], + LocalFrontendUrl = HttpContext.Request.Cookies[FrontendVersionController.FRONTEND_URL_COOKIE_NAME], }; try @@ -234,76 +207,6 @@ public async Task GetTestOrgToken(string id, [FromQuery] string or return Ok(token); } - /// - /// See src\development\loadbalancer\nginx.conf - /// - public static readonly string FRONTEND_URL_COOKIE_NAME = "frontendVersion"; - - [HttpGet] - public async Task FrontendVersion([FromServices] HttpClient client) - { - var versionFromCookie = HttpContext.Request.Cookies[FRONTEND_URL_COOKIE_NAME]; - - - - var frontendVersion = new FrontendVersion() - { - Version = versionFromCookie, - Versions = new List() - { - new () - { - Text = "Keep as is", - Value = "", - }, - new () - { - Text = "localhost:8080 (local dev)", - Value = "http://localhost:8080/" - } - } - }; - var cdnVersionsString = await client.GetStringAsync("https://altinncdn.no/toolkits/altinn-app-frontend/index.json"); - var groupCdnVersions = new SelectListGroup() { Name = "Specific version from cdn" }; - var versions = JsonSerializer.Deserialize>(cdnVersionsString); - versions.Reverse(); - versions.ForEach(version => - { - frontendVersion.Versions.Add(new() - { - Text = version, - Value = $"https://altinncdn.no/toolkits/altinn-app-frontend/{version}/", - Group = groupCdnVersions - }); - }); - - return View(frontendVersion); - } - public ActionResult FrontendVersion(FrontendVersion frontendVersion) - { - var options = new CookieOptions - { - Expires = DateTime.MaxValue, - HttpOnly = true, - }; - ICookieManager cookieManager = new ChunkingCookieManager(); - if (string.IsNullOrWhiteSpace(frontendVersion.Version)) - { - cookieManager.DeleteCookie(HttpContext, FRONTEND_URL_COOKIE_NAME, options); - } - else - { - cookieManager.AppendResponseCookie( - HttpContext, - FRONTEND_URL_COOKIE_NAME, - frontendVersion.Version, - options - ); - } - - return RedirectToAction("Index"); - } - private async Task> GetTestUsersForList() { var data = await _testDataService.GetTestData(); diff --git a/src/Controllers/StorageExplorerController.cs b/src/Controllers/StorageExplorerController.cs new file mode 100644 index 00000000..7085cf93 --- /dev/null +++ b/src/Controllers/StorageExplorerController.cs @@ -0,0 +1,14 @@ +#nullable enable + +using Microsoft.AspNetCore.Mvc; + +namespace LocalTest.Controllers; + +[Route("Home/[controller]/[action]")] +public class StorageExplorerController : Controller +{ + public IActionResult Index() + { + return View(); + } +} \ No newline at end of file diff --git a/src/Controllers/TenorUsersController.cs b/src/Controllers/TenorUsersController.cs new file mode 100644 index 00000000..d5c39abe --- /dev/null +++ b/src/Controllers/TenorUsersController.cs @@ -0,0 +1,73 @@ +#nullable enable +using System.Text.Json; + +using Microsoft.AspNetCore.Mvc; +using LocalTest.Models; +using LocalTest.Services.LocalApp.Interface; + +using LocalTest.Services.TestData; + +namespace LocalTest.Controllers; + +[Route("Home/[controller]/[action]")] +public class TenorUsersController : Controller +{ + private readonly TenorDataRepository _tenorDataRepository; + + public TenorUsersController(TenorDataRepository tenorDataRepository) + { + _tenorDataRepository = tenorDataRepository; + } + + + + public async Task Index() + { + return View(new TenorViewModel() + { + FileItems = await _tenorDataRepository.GetFileItems(), + }); + } + + + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Upload() + { + //TODO: validate uploaded files + foreach (var file in Request.Form.Files) + { + await _tenorDataRepository.StoreUploadedFile(file); + } + return RedirectToAction("Index"); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Update() + { + var files = Request.Form.Keys.Where(k => k.EndsWith(".json")).ToList(); + if (Request.Form.ContainsKey("Download")) + { + return Json(await _tenorDataRepository.GetAppTestDataModel(files)); + } + else if (Request.Form.ContainsKey("DownloadFile")) + { + return File(JsonSerializer.SerializeToUtf8Bytes(await _tenorDataRepository.GetAppTestDataModel(files)), "application/json", "testData.json"); + } + else if (Request.Form.ContainsKey("Delete")) + { + foreach (var file in files) + { + _tenorDataRepository.DeleteFile(file); + } + + return RedirectToAction("Index"); + } + else + { + throw new Exception("Unknown action"); + } + } +} \ No newline at end of file diff --git a/src/Models/Authentication/CustomClaim.cs b/src/Models/Authentication/CustomClaim.cs index 9283c96c..1d14df2e 100644 --- a/src/Models/Authentication/CustomClaim.cs +++ b/src/Models/Authentication/CustomClaim.cs @@ -1,21 +1,26 @@ -namespace Altinn.Platform.Authentication.Model +using System.Text.Json.Serialization; + +namespace Altinn.Platform.Authentication.Model { public class CustomClaim { /// /// Gets or sets the claim type, E.g. custom:claim /// + [JsonPropertyName("type")] public string Type { get; set; } /// /// Gets or sets the claim value, E.g. customValue /// + [JsonPropertyName("value")] public string Value { get; set; } /// /// Gets or sets the value type for the claim, E.g. http://www.w3.org/2001/XMLSchema#string /// See System.Security.Claims.ClaimValueTypes for more value types /// + [JsonPropertyName("valueType")] public string ValueType { get; set; } } } diff --git a/src/Models/Authorization/Role.cs b/src/Models/Authorization/Role.cs index a67fe7f6..17acdf07 100644 --- a/src/Models/Authorization/Role.cs +++ b/src/Models/Authorization/Role.cs @@ -1,7 +1,5 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; +using System.Text.Json.Serialization; namespace Authorization.Interface.Models { @@ -15,12 +13,14 @@ public class Role /// Gets or sets the role type /// [JsonProperty] + [JsonPropertyName("type")] public string Type { get; set; } /// /// Gets or sets the role /// [JsonProperty] + [JsonPropertyName("value")] public string Value { get; set; } } } diff --git a/src/Models/TenorViewModel.cs b/src/Models/TenorViewModel.cs new file mode 100644 index 00000000..d1cb4d9a --- /dev/null +++ b/src/Models/TenorViewModel.cs @@ -0,0 +1,19 @@ +#nullable enable +namespace LocalTest.Models; + +using LocalTest.Services.Tenor.Models; +using LocalTest.Services.TestData; + +public class TenorViewModel +{ + public List FileItems { get; set; } = default!; +} + +public class TenorFileItem +{ + public string FileName { get; set; } = default!; + public string RawFileContent { get; set; } = default!; + public bool Valid => Freg is not null || Brreg is not null; + public Freg? Freg { get; set; } + public BrregErFr? Brreg { get; set; } +} \ No newline at end of file diff --git a/src/Services/Tenor/Models/BrregErFr.cs b/src/Services/Tenor/Models/BrregErFr.cs new file mode 100644 index 00000000..1cdda1c3 --- /dev/null +++ b/src/Services/Tenor/Models/BrregErFr.cs @@ -0,0 +1,365 @@ +#nullable disable +namespace LocalTest.Services.Tenor.Models; + + +using System.Text.Json.Serialization; + +public class BrregErFr +{ + /// + /// Internally assigned property that is Altinn's inernal ID + /// + [JsonIgnore] + public int PartyId { get; set; } + + [JsonPropertyName("organisasjonsnummer")] + public string Organisasjonsnummer { get; set; } + + [JsonPropertyName("navn")] + public string Navn { get; set; } + + [JsonPropertyName("oppdeltNavn")] + public List OppdeltNavn { get; set; } + + [JsonPropertyName("organisasjonsform")] + public Organisasjonsform Organisasjonsform { get; set; } + + [JsonPropertyName("forretningsadresse")] + public Forretningsadresse Forretningsadresse { get; set; } + + [JsonPropertyName("postadresse")] + public Postadresse Postadresse { get; set; } + + [JsonPropertyName("naeringskoder")] + public List Naeringskoder { get; set; } + + [JsonPropertyName("institusjonellSektorkode")] + public InstitusjonellSektorkode InstitusjonellSektorkode { get; set; } + + [JsonPropertyName("registreringsdatoEnhetsregisteret")] + public string RegistreringsdatoEnhetsregisteret { get; set; } + + [JsonPropertyName("slettetIEnhetsregisteret")] + public string SlettetIEnhetsregisteret { get; set; } + + [JsonPropertyName("registrertIForetaksregisteret")] + public string RegistrertIForetaksregisteret { get; set; } + + [JsonPropertyName("registreringsdatoForetaksregisteret")] + public string RegistreringsdatoForetaksregisteret { get; set; } + + [JsonPropertyName("slettetIForetaksregisteret")] + public string SlettetIForetaksregisteret { get; set; } + + [JsonPropertyName("registreringspliktigForetaksregisteret")] + public string RegistreringspliktigForetaksregisteret { get; set; } + + [JsonPropertyName("registrertIFrivillighetsregisteret")] + public string RegistrertIFrivillighetsregisteret { get; set; } + + [JsonPropertyName("registrertIStiftelsesregisteret")] + public string RegistrertIStiftelsesregisteret { get; set; } + + [JsonPropertyName("registrertIMvaregisteret")] + public string RegistrertIMvaregisteret { get; set; } + + [JsonPropertyName("stiftelsesdato")] + public string Stiftelsesdato { get; set; } + + [JsonPropertyName("aktivitetBransje")] + public List AktivitetBransje { get; set; } + + [JsonPropertyName("vedtektsdato")] + public string Vedtektsdato { get; set; } + + [JsonPropertyName("vedtektsfestetFormaal")] + public List VedtektsfestetFormaal { get; set; } + + [JsonPropertyName("sisteInnsendteAarsregnskap")] + public string SisteInnsendteAarsregnskap { get; set; } + + [JsonPropertyName("konkurs")] + public string Konkurs { get; set; } + + [JsonPropertyName("underAvvikling")] + public string UnderAvvikling { get; set; } + + [JsonPropertyName("underTvangsavviklingEllerTvangsopplosning")] + public string UnderTvangsavviklingEllerTvangsopplosning { get; set; } + + [JsonPropertyName("maalform")] + public string Maalform { get; set; } + + [JsonPropertyName("ansvarsbegrensning")] + public string Ansvarsbegrensning { get; set; } + + [JsonPropertyName("harAnsatte")] + public string HarAnsatte { get; set; } + + [JsonPropertyName("antallAnsatte")] + public int? AntallAnsatte { get; set; } + + [JsonPropertyName("underenhet")] + public Underenhet Underenhet { get; set; } + + [JsonPropertyName("bedriftsforsamling")] + public string Bedriftsforsamling { get; set; } + + [JsonPropertyName("representantskap")] + public string Representantskap { get; set; } + + [JsonPropertyName("enhetstatuser")] + public List Enhetstatuser { get; set; } + + [JsonPropertyName("fullmakter")] + public List Fullmakter { get; set; } + + [JsonPropertyName("frivilligMvaRegistrert")] + public List FrivilligMvaRegistrert { get; set; } + + [JsonPropertyName("finansielleInstrumenter")] + public List FinansielleInstrumenter { get; set; } + + [JsonPropertyName("kapital")] + public Kapital Kapital { get; set; } + + [JsonPropertyName("kjonnsrepresentasjon")] + public string Kjonnsrepresentasjon { get; set; } + + [JsonPropertyName("matrikkelnummer")] + public List Matrikkelnummer { get; set; } + + [JsonPropertyName("paategninger")] + public List Paategninger { get; set; } + + [JsonPropertyName("fravalgAvRevisjon")] + public FravalgAvRevisjon FravalgAvRevisjon { get; set; } + + [JsonPropertyName("norskregistrertUtenlandskForetak")] + public NorskregistrertUtenlandskForetak NorskregistrertUtenlandskForetak { get; set; } + + [JsonPropertyName("lovgivningOgForetaksformIHjemlandet")] + public LovgivningOgForetaksformIHjemlandet LovgivningOgForetaksformIHjemlandet { get; set; } + + [JsonPropertyName("registerIHjemlandet")] + public RegisterIHjemlandet RegisterIHjemlandet { get; set; } + + [JsonPropertyName("fusjoner")] + public List Fusjoner { get; set; } + + [JsonPropertyName("fisjoner")] + public List Fisjoner { get; set; } + + [JsonPropertyName("rollegrupper")] + public List Rollegrupper { get; set; } +} + +public class Forretningsadresse +{ + [JsonPropertyName("land")] + public string Land { get; set; } + + [JsonPropertyName("landkode")] + public string Landkode { get; set; } + + [JsonPropertyName("postnummer")] + public string Postnummer { get; set; } + + [JsonPropertyName("poststed")] + public string Poststed { get; set; } + + [JsonPropertyName("adresse")] + public List Adresse { get; set; } + + [JsonPropertyName("kommune")] + public string Kommune { get; set; } + + [JsonPropertyName("kommunenummer")] + public string Kommunenummer { get; set; } +} + +public class FravalgAvRevisjon +{ + [JsonPropertyName("fravalg")] + public string Fravalg { get; set; } +} + +public class Fritekst +{ + [JsonPropertyName("plassering")] + public string Plassering { get; set; } +} + +public class InstitusjonellSektorkode +{ + [JsonPropertyName("kode")] + public string Kode { get; set; } + + [JsonPropertyName("beskrivelse")] + public string Beskrivelse { get; set; } +} + +public class Kapital +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("belop")] + public string Belop { get; set; } + + [JsonPropertyName("antallAksjer")] + public string AntallAksjer { get; set; } + + [JsonPropertyName("innbetaltBelop")] + public string InnbetaltBelop { get; set; } + + [JsonPropertyName("fritekst")] + public List Fritekst { get; set; } + + [JsonPropertyName("fulltInnbetaltBelop")] + public string FulltInnbetaltBelop { get; set; } + + [JsonPropertyName("sakkyndigRedegjorelse")] + public string SakkyndigRedegjorelse { get; set; } +} + +public class LovgivningOgForetaksformIHjemlandet +{ + [JsonPropertyName("foretaksform")] + public string Foretaksform { get; set; } +} + +public class Naeringskoder +{ + [JsonPropertyName("kode")] + public string Kode { get; set; } + + [JsonPropertyName("beskrivelse")] + public string Beskrivelse { get; set; } + + [JsonPropertyName("hjelpeenhetskode")] + public bool? Hjelpeenhetskode { get; set; } + + [JsonPropertyName("rekkefolge")] + public int? Rekkefolge { get; set; } + + [JsonPropertyName("nivaa")] + public int? Nivaa { get; set; } +} + +public class NorskregistrertUtenlandskForetak +{ + [JsonPropertyName("helNorskEierskap")] + public string HelNorskEierskap { get; set; } + + [JsonPropertyName("aktivitetINorge")] + public string AktivitetINorge { get; set; } +} + +public class Organisasjonsform +{ + [JsonPropertyName("kode")] + public string Kode { get; set; } + + [JsonPropertyName("beskrivelse")] + public string Beskrivelse { get; set; } +} + +public class Person +{ + [JsonPropertyName("foedselsnummer")] + public string Foedselsnummer { get; set; } +} + +public class Postadresse +{ + [JsonPropertyName("land")] + public string Land { get; set; } + + [JsonPropertyName("landkode")] + public string Landkode { get; set; } + + [JsonPropertyName("postnummer")] + public string Postnummer { get; set; } + + [JsonPropertyName("poststed")] + public string Poststed { get; set; } + + [JsonPropertyName("adresse")] + public List Adresse { get; set; } + + [JsonPropertyName("kommune")] + public string Kommune { get; set; } + + [JsonPropertyName("kommunenummer")] + public string Kommunenummer { get; set; } +} + +public class RegisterIHjemlandet +{ + [JsonPropertyName("navnRegister")] + public List NavnRegister { get; set; } + + [JsonPropertyName("adresse")] + public List Adresse { get; set; } +} + +public class Rollegrupper +{ + [JsonPropertyName("type")] + public Type Type { get; set; } + + [JsonPropertyName("fritekst")] + public List Fritekst { get; set; } + + [JsonPropertyName("roller")] + public List Roller { get; set; } +} + +public class Roller +{ + [JsonPropertyName("type")] + public Type Type { get; set; } + + [JsonPropertyName("person")] + public Person Person { get; set; } + + [JsonPropertyName("virksomhet")] + public Virksomhet Virksomhet { get; set; } + + [JsonPropertyName("valgtAv")] + public ValgtAv ValgtAv { get; set; } + + [JsonPropertyName("fratraadt")] + public string Fratraadt { get; set; } + + [JsonPropertyName("fritekst")] + public List Fritekst { get; set; } + + [JsonPropertyName("rekkefolge")] + public int? Rekkefolge { get; set; } +} + + + +public class Type +{ + [JsonPropertyName("kode")] + public string Kode { get; set; } + + [JsonPropertyName("beskrivelse")] + public string Beskrivelse { get; set; } +} + +public class Underenhet +{ +} + +public class ValgtAv +{ +} + +public class Virksomhet +{ +} + diff --git a/src/Services/Tenor/Models/Freg.cs b/src/Services/Tenor/Models/Freg.cs new file mode 100644 index 00000000..5c6b3842 --- /dev/null +++ b/src/Services/Tenor/Models/Freg.cs @@ -0,0 +1,342 @@ +#nullable disable +namespace LocalTest.Services.Tenor.Models; + +using System.Text.Json.Serialization; + +public class Freg +{ + /// + /// Internally assigned property that is Altinn's inernal ID + /// + [JsonIgnore] + public int PartyId { get; set; } + + [JsonPropertyName("identifikasjonsnummer")] + public List Identifikasjonsnummer { get; set; } + + [JsonPropertyName("status")] + public List Status { get; set; } + + [JsonPropertyName("kjoenn")] + public List Kjoenn { get; set; } + + [JsonPropertyName("foedsel")] + public List Foedsel { get; set; } + + [JsonPropertyName("foedselINorge")] + public List FoedselINorge { get; set; } + + [JsonPropertyName("familierelasjon")] + public List Familierelasjon { get; set; } + + [JsonPropertyName("sivilstand")] + public List Sivilstand { get; set; } + + [JsonPropertyName("navn")] + public List Navn { get; set; } + + [JsonPropertyName("bostedsadresse")] + public List Bostedsadresse { get; set; } + + [JsonPropertyName("statsborgerskap")] + public List Statsborgerskap { get; set; } +} + +public class Adressenummer +{ + [JsonPropertyName("husnummer")] + public string Husnummer { get; set; } +} + +public class Bostedsadresse +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("aarsak")] + public string Aarsak { get; set; } + + [JsonPropertyName("gyldighetstidspunkt")] + public DateTime? Gyldighetstidspunkt { get; set; } + + [JsonPropertyName("vegadresse")] + public Vegadresse Vegadresse { get; set; } + + [JsonPropertyName("adresseIdentifikatorFraMatrikkelen")] + public string AdresseIdentifikatorFraMatrikkelen { get; set; } + + [JsonPropertyName("adressegradering")] + public string Adressegradering { get; set; } + + [JsonPropertyName("flyttedato")] + public string Flyttedato { get; set; } + + [JsonPropertyName("grunnkrets")] + public int? Grunnkrets { get; set; } + + [JsonPropertyName("stemmekrets")] + public int? Stemmekrets { get; set; } + + [JsonPropertyName("skolekrets")] + public int? Skolekrets { get; set; } + + [JsonPropertyName("kirkekrets")] + public int? Kirkekrets { get; set; } +} + +public class Familierelasjon +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("aarsak")] + public string Aarsak { get; set; } + + [JsonPropertyName("gyldighetstidspunkt")] + public DateTime? Gyldighetstidspunkt { get; set; } + + [JsonPropertyName("relatertPerson")] + public string RelatertPerson { get; set; } + + [JsonPropertyName("relatertPersonsRolle")] + public string RelatertPersonsRolle { get; set; } + + [JsonPropertyName("minRolleForPerson")] + public string MinRolleForPerson { get; set; } +} + +public class Foedsel +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("gyldighetstidspunkt")] + public DateTime? Gyldighetstidspunkt { get; set; } + + [JsonPropertyName("foedselsdato")] + public string Foedselsdato { get; set; } + + [JsonPropertyName("foedselsaar")] + public string Foedselsaar { get; set; } + + [JsonPropertyName("foedekommuneINorge")] + public string FoedekommuneINorge { get; set; } + + [JsonPropertyName("foedeland")] + public string Foedeland { get; set; } +} + +public class FoedselINorge +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("aarsak")] + public string Aarsak { get; set; } + + [JsonPropertyName("gyldighetstidspunkt")] + public DateTime? Gyldighetstidspunkt { get; set; } + + [JsonPropertyName("foedselsinstitusjonsnavn")] + public string Foedselsinstitusjonsnavn { get; set; } + + [JsonPropertyName("rekkefoelgenummer")] + public int? Rekkefoelgenummer { get; set; } +} + +public class Identifikasjonsnummer +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("foedselsEllerDNummer")] + public string FoedselsEllerDNummer { get; set; } + + [JsonPropertyName("identifikatortype")] + public string Identifikatortype { get; set; } +} + +public class KjoennType +{ + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("kjoenn")] + public string Kjoenn { get; set; } +} + +public class Navn +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("aarsak")] + public string Aarsak { get; set; } + + [JsonPropertyName("gyldighetstidspunkt")] + public DateTime? Gyldighetstidspunkt { get; set; } + + [JsonPropertyName("fornavn")] + public string Fornavn { get; set; } + + [JsonPropertyName("mellomnavn")] + public string Mellomnavn { get; set; } + + [JsonPropertyName("etternavn")] + public string Etternavn { get; set; } +} + +public class Poststed +{ + [JsonPropertyName("poststedsnavn")] + public string Poststedsnavn { get; set; } + + [JsonPropertyName("postnummer")] + public string Postnummer { get; set; } +} + + +public class SivilstandType +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("aarsak")] + public string Aarsak { get; set; } + + [JsonPropertyName("gyldighetstidspunkt")] + public DateTime? Gyldighetstidspunkt { get; set; } + + [JsonPropertyName("sivilstand")] + public string Sivilstand { get; set; } + + [JsonPropertyName("sivilstandsdato")] + public string Sivilstandsdato { get; set; } + + [JsonPropertyName("myndighet")] + public string Myndighet { get; set; } + + [JsonPropertyName("kommune")] + public string Kommune { get; set; } + + [JsonPropertyName("sted")] + public string Sted { get; set; } + + [JsonPropertyName("relatertVedSivilstand")] + public string RelatertVedSivilstand { get; set; } +} + +public class StatsborgerskapType +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("aarsak")] + public string Aarsak { get; set; } + + [JsonPropertyName("gyldighetstidspunkt")] + public DateTime? Gyldighetstidspunkt { get; set; } + + [JsonPropertyName("statsborgerskap")] + public string Statsborgerskap { get; set; } + + [JsonPropertyName("ervervsdato")] + public string Ervervsdato { get; set; } +} + +public class StatusType +{ + [JsonPropertyName("ajourholdstidspunkt")] + public DateTime? Ajourholdstidspunkt { get; set; } + + [JsonPropertyName("erGjeldende")] + public bool? ErGjeldende { get; set; } + + [JsonPropertyName("kilde")] + public string Kilde { get; set; } + + [JsonPropertyName("gyldighetstidspunkt")] + public DateTime? Gyldighetstidspunkt { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } +} + +public class Vegadresse +{ + [JsonPropertyName("kommunenummer")] + public string Kommunenummer { get; set; } + + [JsonPropertyName("bruksenhetsnummer")] + public string Bruksenhetsnummer { get; set; } + + [JsonPropertyName("bruksenhetstype")] + public string Bruksenhetstype { get; set; } + + [JsonPropertyName("adressenavn")] + public string Adressenavn { get; set; } + + [JsonPropertyName("adressenummer")] + public Adressenummer Adressenummer { get; set; } + + [JsonPropertyName("adressekode")] + public string Adressekode { get; set; } + + [JsonPropertyName("poststed")] + public Poststed Poststed { get; set; } +} + diff --git a/src/Services/Tenor/TenorDataReader.cs b/src/Services/Tenor/TenorDataReader.cs new file mode 100644 index 00000000..d41cfa6c --- /dev/null +++ b/src/Services/Tenor/TenorDataReader.cs @@ -0,0 +1,199 @@ +#nullable enable +using System.Text.Json; +using Authorization.Interface.Models; +using LocalTest.Configuration; +using LocalTest.Models; +using LocalTest.Services.Tenor.Models; +using Microsoft.Extensions.Options; + +namespace LocalTest.Services.TestData; + +public class TenorDataRepository +{ + private static readonly JsonSerializerOptions _options = new(JsonSerializerDefaults.Web) + { + Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() } + }; + private readonly LocalPlatformSettings _settings; + + public TenorDataRepository(IOptions settings) + { + _settings = settings.Value; + } + + public DirectoryInfo GetTenorStorageDirectory() + { + var dir = new DirectoryInfo(Path.Join(_settings.LocalTestingStorageBasePath, _settings.TenorDataFolder)); + if (!dir.Exists) + { + dir.Create(); + } + return dir; + } + + + public async Task<(List, List)> ReadFromDisk(List? files = null) + { + var freg = new List(); + var brregErFr = new List(); + var tenorFolder = GetTenorStorageDirectory(); + + foreach (var fregFile in tenorFolder.GetFiles("freg.*.kildedata.json").Where(f => files?.Contains(f.Name) ?? true)) + { + var fileBytes = await File.ReadAllBytesAsync(fregFile.FullName); + var fileData = JsonSerializer.Deserialize(fileBytes, _options); + if (fileData is not null) + freg.Add(fileData); + } + foreach (var brregFile in tenorFolder.GetFiles("brreg-er-fr.*.kildedata.json").Where(f => files?.Contains(f.Name) ?? true)) + { + var fileBytes = await File.ReadAllBytesAsync(brregFile.FullName); + var fileData = JsonSerializer.Deserialize(fileBytes, _options); + if (fileData is not null) + brregErFr.Add(fileData); + } + + return (brregErFr, freg); + } + + public async Task GetAppTestDataModel(List? files = null) + { + var (brreg, freg) = await ReadFromDisk(files); + // Assign partyId to all entities + int partyId = 600000; + freg.ForEach(f => f.PartyId = partyId++); + partyId = 700000; + brreg.ForEach(b => b.PartyId = partyId++); + + + var roles = brreg.SelectMany(b => b.Rollegrupper.SelectMany(rg => rg.Roller.Select(r => (b.PartyId, r.Type.Kode, r.Person.Foedselsnummer)))).Where(r => r.Foedselsnummer is not null); + var fnrRoleLookup = roles.GroupBy(r => r.Foedselsnummer).ToDictionary(role => role.Key, role => role.GroupBy(r => r.PartyId).ToDictionary(k => k.Key, k => k.Select(tuple => new Role { Type = "Altinn", Value = tuple.Kode }).ToList())); + + + int userId = 10000; + return new() + { + Persons = freg.Select(f => + { + var fnr = f.Identifikasjonsnummer.FirstErGjeldende()?.FoedselsEllerDNummer ?? throw new Exception("fødselsnummer ikke funnet"); + var adresse = f.Bostedsadresse.FirstErGjeldende() ?? throw new Exception("Mangler bostedsadresse"); + return new AppTestPerson + { + UserId = userId++, + PartyId = f.PartyId, + SSN = fnr, + FirstName = f.Navn.FirstErGjeldende()?.Fornavn ?? "Ukjent", + LastName = f.Navn.FirstErGjeldende()?.Etternavn ?? "Ukjent", + MiddleName = f.Navn.FirstErGjeldende()?.Mellomnavn, + UserName = $"user-{99999999999 - long.Parse(fnr)}", // Make an sytnetic username based on an obfuscated fnr + AddressCity = adresse.Vegadresse.Poststed.Poststedsnavn, + // AddressMunicipalName = adresse.Vegadresse.Kommunenummer, + AddressMunicipalNumber = adresse.Vegadresse.Kommunenummer, + AddressHouseLetter = f.Bostedsadresse.FirstErGjeldende()?.Vegadresse.Adressekode, + AddressHouseNumber = f.Bostedsadresse.FirstErGjeldende()?.Vegadresse.Adressenummer.Husnummer, + AddressMunicipalName = null, + AddressPostalCode = null, + AddressStreetName = null, + Email = null, + MobileNumber = null, + TelephoneNumber = null, + Language = null, + MailingAddress = null, + MailingPostalCity = null, + MailingPostalCode = null, + PartyRoles = fnrRoleLookup.TryGetValue(fnr, out var partyRoles) ? partyRoles : new Dictionary>(), + CustomClaims = new() + { + new() + { + Type = "user:source", + ValueType = "http://www.w3.org/2001/XMLSchema#string", + Value = "localTenor" + } + }, + + }; + }).ToList(), + Orgs = brreg.Select(b => new AppTestOrg + { + PartyId = b.PartyId, + ParentPartyId = null, + OrgNumber = b.Organisasjonsnummer, + Name = b.Navn, + BusinessAddress = string.Join("\n", b.Forretningsadresse.Adresse), + BusinessPostalCity = b.Forretningsadresse.Poststed, + BusinessPostalCode = b.Forretningsadresse.Postnummer, + MailingAddress = string.Join("\n", b.Postadresse.Adresse), + MailingPostalCity = b.Postadresse.Poststed, + MailingPostalCode = b.Postadresse.Postnummer, + EMailAddress = null, + FaxNumber = null, + InternetAddress = null, + MobileNumber = null, + TelephoneNumber = null, + UnitStatus = null, + UnitType = null, + }).ToList(), + }; + } + + public async Task StoreUploadedFile(IFormFile file) + { + var dir = GetTenorStorageDirectory(); + var filename = new FileInfo(Path.Join(dir.FullName, file.FileName)); + if (filename.Directory?.FullName != dir.FullName) + { + throw new Exception($"Invalid filename {file.FileName}"); + } + using Stream fileStream = filename.OpenWrite(); + await file.CopyToAsync(fileStream); + } + private static T? ParseCatchException(string rawJson) where T : class + { + try + { + return JsonSerializer.Deserialize(rawJson, _options); + } + catch (Exception) + { + return null; + } + } + public async Task> GetFileItems() + { + var directory = GetTenorStorageDirectory(); + var itemList = new List(); + foreach (var f in directory.GetFiles()) + { + if (f.Name.StartsWith('.')) + { + continue; // Ignore hidden files (like .DS_Store) + } + var content = await System.IO.File.ReadAllTextAsync(f.FullName); + + itemList.Add(new() + { + FileName = f.Name, + RawFileContent = content, + Freg = ParseCatchException(content), + Brreg = ParseCatchException(content), + }); + }; + return itemList; + } + + public void DeleteFile(string fileName) + { + var fileHandle = GetTenorStorageDirectory().GetFiles(fileName).First(f => f.Name == fileName); + fileHandle.Delete(); + } +} + +public static class ListExtentions +{ + public static T? FirstErGjeldende(this List list) + { + var erGjeldendeAccessor = typeof(T).GetProperty("erGjeldende"); + return list.FirstOrDefault(t => erGjeldendeAccessor?.GetValue(t) as bool? == true) ?? list.FirstOrDefault(); + } +} \ No newline at end of file diff --git a/src/Services/TestData/AppTestDataModel.cs b/src/Services/TestData/AppTestDataModel.cs index b97792e8..a6ead9e1 100644 --- a/src/Services/TestData/AppTestDataModel.cs +++ b/src/Services/TestData/AppTestDataModel.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Text.Json.Serialization; using Altinn.Platform.Authentication.Model; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; @@ -12,7 +13,12 @@ namespace LocalTest.Services.TestData; /// public class AppTestDataModel { + [JsonPropertyOrder(int.MinValue)] + [JsonPropertyName("$schema")] + public string Schema => "https://altinncdn.no/schemas/json/test-users/test-users.schema.v1.json"; + [JsonPropertyName("persons")] public List Persons { get; set; } = default!; + [JsonPropertyName("orgs")] public List Orgs { get; set; } = default!; public TestDataModel GetTestDataModel() @@ -155,27 +161,49 @@ public static AppTestDataModel FromTestDataModel(TestDataModel localData) }; return constructedAppData; } + + public bool IsEmpty() + { + return Persons.Count == 0 && Orgs.Count == 0; + } } public class AppTestOrg { + [JsonPropertyName("partyId")] public int PartyId { get; set; } + [JsonPropertyName("orgNumber")] public string OrgNumber { get; set; } = default!; + [JsonPropertyName("parentPartyId")] public int? ParentPartyId { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("businessAddress")] public string? BusinessAddress { get; set; } + [JsonPropertyName("businessPostalCity")] public string? BusinessPostalCity { get; set; } + [JsonPropertyName("businessPostalCode")] public string? BusinessPostalCode { get; set; } + [JsonPropertyName("eMailAddress")] public string? EMailAddress { get; set; } + [JsonPropertyName("faxNumber")] public string? FaxNumber { get; set; } + [JsonPropertyName("internetAddress")] public string? InternetAddress { get; set; } + [JsonPropertyName("mailingAddress")] public string? MailingAddress { get; set; } + [JsonPropertyName("mailingPostalCity")] public string? MailingPostalCity { get; set; } + [JsonPropertyName("mailingPostalCode")] public string? MailingPostalCode { get; set; } + [JsonPropertyName("mobileNumber")] public string? MobileNumber { get; set; } + [JsonPropertyName("telephoneNumber")] public string? TelephoneNumber { get; set; } + [JsonPropertyName("unitStatus")] public string? UnitStatus { get; set; } + [JsonPropertyName("unitType")] public string? UnitType { get; set; } public Party ToParty(List? potentialChildOrgs = null) @@ -226,28 +254,51 @@ public Party ToParty(List? potentialChildOrgs = null) public class AppTestPerson { + [JsonPropertyName("partyId")] public int PartyId { get; set; } = default!; + [JsonPropertyName("ssn")] public string SSN { get; set; } = default!; - public string FirstName { get; set; } = default!; - public string MiddleName { get; set; } = default!; + [JsonPropertyName("firstName")] + public string? FirstName { get; set; } = default!; + [JsonPropertyName("middleName")] + public string? MiddleName { get; set; } + [JsonPropertyName("lastName")] public string LastName { get; set; } = default!; + [JsonPropertyName("customClaims")] public List CustomClaims { get; set; } = new(); + [JsonPropertyName("partyRoles")] public Dictionary> PartyRoles { get; set; } = new(); + [JsonPropertyName("addressCity")] public string? AddressCity { get; set; } + [JsonPropertyName("addressHouseLetter")] public string? AddressHouseLetter { get; set; } + [JsonPropertyName("addressHouseNumber")] public string? AddressHouseNumber { get; set; } + [JsonPropertyName("addressMunicipalName")] public string? AddressMunicipalName { get; set; } + [JsonPropertyName("addressMunicipalNumber")] public string? AddressMunicipalNumber { get; set; } + [JsonPropertyName("addressPostalCode")] public string? AddressPostalCode { get; set; } + [JsonPropertyName("addressStreetName")] public string? AddressStreetName { get; set; } + [JsonPropertyName("mailingAddress")] public string? MailingAddress { get; set; } + [JsonPropertyName("mailingPostalCity")] public string? MailingPostalCity { get; set; } + [JsonPropertyName("mailingPostalCode")] public string? MailingPostalCode { get; set; } + [JsonPropertyName("mobileNumber")] public string? MobileNumber { get; set; } + [JsonPropertyName("telephoneNumber")] public string? TelephoneNumber { get; set; } + [JsonPropertyName("email")] public string? Email { get; set; } + [JsonPropertyName("userId")] public int UserId { get; set; } + [JsonPropertyName("language")] public string? Language { get; set; } + [JsonPropertyName("userName")] public string? UserName { get; set; } public string GetFullName() => string.IsNullOrWhiteSpace(MiddleName) ? $"{FirstName} {LastName}" : $"{FirstName} {MiddleName} {LastName}"; diff --git a/src/Services/TestData/TestDataService.cs b/src/Services/TestData/TestDataService.cs index aca43d46..816f0a7a 100644 --- a/src/Services/TestData/TestDataService.cs +++ b/src/Services/TestData/TestDataService.cs @@ -9,13 +9,15 @@ namespace LocalTest.Services.TestData; public class TestDataService { private readonly ILocalApp _localApp; - private readonly LocalPlatformSettings _settings; + private readonly TenorDataRepository _tenorDataRepository; private readonly IMemoryCache _cache; + private readonly LocalPlatformSettings _settings; private readonly ILogger _logger; - public TestDataService(ILocalApp localApp, IOptions settings, IMemoryCache memoryCache, ILogger logger) + public TestDataService(ILocalApp localApp, TenorDataRepository tenorDataRepository, IOptions settings, IMemoryCache memoryCache, ILogger logger) { - _cache = memoryCache; _localApp = localApp; + _tenorDataRepository = tenorDataRepository; + _cache = memoryCache; _settings = settings.Value; _logger = logger; } @@ -42,6 +44,14 @@ public async Task GetTestData() _logger.LogInformation(e, "Fetching Test data from app failed."); } + var tenorUsers = await _tenorDataRepository.GetAppTestDataModel(); + if (tenorUsers is not null && !tenorUsers.IsEmpty()) + { + // Use tenor users if they exist + return tenorUsers.GetTestDataModel(); + } + + //Fallback to Ola Nordmann, Sofie Salt ... if no other users are availible return await TestDataDiskReader.ReadFromDisk(_settings.LocalTestingStaticTestDataPath); }))!; } diff --git a/src/Startup.cs b/src/Startup.cs index dbd1ba94..532a7f28 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -109,6 +109,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Views/Home/FrontendVersion.cshtml b/src/Views/FrontendVersion/Index.cshtml similarity index 66% rename from src/Views/Home/FrontendVersion.cshtml rename to src/Views/FrontendVersion/Index.cshtml index 80787a16..d2ed08ed 100644 --- a/src/Views/Home/FrontendVersion.cshtml +++ b/src/Views/FrontendVersion/Index.cshtml @@ -1,13 +1,13 @@ @model FrontendVersion @{ - ViewData["Title"] = "Frontend version"; + ViewData["Title"] = "Frontend version"; }

@ViewData["Title"]

-@using (Html.BeginForm("FrontendVersion", "Home", FormMethod.Post, new { Class = "form-signin" })) +@using (Html.BeginForm("Index", "FrontendVersion", FormMethod.Post, new { Class = "form-signin" })) { - @Html.AntiForgeryToken(); + @Html.AntiForgeryToken()
@Html.DropDownListFor(model => model.Version, Model.Versions, new { Class = "form-control" }) diff --git a/src/Views/Home/Index.cshtml b/src/Views/Home/Index.cshtml index 43cc4659..c910fb34 100644 --- a/src/Views/Home/Index.cshtml +++ b/src/Views/Home/Index.cshtml @@ -84,7 +84,6 @@ @Html.DropDownListFor(model => model.AuthenticationLevel, Model.AuthenticationLevels, new { Class = "form-control" })
- @if(Model.AppModeIsHttp) {
@@ -95,23 +94,16 @@
} - - } - - - - @if(string.IsNullOrWhiteSpace(Model.LocalFrontendUrl)) - { - diff --git a/src/Views/Shared/_Layout.cshtml b/src/Views/Shared/_Layout.cshtml index f8ea777e..5bcddce3 100644 --- a/src/Views/Shared/_Layout.cshtml +++ b/src/Views/Shared/_Layout.cshtml @@ -4,20 +4,35 @@ @ViewData["Title"] - Altinn Studio - +
@@ -27,12 +42,7 @@ @RenderBody() - -
-
- © 2019 - LocalTest - Privacy -
-
@RenderSection("Scripts", required: false) + diff --git a/src/Views/StorageExplorer/Index.cshtml b/src/Views/StorageExplorer/Index.cshtml new file mode 100644 index 00000000..4e4e9c03 --- /dev/null +++ b/src/Views/StorageExplorer/Index.cshtml @@ -0,0 +1,39 @@ + + + + + diff --git a/src/Views/TenorUsers/Index.cshtml b/src/Views/TenorUsers/Index.cshtml new file mode 100644 index 00000000..28a3a79b --- /dev/null +++ b/src/Views/TenorUsers/Index.cshtml @@ -0,0 +1,106 @@ +@model LocalTest.Models.TenorViewModel + +@using Microsoft.Extensions.Options +@using LocalTest.Configuration +@using LocalTest.Services.TestData + +@inject IOptions LocalPlatformSettings + + +@{ + ViewData["Title"] = "Local users and orgs (preview)"; +} +

@ViewData["Title"]

+ +

+ By default Altinn Studio publishes a limited set of test accouns for local testing. For most apps these will be + enough, but if you have more specific needs, or want the same users as in tt02, you probably want to import data + from Tenor testdata to use for your local development +

+ +

+ Tenor requires login with an official norwegian ID (like BankID), so you currently have to login and download + "kildedata" for the persons and organisations that you want to reference. Make sure you find suitable users and + organisations and download "kildedata" for each org/user. +

+ + + +
+
+

Last opp tenor kildefiler

+
+ @using (Html.BeginForm("Upload", "TenorUsers", FormMethod.Post, new + { + Class = "form-signin", + enctype = + "multipart/form-data" + })) + { + @Html.AntiForgeryToken() + ; +
+ +
+
+ +
+ } +
+ +@using (Html.BeginForm("Update", "TenorUsers", FormMethod.Post, new { })) +{ + + + + + + + + + + + + @foreach (var item in Model.FileItems) + { + + + + @if (item.Brreg is not null) + { + + + + } + else if (item.Freg is not null) + { + + + + + + } + else + { + + + } + + } + +
VelgFilenameNameFnr/org
+ + @item.FileName@item.Brreg.Navn@item.Brreg.Organisasjonsnummer@item.Freg.Navn?.FirstErGjeldende()?.Fornavn @item.Freg.Navn?.FirstErGjeldende()?.Mellomnavn + @item.Freg.Navn?.FirstErGjeldende()?.Etternavn@item.Freg.Identifikasjonsnummer?.FirstErGjeldende()?.FoedselsEllerDNummer +
@item.RawFileContent
+
+ +
+ + +
+} From 974e636c4f2fdd392e100132fdf73fe9b2e339fd Mon Sep 17 00:00:00 2001 From: Christer Rustand Date: Wed, 15 Nov 2023 21:19:15 +0100 Subject: [PATCH 4/5] Add HTTP attributes to actions in HomeController (#63) --- src/Controllers/HomeController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Controllers/HomeController.cs b/src/Controllers/HomeController.cs index 945b881c..c46c4792 100644 --- a/src/Controllers/HomeController.cs +++ b/src/Controllers/HomeController.cs @@ -180,6 +180,7 @@ public async Task LogInTestUser(string action, StartAppModel start /// /// /// + [HttpGet("{userId}")] public async Task GetTestUserToken(int userId) { UserProfile profile = await _userProfileService.GetUser(userId); @@ -199,6 +200,7 @@ public async Task GetTestUserToken(int userId) /// /// /// + [HttpGet("{id}")] public async Task GetTestOrgToken(string id, [FromQuery] string orgNumber = null) { // Create a test token with long duration From be9992676fb577e809035eb614656a8911e87278 Mon Sep 17 00:00:00 2001 From: Stephanie Buadu <47737608+acn-sbuad@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:56:58 +0100 Subject: [PATCH 5/5] notifications funcitonality added #GCPActive (#62) * copied all code * Added repository logic * all notifications code in single folder * Reverted changes to other components * added readme * copied with namespace * added nullable enable * My suggested changes to feature/notifications (#65) --------- Co-authored-by: Ivar Nesje --- src/Configuration/LocalPlatformSettings.cs | 12 +- src/LocalTest.csproj | 4 + .../EmailNotificationOrdersController.cs | 98 ++++++++++ .../API/Extensions/ResourceLinkExtensions.cs | 81 +++++++++ src/Notifications/API/Mappers/OrderMapper.cs | 168 ++++++++++++++++++ .../API/Models/EmailContentTypeExt.cs | 18 ++ .../EmailNotificationOrderRequestExt.cs | 58 ++++++ .../API/Models/EmailNotificationSummaryExt.cs | 44 +++++ .../Models/EmailNotificationWithResultExt.cs | 37 ++++ .../API/Models/EmailTemplateExt.cs | 34 ++++ .../API/Models/IBaseNotificationOrderExt.cs | 41 +++++ .../API/Models/NotificationChannelExt.cs | 13 ++ .../API/Models/NotificationOrderExt.cs | 56 ++++++ .../API/Models/NotificationOrderListExt.cs | 25 +++ .../Models/NotificationOrderWithStatusExt.cs | 50 ++++++ .../Models/NotificationResourceLinksExt.cs | 19 ++ .../API/Models/NotificationStatusExt.cs | 38 ++++ .../Models/NotificationsStatusSummaryExt.cs | 19 ++ src/Notifications/API/Models/OrderIdExt.cs | 27 +++ .../API/Models/OrderResourceLinksExt.cs | 31 ++++ src/Notifications/API/Models/RecipientExt.cs | 19 ++ src/Notifications/API/Models/StatusExt.cs | 31 ++++ .../EmailNotificationOrderRequestValidator.cs | 54 ++++++ .../Validators/ValidationResultExtensions.cs | 23 +++ .../Configuration/NotificationOrderConfig.cs | 13 ++ src/Notifications/Core/Enums/AddressType.cs | 12 ++ .../Core/Enums/EmailContentType.cs | 18 ++ .../Core/Enums/EmailNotificationResultType.cs | 43 +++++ src/Notifications/Core/Enums/IResultType.cs | 9 + .../Core/Enums/NotificationChannel.cs | 13 ++ .../Core/Enums/NotificationTemplateType.cs | 12 ++ .../Core/Enums/OrderProcessingStatus.cs | 14 ++ .../Core/Models/Address/EmailAddressPoint.cs | 35 ++++ .../Core/Models/Address/IAddressPoint.cs | 19 ++ src/Notifications/Core/Models/Creator.cs | 21 +++ src/Notifications/Core/Models/Email.cs | 71 ++++++++ .../Models/Notification/EmailNotification.cs | 57 ++++++ .../Notification/EmailNotificationSummary.cs | 32 ++++ .../EmailNotificationWithResult.cs | 46 +++++ .../Core/Models/Notification/INotification.cs | 38 ++++ .../Notification/INotificationSummary.cs | 34 ++++ .../Notification/INotificationWithResult.cs | 32 ++++ .../Models/Notification/NotificationResult.cs | 41 +++++ .../Notification/SendOperationResult.cs | 85 +++++++++ .../NotificationTemplate/EmailTemplate.cs | 52 ++++++ .../INotificationTemplate.cs | 19 ++ .../Models/Orders/IBaseNotificationOrder.cs | 40 +++++ .../Core/Models/Orders/NotificationOrder.cs | 122 +++++++++++++ .../Models/Orders/NotificationOrderRequest.cs | 64 +++++++ .../Orders/NotificationOrderWithStatus.cs | 134 ++++++++++++++ src/Notifications/Core/Models/Recipient.cs | 44 +++++ .../Core/Models/Recipients/EmailRecipient.cs | 18 ++ src/Notifications/Core/Models/ServiceError.cs | 36 ++++ .../Repository/Interfaces/IOrderRepository.cs | 53 ++++++ .../Core/Services/DateTimeService.cs | 19 ++ .../Services/EmailNotificationOrderService.cs | 66 +++++++ .../Core/Services/GuidService.cs | 19 ++ .../Services/Interfaces/IDateTimeService.cs | 14 ++ .../IEmailNotificationOrderService.cs | 18 ++ .../Core/Services/Interfaces/IGuidService.cs | 13 ++ .../LocalOrderRepository.cs | 79 ++++++++ .../NotificationsServiceExtentions.cs | 30 ++++ src/Notifications/README.md | 10 ++ src/Startup.cs | 13 +- 64 files changed, 2497 insertions(+), 11 deletions(-) create mode 100644 src/Notifications/API/Controllers/EmailNotificationOrdersController.cs create mode 100644 src/Notifications/API/Extensions/ResourceLinkExtensions.cs create mode 100644 src/Notifications/API/Mappers/OrderMapper.cs create mode 100644 src/Notifications/API/Models/EmailContentTypeExt.cs create mode 100644 src/Notifications/API/Models/EmailNotificationOrderRequestExt.cs create mode 100644 src/Notifications/API/Models/EmailNotificationSummaryExt.cs create mode 100644 src/Notifications/API/Models/EmailNotificationWithResultExt.cs create mode 100644 src/Notifications/API/Models/EmailTemplateExt.cs create mode 100644 src/Notifications/API/Models/IBaseNotificationOrderExt.cs create mode 100644 src/Notifications/API/Models/NotificationChannelExt.cs create mode 100644 src/Notifications/API/Models/NotificationOrderExt.cs create mode 100644 src/Notifications/API/Models/NotificationOrderListExt.cs create mode 100644 src/Notifications/API/Models/NotificationOrderWithStatusExt.cs create mode 100644 src/Notifications/API/Models/NotificationResourceLinksExt.cs create mode 100644 src/Notifications/API/Models/NotificationStatusExt.cs create mode 100644 src/Notifications/API/Models/NotificationsStatusSummaryExt.cs create mode 100644 src/Notifications/API/Models/OrderIdExt.cs create mode 100644 src/Notifications/API/Models/OrderResourceLinksExt.cs create mode 100644 src/Notifications/API/Models/RecipientExt.cs create mode 100644 src/Notifications/API/Models/StatusExt.cs create mode 100644 src/Notifications/API/Validators/EmailNotificationOrderRequestValidator.cs create mode 100644 src/Notifications/API/Validators/ValidationResultExtensions.cs create mode 100644 src/Notifications/Core/Configuration/NotificationOrderConfig.cs create mode 100644 src/Notifications/Core/Enums/AddressType.cs create mode 100644 src/Notifications/Core/Enums/EmailContentType.cs create mode 100644 src/Notifications/Core/Enums/EmailNotificationResultType.cs create mode 100644 src/Notifications/Core/Enums/IResultType.cs create mode 100644 src/Notifications/Core/Enums/NotificationChannel.cs create mode 100644 src/Notifications/Core/Enums/NotificationTemplateType.cs create mode 100644 src/Notifications/Core/Enums/OrderProcessingStatus.cs create mode 100644 src/Notifications/Core/Models/Address/EmailAddressPoint.cs create mode 100644 src/Notifications/Core/Models/Address/IAddressPoint.cs create mode 100644 src/Notifications/Core/Models/Creator.cs create mode 100644 src/Notifications/Core/Models/Email.cs create mode 100644 src/Notifications/Core/Models/Notification/EmailNotification.cs create mode 100644 src/Notifications/Core/Models/Notification/EmailNotificationSummary.cs create mode 100644 src/Notifications/Core/Models/Notification/EmailNotificationWithResult.cs create mode 100644 src/Notifications/Core/Models/Notification/INotification.cs create mode 100644 src/Notifications/Core/Models/Notification/INotificationSummary.cs create mode 100644 src/Notifications/Core/Models/Notification/INotificationWithResult.cs create mode 100644 src/Notifications/Core/Models/Notification/NotificationResult.cs create mode 100644 src/Notifications/Core/Models/Notification/SendOperationResult.cs create mode 100644 src/Notifications/Core/Models/NotificationTemplate/EmailTemplate.cs create mode 100644 src/Notifications/Core/Models/NotificationTemplate/INotificationTemplate.cs create mode 100644 src/Notifications/Core/Models/Orders/IBaseNotificationOrder.cs create mode 100644 src/Notifications/Core/Models/Orders/NotificationOrder.cs create mode 100644 src/Notifications/Core/Models/Orders/NotificationOrderRequest.cs create mode 100644 src/Notifications/Core/Models/Orders/NotificationOrderWithStatus.cs create mode 100644 src/Notifications/Core/Models/Recipient.cs create mode 100644 src/Notifications/Core/Models/Recipients/EmailRecipient.cs create mode 100644 src/Notifications/Core/Models/ServiceError.cs create mode 100644 src/Notifications/Core/Repository/Interfaces/IOrderRepository.cs create mode 100644 src/Notifications/Core/Services/DateTimeService.cs create mode 100644 src/Notifications/Core/Services/EmailNotificationOrderService.cs create mode 100644 src/Notifications/Core/Services/GuidService.cs create mode 100644 src/Notifications/Core/Services/Interfaces/IDateTimeService.cs create mode 100644 src/Notifications/Core/Services/Interfaces/IEmailNotificationOrderService.cs create mode 100644 src/Notifications/Core/Services/Interfaces/IGuidService.cs create mode 100644 src/Notifications/LocalTestNotifications/LocalOrderRepository.cs create mode 100644 src/Notifications/LocalTestNotifications/NotificationsServiceExtentions.cs create mode 100644 src/Notifications/README.md diff --git a/src/Configuration/LocalPlatformSettings.cs b/src/Configuration/LocalPlatformSettings.cs index 8b3aadcb..e7d540cc 100644 --- a/src/Configuration/LocalPlatformSettings.cs +++ b/src/Configuration/LocalPlatformSettings.cs @@ -10,7 +10,7 @@ public class LocalPlatformSettings string _localTestDataPath = null; /// - /// The endpoint for the bridge + /// The path to the local storage folder /// public string LocalTestingStorageBasePath { get; set; } @@ -21,12 +21,16 @@ public class LocalPlatformSettings public string BlobStorageFolder { get; set; } = "blobs/"; + public string NotificationsStorageFolder { get; set; } = "notifications/"; + /// /// Folder where static test data like profile, authorization, and register data is available for local testing. /// - public string LocalTestingStaticTestDataPath { + public string LocalTestingStaticTestDataPath + { get => _localTestDataPath; - set { + set + { if (!value.EndsWith(Path.DirectorySeparatorChar) && !value.EndsWith(Path.AltDirectorySeparatorChar)) { @@ -47,7 +51,7 @@ public string LocalTestingStaticTestDataPath { /// public string LocalAppMode { get; set; } - public string DocumentDbFolder { get; set; } = "documentdb/"; + public string DocumentDbFolder { get; set; } = "documentdb/"; public string InstanceCollectionFolder { get; set; } = "instances/"; diff --git a/src/LocalTest.csproj b/src/LocalTest.csproj index eae18855..0932a0e4 100644 --- a/src/LocalTest.csproj +++ b/src/LocalTest.csproj @@ -4,6 +4,9 @@ net6.0 56f36ce2-b44b-415e-a8a5-f399a76e35b9 enable + + + $(DefineConstants);LOCALTEST @@ -12,6 +15,7 @@ + diff --git a/src/Notifications/API/Controllers/EmailNotificationOrdersController.cs b/src/Notifications/API/Controllers/EmailNotificationOrdersController.cs new file mode 100644 index 00000000..aa055931 --- /dev/null +++ b/src/Notifications/API/Controllers/EmailNotificationOrdersController.cs @@ -0,0 +1,98 @@ +#nullable enable +#if !LOCALTEST +using Altinn.Notifications.Configuration; +#endif +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Services.Interfaces; +using Altinn.Notifications.Extensions; +using Altinn.Notifications.Mappers; +using Altinn.Notifications.Models; +using Altinn.Notifications.Validators; + +using FluentValidation; + +#if !LOCALTEST +using Microsoft.AspNetCore.Authorization; +#endif +using Microsoft.AspNetCore.Mvc; + +#if !LOCALTEST +using Swashbuckle.AspNetCore.Annotations; +using Swashbuckle.AspNetCore.Filters; +#endif + +namespace Altinn.Notifications.Controllers; + +/// +/// Controller for all operations related to email notification orders +/// +[Route("notifications/api/v1/orders/email")] +[ApiController] +#if !LOCALTEST +[Authorize(Policy = AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS)] +[SwaggerResponse(401, "Caller is unauthorized")] +[SwaggerResponse(403, "Caller is not authorized to access the requested resource")] +# endif +public class EmailNotificationOrdersController : ControllerBase +{ + private readonly IValidator _validator; + private readonly IEmailNotificationOrderService _orderService; + + /// + /// Initializes a new instance of the class. + /// + public EmailNotificationOrdersController(IValidator validator, IEmailNotificationOrderService orderService) + { + _validator = validator; + _orderService = orderService; + } + + /// + /// Add an email notification order. + /// + /// + /// The API will accept the request after som basic validation of the request. + /// The system will also attempt to verify that it will be possible to fulfill the order. + /// + /// The id of the registered notification order + [HttpPost] + [Consumes("application/json")] + [Produces("application/json")] +#if !LOCALTEST + [SwaggerResponse(202, "The notification order was accepted", typeof(OrderIdExt))] + [SwaggerResponse(400, "The notification order is invalid", typeof(ValidationProblemDetails))] + [SwaggerResponseHeader(202, "Location", "string", "Link to access the newly created notification order.")] +#endif + public async Task> Post(EmailNotificationOrderRequestExt emailNotificationOrderRequest) + { + var validationResult = _validator.Validate(emailNotificationOrderRequest); + if (!validationResult.IsValid) + { + validationResult.AddToModelState(ModelState); + return ValidationProblem(ModelState); + } + +#if LOCALTEST + string creator = "localtest"; +#else + string? creator = HttpContext.GetOrg(); + + if (creator == null) + { + return Forbid(); + } +#endif + + var orderRequest = emailNotificationOrderRequest.MapToOrderRequest(creator); + (NotificationOrder? registeredOrder, ServiceError? error) = await _orderService.RegisterEmailNotificationOrder(orderRequest); + + if (error != null) + { + return StatusCode(error.ErrorCode, error.ErrorMessage); + } + + string selfLink = registeredOrder!.GetSelfLink(); + return Accepted(selfLink, new OrderIdExt(registeredOrder!.Id)); + } +} \ No newline at end of file diff --git a/src/Notifications/API/Extensions/ResourceLinkExtensions.cs b/src/Notifications/API/Extensions/ResourceLinkExtensions.cs new file mode 100644 index 00000000..a8ce73fa --- /dev/null +++ b/src/Notifications/API/Extensions/ResourceLinkExtensions.cs @@ -0,0 +1,81 @@ +#nullable enable +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Models; + +namespace Altinn.Notifications.Extensions; + +/// +/// Extension class for ResourceLinks +/// +public static class ResourceLinkExtensions +{ + private static string? _baseUri; + + /// + /// Initializes the ResourceLinkExtensions with the base URI from settings. + /// + /// + /// Should be called during startup to ensure base url is set + /// + public static void Initialize(string baseUri) + { + _baseUri = baseUri; + } + + /// + /// Sets the resource links on an external notification order + /// + /// Exception if class has not been initialized in Program.cs + public static void SetResourceLinks(this NotificationOrderExt order) + { + if (_baseUri == null) + { + throw new InvalidOperationException("ResourceLinkExtensions has not been initialized with the base URI."); + } + + string self = _baseUri + "/notifications/api/v1/orders/" + order.Id; + + order.Links = new() + { + Self = self, + Status = self + "/status", + Notifications = self + "/notifications" + }; + } + + /// + /// Gets the self link for the provided notification order + /// + /// Exception if class has not been initialized in Program.cs + public static void NotificationSummaryResourceLinks(this NotificationOrderWithStatusExt order) + { + if (_baseUri == null) + { + throw new InvalidOperationException("ResourceLinkExtensions has not been initialized with the base URI."); + } + + string baseUri = $"{_baseUri}/notifications/api/v1/orders/{order!.Id}/notifications/"; + + if (order.NotificationsStatusSummary?.Email != null) + { + order.NotificationsStatusSummary.Email.Links = new() + { + Self = baseUri + "email" + }; + } + } + + /// + /// Gets the self link for the provided notification order + /// + /// Exception if class has not been initialized in Program.cs + public static string GetSelfLink(this NotificationOrder order) + { + if (_baseUri == null) + { + throw new InvalidOperationException("ResourceLinkExtensions has not been initialized with the base URI."); + } + + return _baseUri + "/notifications/api/v1/orders/" + order!.Id; + } +} diff --git a/src/Notifications/API/Mappers/OrderMapper.cs b/src/Notifications/API/Mappers/OrderMapper.cs new file mode 100644 index 00000000..c0738e15 --- /dev/null +++ b/src/Notifications/API/Mappers/OrderMapper.cs @@ -0,0 +1,168 @@ +#nullable enable +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Address; +using Altinn.Notifications.Core.Models.NotificationTemplate; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Extensions; +using Altinn.Notifications.Models; + +namespace Altinn.Notifications.Mappers; + +/// +/// Mapper for +/// +public static class OrderMapper +{ + /// + /// Maps a to a + /// + public static NotificationOrderRequest MapToOrderRequest(this EmailNotificationOrderRequestExt extRequest, string creator) + { + var emailTemplate = new EmailTemplate(null, extRequest.Subject, extRequest.Body, (EmailContentType)extRequest.ContentType); + + var recipients = new List(); + + recipients.AddRange( + extRequest.Recipients.Select(r => new Recipient(string.Empty, new List() { new EmailAddressPoint(r.EmailAddress!) }))); + + return new NotificationOrderRequest( + extRequest.SendersReference, + creator, + new List() { emailTemplate }, + extRequest.RequestedSendTime, + NotificationChannel.Email, + recipients); + } + + /// + /// Maps a to a + /// + public static NotificationOrderExt MapToNotificationOrderExt(this NotificationOrder order) + { + var orderExt = new NotificationOrderExt(); + + orderExt.MapBaseNotificationOrder(order); + orderExt.Recipients = order.Recipients.MapToRecipientExt(); + + foreach (var template in order.Templates) + { + switch (template.Type) + { + case NotificationTemplateType.Email: + var emailTemplate = template! as EmailTemplate; + + orderExt.EmailTemplate = new() + { + Body = emailTemplate!.Body, + FromAddress = emailTemplate.FromAddress, + ContentType = (EmailContentTypeExt)emailTemplate.ContentType, + Subject = emailTemplate.Subject + }; + + break; + default: + break; + } + } + + orderExt.SetResourceLinks(); + return orderExt; + } + + /// + /// Maps a to a + /// + public static NotificationOrderWithStatusExt MapToNotificationOrderWithStatusExt(this NotificationOrderWithStatus order) + { + var orderExt = new NotificationOrderWithStatusExt(); + orderExt.MapBaseNotificationOrder(order); + + orderExt.ProcessingStatus = new() + { + LastUpdate = order.ProcessingStatus.LastUpdate, + Status = order.ProcessingStatus.Status.ToString(), + StatusDescription = order.ProcessingStatus.StatusDescription + }; + + if (order.NotificationStatuses.Any()) + { + orderExt.NotificationsStatusSummary = new(); + foreach (var entry in order.NotificationStatuses) + { + NotificationTemplateType notificationType = entry.Key; + NotificationStatus status = entry.Value; + + switch (notificationType) + { + case NotificationTemplateType.Email: + orderExt.NotificationsStatusSummary.Email = new() + { + Generated = status.Generated, + Succeeded = status.Succeeded + }; + break; + } + } + + orderExt.NotificationSummaryResourceLinks(); + } + + return orderExt; + } + + /// + /// Maps a list of to a + /// + public static NotificationOrderListExt MapToNotificationOrderListExt(this List orders) + { + NotificationOrderListExt ordersExt = new() + { + Count = orders.Count + }; + + foreach (NotificationOrder order in orders) + { + ordersExt.Orders.Add(order.MapToNotificationOrderExt()); + } + + return ordersExt; + } + + /// + /// Maps a List of to a List of + /// + internal static List MapToRecipientExt(this List recipients) + { + var recipientExt = new List(); + + recipientExt.AddRange( + recipients.Select(r => new RecipientExt + { + EmailAddress = GetEmailFromAddressList(r.AddressInfo) + })); + + return recipientExt; + } + + private static IBaseNotificationOrderExt MapBaseNotificationOrder(this IBaseNotificationOrderExt orderExt, IBaseNotificationOrder order) + { + orderExt.Id = order.Id.ToString(); + orderExt.SendersReference = order.SendersReference; + orderExt.Created = order.Created; + orderExt.Creator = order.Creator.ShortName; + orderExt.NotificationChannel = (NotificationChannelExt)order.NotificationChannel; + orderExt.RequestedSendTime = order.RequestedSendTime; + + return orderExt; + } + + private static string? GetEmailFromAddressList(List addressPoints) + { + var emailAddressPoint = addressPoints + .Find(a => a.AddressType.Equals(AddressType.Email)) + as EmailAddressPoint; + + return emailAddressPoint?.EmailAddress; + } +} diff --git a/src/Notifications/API/Models/EmailContentTypeExt.cs b/src/Notifications/API/Models/EmailContentTypeExt.cs new file mode 100644 index 00000000..22921275 --- /dev/null +++ b/src/Notifications/API/Models/EmailContentTypeExt.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace Altinn.Notifications.Models; + +/// +/// Enum describing available content types for an email. +/// +public enum EmailContentTypeExt +{ + /// + /// The email format is plain text. + /// + Plain, + + /// + /// The email contains HTML elements + /// + Html +} diff --git a/src/Notifications/API/Models/EmailNotificationOrderRequestExt.cs b/src/Notifications/API/Models/EmailNotificationOrderRequestExt.cs new file mode 100644 index 00000000..20153a7e --- /dev/null +++ b/src/Notifications/API/Models/EmailNotificationOrderRequestExt.cs @@ -0,0 +1,58 @@ +#nullable enable +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// Class representing an email notiication order request +/// +/// +/// External representaion to be used in the API. +/// +public class EmailNotificationOrderRequestExt +{ + /// + /// Gets or sets the subject of the email + /// + [JsonPropertyName("subject")] + public string Subject { get; set; } = string.Empty; + + /// + /// Gets or sets the body of the email + /// + [JsonPropertyName("body")] + public string Body { get; set; } = string.Empty; + + /// + /// Gets or sets the content type of the email + /// + [JsonPropertyName("contentType")] + public EmailContentTypeExt ContentType { get; set; } = EmailContentTypeExt.Plain; + + /// + /// Gets or sets the send time of the email. Defaults to UtcNow. + /// + [JsonPropertyName("requestedSendTime")] + public DateTime RequestedSendTime { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the senders reference on the notification + /// + [JsonPropertyName("sendersReference")] + public string? SendersReference { get; set; } + + /// + /// Gets or sets the list of recipients + /// + [JsonPropertyName("recipients")] + public List Recipients { get; set; } = new List(); + + /// + /// Json serialized the + /// + public string Serialize() + { + return JsonSerializer.Serialize(this); + } +} diff --git a/src/Notifications/API/Models/EmailNotificationSummaryExt.cs b/src/Notifications/API/Models/EmailNotificationSummaryExt.cs new file mode 100644 index 00000000..99db078a --- /dev/null +++ b/src/Notifications/API/Models/EmailNotificationSummaryExt.cs @@ -0,0 +1,44 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Core.Models.Notification +{ + /// + /// A class representing an email notification summary + /// + /// + /// External representaion to be used in the API. + /// + public class EmailNotificationSummaryExt + { + /// + /// The order id + /// + [JsonPropertyName("orderId")] + public Guid OrderId { get; set; } + + /// + /// The senders reference + /// + [JsonPropertyName("sendersReference")] + public string? SendersReference { get; set; } + + /// + /// The number of generated email notifications + /// + [JsonPropertyName("generated")] + public int Generated { get; set; } + + /// + /// The number of email notifications that were sent successfully + /// + [JsonPropertyName("succeeded")] + public int Succeeded { get; set; } + + /// + /// A list of notifications with send result + /// + [JsonPropertyName("notifications")] + public List Notifications { get; set; } = new List(); + } +} diff --git a/src/Notifications/API/Models/EmailNotificationWithResultExt.cs b/src/Notifications/API/Models/EmailNotificationWithResultExt.cs new file mode 100644 index 00000000..2e664978 --- /dev/null +++ b/src/Notifications/API/Models/EmailNotificationWithResultExt.cs @@ -0,0 +1,37 @@ +#nullable enable +using System.Text.Json.Serialization; + +using Altinn.Notifications.Models; + +namespace Altinn.Notifications.Core.Models.Notification +{ + /// + /// EmailNotificationWithResultExt class + /// + public class EmailNotificationWithResultExt + { + /// + /// The notification id + /// + [JsonPropertyName("id")] + public Guid Id { get; set; } + + /// + /// Boolean indicating if the sending of the notification was successful + /// + [JsonPropertyName("succeeded")] + public bool Succeeded { get; set; } + + /// + /// The recipient of the notification + /// + [JsonPropertyName("recipient")] + public RecipientExt Recipient { get; set; } = new(); + + /// + /// The result status of the notification + /// + [JsonPropertyName("sendStatus")] + public StatusExt SendStatus { get; set; } = new(); + } +} diff --git a/src/Notifications/API/Models/EmailTemplateExt.cs b/src/Notifications/API/Models/EmailTemplateExt.cs new file mode 100644 index 00000000..8e2c40c4 --- /dev/null +++ b/src/Notifications/API/Models/EmailTemplateExt.cs @@ -0,0 +1,34 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// Template for an email notification +/// +public class EmailTemplateExt +{ + /// + /// Gets the from adress of emails created by the template + /// + [JsonPropertyName("fromAddress")] + public string FromAddress { get; set; } = string.Empty; + + /// + /// Gets the subject of emails created by the template + /// + [JsonPropertyName("subject")] + public string Subject { get; set; } = string.Empty; + + /// + /// Gets the body of emails created by the template + /// + [JsonPropertyName("body")] + public string Body { get; set; } = string.Empty; + + /// + /// Gets the content type of emails created by the template + /// + [JsonPropertyName("contentType")] + public EmailContentTypeExt ContentType { get; set; } = EmailContentTypeExt.Plain; +} diff --git a/src/Notifications/API/Models/IBaseNotificationOrderExt.cs b/src/Notifications/API/Models/IBaseNotificationOrderExt.cs new file mode 100644 index 00000000..4d7f2065 --- /dev/null +++ b/src/Notifications/API/Models/IBaseNotificationOrderExt.cs @@ -0,0 +1,41 @@ +#nullable enable +namespace Altinn.Notifications.Models; + +/// +/// A class representing the base properties of a registered notification order. +/// +/// +/// External representaion to be used in the API. +/// +public interface IBaseNotificationOrderExt +{ + /// + /// Gets or sets the id of the notification order + /// + public string Id { get; set; } + + /// + /// Gets or sets the short name of the creator of the notification order + /// + public string Creator { get; set; } + + /// + /// Gets or sets the senders reference of the notification + /// + public string? SendersReference { get; set; } + + /// + /// Gets or sets the requested send time of the notification + /// + public DateTime RequestedSendTime { get; set; } + + /// + /// Gets or sets the date and time of when the notification order was created + /// + public DateTime Created { get; set; } + + /// + /// Gets or sets the preferred notification channel of the notification order + /// + public NotificationChannelExt NotificationChannel { get; set; } +} diff --git a/src/Notifications/API/Models/NotificationChannelExt.cs b/src/Notifications/API/Models/NotificationChannelExt.cs new file mode 100644 index 00000000..11bea3a2 --- /dev/null +++ b/src/Notifications/API/Models/NotificationChannelExt.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace Altinn.Notifications.Models; + +/// +/// Enum describing available notification channels. +/// +public enum NotificationChannelExt +{ + /// + /// The selected channel for the notification is email. + /// + Email +} diff --git a/src/Notifications/API/Models/NotificationOrderExt.cs b/src/Notifications/API/Models/NotificationOrderExt.cs new file mode 100644 index 00000000..90aa71c8 --- /dev/null +++ b/src/Notifications/API/Models/NotificationOrderExt.cs @@ -0,0 +1,56 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// A class representing a registered notification order. +/// +/// +/// External representaion to be used in the API. +/// +public class NotificationOrderExt : IBaseNotificationOrderExt +{ + /// > + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// > + [JsonPropertyName("creator")] + public string Creator { get; set; } = string.Empty; + + /// > + [JsonPropertyName("sendersReference")] + public string? SendersReference { get; set; } + + /// > + [JsonPropertyName("requestedSendTime")] + public DateTime RequestedSendTime { get; set; } + + /// > + [JsonPropertyName("created")] + public DateTime Created { get; set; } + + /// > + [JsonPropertyName("notificationChannel")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public NotificationChannelExt NotificationChannel { get; set; } + + /// + /// Gets or sets the list of recipients + /// + [JsonPropertyName("recipients")] + public List Recipients { get; set; } = new List(); + + /// + /// Gets or sets the emailTemplate + /// + [JsonPropertyName("emailTemplate")] + public EmailTemplateExt? EmailTemplate { get; set; } + + /// + /// Gets or sets the link of the order + /// + [JsonPropertyName("links")] + public OrderResourceLinksExt Links { get; set; } = new OrderResourceLinksExt(); +} diff --git a/src/Notifications/API/Models/NotificationOrderListExt.cs b/src/Notifications/API/Models/NotificationOrderListExt.cs new file mode 100644 index 00000000..e21362ae --- /dev/null +++ b/src/Notifications/API/Models/NotificationOrderListExt.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// A class representing a list of notification order. +/// +/// +/// External representaion to be used in the API. +/// +public class NotificationOrderListExt +{ + /// + /// Gets or sets the number of orders in the list + /// + [JsonPropertyName("count")] + public int Count { get; set; } + + /// + /// Gets or sets the list of notification orders + /// + [JsonPropertyName("orders")] + public List Orders { get; set; } = new List(); +} diff --git a/src/Notifications/API/Models/NotificationOrderWithStatusExt.cs b/src/Notifications/API/Models/NotificationOrderWithStatusExt.cs new file mode 100644 index 00000000..856bd3b2 --- /dev/null +++ b/src/Notifications/API/Models/NotificationOrderWithStatusExt.cs @@ -0,0 +1,50 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// A class representing a registered notification order with status information. +/// +/// +/// External representation to be used in the API. +/// +public class NotificationOrderWithStatusExt : IBaseNotificationOrderExt +{ + /// > + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// > + [JsonPropertyName("sendersReference")] + public string? SendersReference { get; set; } + + /// > + [JsonPropertyName("requestedSendTime")] + public DateTime RequestedSendTime { get; set; } + + /// > + [JsonPropertyName("creator")] + public string Creator { get; set; } = string.Empty; + + /// > + [JsonPropertyName("created")] + public DateTime Created { get; set; } + + /// > + [JsonPropertyName("notificationChannel")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public NotificationChannelExt NotificationChannel { get; set; } + + /// + /// Gets or sets the processing status of the notication order + /// + [JsonPropertyName("processingStatus")] + public StatusExt ProcessingStatus { get; set; } = new(); + + /// + /// Gets or sets the summary of the notifiications statuses + /// + [JsonPropertyName("notificationsStatusSummary")] + public NotificationsStatusSummaryExt? NotificationsStatusSummary { get; set; } +} diff --git a/src/Notifications/API/Models/NotificationResourceLinksExt.cs b/src/Notifications/API/Models/NotificationResourceLinksExt.cs new file mode 100644 index 00000000..72376c2d --- /dev/null +++ b/src/Notifications/API/Models/NotificationResourceLinksExt.cs @@ -0,0 +1,19 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// A class representing a set of resource links of a notification +/// +/// +/// External representaion to be used in the API. +/// +public class NotificationResourceLinksExt +{ + /// + /// Gets or sets the self link + /// + [JsonPropertyName("self")] + public string Self { get; set; } = string.Empty; +} diff --git a/src/Notifications/API/Models/NotificationStatusExt.cs b/src/Notifications/API/Models/NotificationStatusExt.cs new file mode 100644 index 00000000..bb2bb54d --- /dev/null +++ b/src/Notifications/API/Models/NotificationStatusExt.cs @@ -0,0 +1,38 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// An abstrct class representing a status overview of a notification channels +/// +public abstract class NotificationStatusExt +{ + /// + /// Gets or sets the self link of the notification status object + /// + [JsonPropertyName("links")] + public NotificationResourceLinksExt Links { get; set; } = new(); + + /// + /// Gets or sets the number of generated notifications + /// + [JsonPropertyName("generated")] + public int Generated { get; set; } + + /// + /// Gets or sets the number of succeeeded notifications + /// + [JsonPropertyName("succeeded")] + public int Succeeded { get; set; } +} + +/// +/// A class representing a status overview for email notifications +/// +/// +/// External representaion to be used in the API. +/// +public class EmailNotificationStatusExt : NotificationStatusExt +{ +} diff --git a/src/Notifications/API/Models/NotificationsStatusSummaryExt.cs b/src/Notifications/API/Models/NotificationsStatusSummaryExt.cs new file mode 100644 index 00000000..c306a9f5 --- /dev/null +++ b/src/Notifications/API/Models/NotificationsStatusSummaryExt.cs @@ -0,0 +1,19 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// A class representing a summary of status overviews of all notification channels +/// +/// +/// External representaion to be used in the API. +/// +public class NotificationsStatusSummaryExt +{ + /// + /// Gets or sets the status of the email notifications + /// + [JsonPropertyName("email")] + public EmailNotificationStatusExt? Email { get; set; } +} diff --git a/src/Notifications/API/Models/OrderIdExt.cs b/src/Notifications/API/Models/OrderIdExt.cs new file mode 100644 index 00000000..551e574e --- /dev/null +++ b/src/Notifications/API/Models/OrderIdExt.cs @@ -0,0 +1,27 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// A class representing a container for an order id. +/// +/// +/// External representaion to be used in the API. +/// +public class OrderIdExt +{ + /// + /// The order id + /// + [JsonPropertyName("orderId")] + public Guid OrderId { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public OrderIdExt(Guid orderId) + { + OrderId = orderId; + } +} diff --git a/src/Notifications/API/Models/OrderResourceLinksExt.cs b/src/Notifications/API/Models/OrderResourceLinksExt.cs new file mode 100644 index 00000000..843f44ec --- /dev/null +++ b/src/Notifications/API/Models/OrderResourceLinksExt.cs @@ -0,0 +1,31 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// A class representing a set of resource links of a notification order. +/// +/// +/// External representaion to be used in the API. +/// +public class OrderResourceLinksExt +{ + /// + /// Gets or sets the self link + /// + [JsonPropertyName("self")] + public string Self { get; set; } = string.Empty; + + /// + /// Gets or sets the status link + /// + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + /// + /// Gets or sets the notifications link + /// + [JsonPropertyName("notifications")] + public string Notifications { get; set; } = string.Empty; +} diff --git a/src/Notifications/API/Models/RecipientExt.cs b/src/Notifications/API/Models/RecipientExt.cs new file mode 100644 index 00000000..2f7a7923 --- /dev/null +++ b/src/Notifications/API/Models/RecipientExt.cs @@ -0,0 +1,19 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// Class representing a notification recipient +/// +/// +/// External representaion to be used in the API. +/// +public class RecipientExt +{ + /// + /// Gets or sets the email address of the recipient + /// + [JsonPropertyName("emailAddress")] + public string? EmailAddress { get; set; } +} diff --git a/src/Notifications/API/Models/StatusExt.cs b/src/Notifications/API/Models/StatusExt.cs new file mode 100644 index 00000000..4b7165ca --- /dev/null +++ b/src/Notifications/API/Models/StatusExt.cs @@ -0,0 +1,31 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Notifications.Models; + +/// +/// A class representing a status summary +/// +/// +/// External representaion to be used in the API. +/// +public class StatusExt +{ + /// + /// Gets or sets the status + /// + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + /// + /// Gets or sets the description + /// + [JsonPropertyName("description")] + public string? StatusDescription { get; set; } + + /// + /// Gets or sets the date time of when the status was last updated + /// + [JsonPropertyName("lastUpdate")] + public DateTime LastUpdate { get; set; } +} diff --git a/src/Notifications/API/Validators/EmailNotificationOrderRequestValidator.cs b/src/Notifications/API/Validators/EmailNotificationOrderRequestValidator.cs new file mode 100644 index 00000000..d5165dce --- /dev/null +++ b/src/Notifications/API/Validators/EmailNotificationOrderRequestValidator.cs @@ -0,0 +1,54 @@ +#nullable enable +using System.Text.RegularExpressions; + +using Altinn.Notifications.Models; + +using FluentValidation; + +namespace Altinn.Notifications.Validators; + +/// +/// Class containing validation logic for the model +/// +public class EmailNotificationOrderRequestValidator : AbstractValidator +{ + /// + /// Initializes a new instance of the class. + /// + public EmailNotificationOrderRequestValidator() + { + RuleFor(order => order.Recipients) + .NotEmpty() + .WithMessage("One or more recipient is required.") + .Must(recipients => recipients.TrueForAll(a => IsValidEmail(a.EmailAddress))) + .WithMessage("A valid email address must be provided for all recipients."); + + RuleFor(order => order.RequestedSendTime) + .Must(sendTime => sendTime >= DateTime.UtcNow.AddMinutes(-5)) + .WithMessage("Send time must be in the future. Leave blank to send immediately."); + + RuleFor(order => order.Body).NotEmpty(); + RuleFor(order => order.Subject).NotEmpty(); + } + + /// + /// Validated as email address based on the Altinn 2 regex + /// + /// The string to validate as an email address + /// A boolean indicating that the email is valid or not + internal static bool IsValidEmail(string? email) + { + if (string.IsNullOrEmpty(email)) + { + return false; + } + + string emailRegexPattern = @"(("[^"]+")|(([a-zA-Z0-9!#$%&'*+\-=?\^_`{|}~])+(\.([a-zA-Z0-9!#$%&'*+\-=?\^_`{|}~])+)*))@((((([a-zA-Z0-9æøåÆØÅ]([a-zA-Z0-9\-æøåÆØÅ]{0,61})[a-zA-Z0-9æøåÆØÅ]\.)|[a-zA-Z0-9æøåÆØÅ]\.){1,9})([a-zA-Z]{2,14}))|((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})))"; + + Regex regex = new(emailRegexPattern, RegexOptions.None, TimeSpan.FromSeconds(1)); + + Match match = regex.Match(email); + + return match.Success; + } +} diff --git a/src/Notifications/API/Validators/ValidationResultExtensions.cs b/src/Notifications/API/Validators/ValidationResultExtensions.cs new file mode 100644 index 00000000..18ffe907 --- /dev/null +++ b/src/Notifications/API/Validators/ValidationResultExtensions.cs @@ -0,0 +1,23 @@ +#nullable enable +using FluentValidation.Results; + +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Altinn.Notifications.Validators; + +/// +/// Extension class for +/// +public static class ValidationResultExtensions +{ + /// + /// Adds the validation result to the model state + /// + public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState) + { + foreach (var error in result.Errors) + { + modelState.AddModelError(error.PropertyName, error.ErrorMessage); + } + } +} diff --git a/src/Notifications/Core/Configuration/NotificationOrderConfig.cs b/src/Notifications/Core/Configuration/NotificationOrderConfig.cs new file mode 100644 index 00000000..571b9cf1 --- /dev/null +++ b/src/Notifications/Core/Configuration/NotificationOrderConfig.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace Altinn.Notifications.Core.Configuration; + +/// +/// Configuration class for notification orders +/// +public class NotificationOrderConfig +{ + /// + /// Default from address for email notifications + /// + public string DefaultEmailFromAddress { get; set; } = string.Empty; +} diff --git a/src/Notifications/Core/Enums/AddressType.cs b/src/Notifications/Core/Enums/AddressType.cs new file mode 100644 index 00000000..caf5ca9a --- /dev/null +++ b/src/Notifications/Core/Enums/AddressType.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace Altinn.Notifications.Core.Enums; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +/// +/// Enum describing available address types +/// +public enum AddressType +{ + Email +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/src/Notifications/Core/Enums/EmailContentType.cs b/src/Notifications/Core/Enums/EmailContentType.cs new file mode 100644 index 00000000..991ae849 --- /dev/null +++ b/src/Notifications/Core/Enums/EmailContentType.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace Altinn.Notifications.Core.Enums; + +/// +/// Enum describing available email content types +/// +public enum EmailContentType +{ + /// + /// The email format is plain text. + /// + Plain, + + /// + /// The email contains HTML elements + /// + Html +} diff --git a/src/Notifications/Core/Enums/EmailNotificationResultType.cs b/src/Notifications/Core/Enums/EmailNotificationResultType.cs new file mode 100644 index 00000000..28e0562d --- /dev/null +++ b/src/Notifications/Core/Enums/EmailNotificationResultType.cs @@ -0,0 +1,43 @@ +#nullable enable +namespace Altinn.Notifications.Core.Enums; + +/// +/// Enum describing email notification result types +/// +public enum EmailNotificationResultType +{ + /// + /// Default result for new notifications + /// + New, + + /// + /// Email notification being sent + /// + Sending, + + /// + /// Email notification sent + /// + Succeeded, + + /// + /// Email delivered to recipient + /// + Delivered, + + /// + /// Failed, unknown reason + /// + Failed, + + /// + /// Recipient to address was not identified + /// + Failed_RecipientNotIdentified, + + /// + /// Invalid format for email address + /// + Failed_InvalidEmailFormat +} diff --git a/src/Notifications/Core/Enums/IResultType.cs b/src/Notifications/Core/Enums/IResultType.cs new file mode 100644 index 00000000..d8dea4d1 --- /dev/null +++ b/src/Notifications/Core/Enums/IResultType.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace Altinn.Notifications.Core.Enums; + +/// +/// Base class for send result of a notification +/// +public interface IResultType +{ +} diff --git a/src/Notifications/Core/Enums/NotificationChannel.cs b/src/Notifications/Core/Enums/NotificationChannel.cs new file mode 100644 index 00000000..4e23d060 --- /dev/null +++ b/src/Notifications/Core/Enums/NotificationChannel.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace Altinn.Notifications.Core.Enums; + +/// +/// Enum describing available notification channels. +/// +public enum NotificationChannel +{ + /// + /// The selected channel for the notification is email. + /// + Email +} diff --git a/src/Notifications/Core/Enums/NotificationTemplateType.cs b/src/Notifications/Core/Enums/NotificationTemplateType.cs new file mode 100644 index 00000000..a7e02909 --- /dev/null +++ b/src/Notifications/Core/Enums/NotificationTemplateType.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace Altinn.Notifications.Core.Enums; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +/// +/// Enum describing available notification template types +/// +public enum NotificationTemplateType +{ + Email +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/src/Notifications/Core/Enums/OrderProcessingStatus.cs b/src/Notifications/Core/Enums/OrderProcessingStatus.cs new file mode 100644 index 00000000..20c22fa9 --- /dev/null +++ b/src/Notifications/Core/Enums/OrderProcessingStatus.cs @@ -0,0 +1,14 @@ +#nullable enable +namespace Altinn.Notifications.Core.Enums; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +/// +/// Enum describing the various processing states of a notification order +/// +public enum OrderProcessingStatus +{ + Registered, + Processing, + Completed +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/src/Notifications/Core/Models/Address/EmailAddressPoint.cs b/src/Notifications/Core/Models/Address/EmailAddressPoint.cs new file mode 100644 index 00000000..aa2b3031 --- /dev/null +++ b/src/Notifications/Core/Models/Address/EmailAddressPoint.cs @@ -0,0 +1,35 @@ +#nullable enable +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models.Address; + +/// +/// A class represeting an address point +/// +public class EmailAddressPoint : IAddressPoint +{ + /// + public AddressType AddressType { get; internal set; } + + /// + /// Gets the email address + /// + public string EmailAddress { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + public EmailAddressPoint(string emailAddress) + { + AddressType = AddressType.Email; + EmailAddress = emailAddress; + } + + /// + /// Initializes a new instance of the class. + /// + internal EmailAddressPoint() + { + EmailAddress = string.Empty; + } +} diff --git a/src/Notifications/Core/Models/Address/IAddressPoint.cs b/src/Notifications/Core/Models/Address/IAddressPoint.cs new file mode 100644 index 00000000..e15ccc17 --- /dev/null +++ b/src/Notifications/Core/Models/Address/IAddressPoint.cs @@ -0,0 +1,19 @@ +#nullable enable +using System.Text.Json.Serialization; + +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models.Address; + +/// +/// Interface describing an address point +/// +[JsonDerivedType(typeof(EmailAddressPoint), "email")] +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$")] +public interface IAddressPoint +{ + /// + /// Gets or sets the address type for the address point + /// + public AddressType AddressType { get; } +} diff --git a/src/Notifications/Core/Models/Creator.cs b/src/Notifications/Core/Models/Creator.cs new file mode 100644 index 00000000..ef0e334a --- /dev/null +++ b/src/Notifications/Core/Models/Creator.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace Altinn.Notifications.Core.Models; + +/// +/// A class representing a notification creator +/// +public class Creator +{ + /// + /// Gets the short name of the creator + /// + public string ShortName { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + public Creator(string shortName) + { + ShortName = shortName; + } +} diff --git a/src/Notifications/Core/Models/Email.cs b/src/Notifications/Core/Models/Email.cs new file mode 100644 index 00000000..00a3ecce --- /dev/null +++ b/src/Notifications/Core/Models/Email.cs @@ -0,0 +1,71 @@ +#nullable enable +using System.Text.Json; +using System.Text.Json.Serialization; + +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models; + +/// +/// Class representing an email +/// +public class Email +{ + /// + /// Gets or sets the id of the email. + /// + public Guid NotificationId { get; set; } + + /// + /// Gets or sets the subject of the email. + /// + public string Subject { get; set; } + + /// + /// Gets or sets the body of the email. + /// + public string Body { get; set; } + + /// + /// Gets or sets the to fromAdress of the email. + /// + public string FromAddress { get; set; } + + /// + /// Gets or sets the to adress of the email. + /// + public string ToAddress { get; set; } + + /// + /// Gets or sets the content type of the email. + /// + public EmailContentType ContentType { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public Email(Guid notificationId, string subject, string body, string fromAddress, string toAddress, EmailContentType contentType) + { + NotificationId = notificationId; + Subject = subject; + Body = body; + FromAddress = fromAddress; + ToAddress = toAddress; + ContentType = contentType; + } + + /// + /// Json serializes the + /// + public string Serialize() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + } +} diff --git a/src/Notifications/Core/Models/Notification/EmailNotification.cs b/src/Notifications/Core/Models/Notification/EmailNotification.cs new file mode 100644 index 00000000..3348ab48 --- /dev/null +++ b/src/Notifications/Core/Models/Notification/EmailNotification.cs @@ -0,0 +1,57 @@ +#nullable enable +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models.Notification; + +/// +/// Class describing an email notification and extends the +/// +public class EmailNotification : INotification +{ + /// + public Guid Id { get; internal set; } + + /// + public Guid OrderId { get; internal set; } + + /// + public DateTime RequestedSendTime { get; internal set; } + + /// + public NotificationChannel NotificationChannel { get; } = NotificationChannel.Email; + + /// + /// Get the id of the recipient of the email notification + /// + public string? RecipientId { get; internal set; } + + /// + /// Get or sets the to address of the email notification + /// + public string ToAddress { get; internal set; } = string.Empty; + + /// + /// Get or sets the send result of the notification + /// + public NotificationResult SendResult { get; internal set; } = new(EmailNotificationResultType.New, DateTime.UtcNow); + + /// + /// Initializes a new instance of the class. + /// + public EmailNotification(Guid orderId, DateTime sendTime) + { + Id = Guid.NewGuid(); + OrderId = orderId; + RequestedSendTime = sendTime; + } + + /// + /// Initializes a new instance of the class. + /// + internal EmailNotification() + { + Id = Guid.Empty; + OrderId = Guid.Empty; + RequestedSendTime = DateTime.MinValue; + } +} diff --git a/src/Notifications/Core/Models/Notification/EmailNotificationSummary.cs b/src/Notifications/Core/Models/Notification/EmailNotificationSummary.cs new file mode 100644 index 00000000..28d97540 --- /dev/null +++ b/src/Notifications/Core/Models/Notification/EmailNotificationSummary.cs @@ -0,0 +1,32 @@ +#nullable enable +namespace Altinn.Notifications.Core.Models.Notification +{ + /// + /// An implementation of for email notifications"/> + /// + public class EmailNotificationSummary : INotificationSummary + { + /// + public Guid OrderId { get; set; } + + /// + public string? SendersReference { get; set; } + + /// + public int Generated { get; internal set; } + + /// + public int Succeeded { get; internal set; } + + /// + public List Notifications { get; set; } = new List(); + + /// + /// Initializes a new instance of the class. + /// + public EmailNotificationSummary(Guid orderId) + { + OrderId = orderId; + } + } +} diff --git a/src/Notifications/Core/Models/Notification/EmailNotificationWithResult.cs b/src/Notifications/Core/Models/Notification/EmailNotificationWithResult.cs new file mode 100644 index 00000000..a8c7e851 --- /dev/null +++ b/src/Notifications/Core/Models/Notification/EmailNotificationWithResult.cs @@ -0,0 +1,46 @@ +#nullable enable +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Recipients; + +namespace Altinn.Notifications.Core.Models.Notification +{ + /// + /// An implementation of for email notifications"/> + /// Using the as recipient type and the as result type + /// + public class EmailNotificationWithResult : INotificationWithResult + { + /// + public Guid Id { get; } + + /// + public bool Succeeded { get; internal set; } + + /// + public EmailRecipient Recipient { get; } + + /// + public NotificationResult ResultStatus { get; } + + /// + /// Initializes a new instance of the class. + /// + public EmailNotificationWithResult(Guid id, EmailRecipient recipient, NotificationResult result) + { + Id = id; + Recipient = recipient; + ResultStatus = result; + } + + /// + /// Initializes a new instance of the class. + /// + internal EmailNotificationWithResult(Guid id, bool succeeded, EmailRecipient recipient, NotificationResult result) + { + Id = id; + Succeeded = succeeded; + Recipient = recipient; + ResultStatus = result; + } + } +} diff --git a/src/Notifications/Core/Models/Notification/INotification.cs b/src/Notifications/Core/Models/Notification/INotification.cs new file mode 100644 index 00000000..4bee1b10 --- /dev/null +++ b/src/Notifications/Core/Models/Notification/INotification.cs @@ -0,0 +1,38 @@ +#nullable enable +using System; + +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models.Notification; + +/// +/// Interface describing a base notification. +/// +public interface INotification + where TEnum : struct, Enum +{ + /// + /// Gets the id of the notification. + /// + public Guid Id { get; } + + /// + /// Gets the order id of the notification. + /// + public Guid OrderId { get; } + + /// + /// Gets the requested send time of the notification. + /// + public DateTime RequestedSendTime { get; } + + /// + /// Gets the notifiction channel for the notification. + /// + public NotificationChannel NotificationChannel { get; } + + /// + /// Gets the send result of the notification. + /// + public NotificationResult SendResult { get; } +} diff --git a/src/Notifications/Core/Models/Notification/INotificationSummary.cs b/src/Notifications/Core/Models/Notification/INotificationSummary.cs new file mode 100644 index 00000000..911453b2 --- /dev/null +++ b/src/Notifications/Core/Models/Notification/INotificationSummary.cs @@ -0,0 +1,34 @@ +#nullable enable +namespace Altinn.Notifications.Core.Models.Notification; + +/// +/// An interface representing a summary of the notifications related to an order +/// +public interface INotificationSummary + where TClass : class +{ + /// + /// Gets the notification order id + /// + public Guid OrderId { get; } + + /// + /// Gets the senders reference of the notification order + /// + public string? SendersReference { get; } + + /// + /// Gets the number of generated notifications + /// + public int Generated { get; } + + /// + /// Gets the number of succeeeded notifications + /// + public int Succeeded { get; } + + /// + /// Gets the list of notifications with send result + /// + public List Notifications { get; } +} diff --git a/src/Notifications/Core/Models/Notification/INotificationWithResult.cs b/src/Notifications/Core/Models/Notification/INotificationWithResult.cs new file mode 100644 index 00000000..aa95da12 --- /dev/null +++ b/src/Notifications/Core/Models/Notification/INotificationWithResult.cs @@ -0,0 +1,32 @@ +#nullable enable +namespace Altinn.Notifications.Core.Models.Notification; + +/// +/// Interface representing a notification object with send result data +/// +/// The class representing the recipient +/// The enum used to describe the send result +public interface INotificationWithResult + where TClass : class + where TEnum : struct, Enum +{ + /// + /// Gets the notification id + /// + public Guid Id { get; } + + /// + /// Gets a boolean indicating if the sending was successful + /// + public bool Succeeded { get; } + + /// + /// Sets the recipient with contact points + /// + public TClass Recipient { get; } + + /// + /// Gets the send result + /// + public NotificationResult ResultStatus { get; } +} diff --git a/src/Notifications/Core/Models/Notification/NotificationResult.cs b/src/Notifications/Core/Models/Notification/NotificationResult.cs new file mode 100644 index 00000000..1f85bb3a --- /dev/null +++ b/src/Notifications/Core/Models/Notification/NotificationResult.cs @@ -0,0 +1,41 @@ +#nullable enable +namespace Altinn.Notifications.Core.Models.Notification; + +/// +/// A class represednting a notification result +/// +public class NotificationResult + where TEnum : struct, Enum +{ + /// + /// Initializes a new instance of the class. + /// + public NotificationResult(TEnum result, DateTime resultTime) + { + ResultTime = resultTime; + Result = result; + } + + /// + /// Sets the result description + /// + public void SetResultDescription(string? description) + { + ResultDescription = description; + } + + /// + /// Gets the date and time for when the last result was set. + /// + public DateTime ResultTime { get; } + + /// + /// Gets the send result of the notification + /// + public TEnum Result { get; } + + /// + /// Gets the description of the send result + /// + public string? ResultDescription { get; private set; } +} diff --git a/src/Notifications/Core/Models/Notification/SendOperationResult.cs b/src/Notifications/Core/Models/Notification/SendOperationResult.cs new file mode 100644 index 00000000..2031dcaa --- /dev/null +++ b/src/Notifications/Core/Models/Notification/SendOperationResult.cs @@ -0,0 +1,85 @@ +#nullable enable +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Orders; + +namespace Altinn.Notifications.Core.Models.Notification; + +/// +/// A class representing a send operation update object +/// +public class SendOperationResult +{ + /// + /// The notification id + /// + public Guid NotificationId { get; set; } + + /// + /// The send operation id + /// + public string OperationId { get; set; } = string.Empty; + + /// + /// The email send result + /// + public EmailNotificationResultType? SendResult { get; set; } + + /// + /// Json serializes the + /// + public string Serialize() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + } + + /// + /// Deserialize a json string into the + /// + public static SendOperationResult? Deserialize(string serializedString) + { + return JsonSerializer.Deserialize( + serializedString, + new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }); + } + + /// + /// Try to parse a json string into a + /// + public static bool TryParse(string input, out SendOperationResult value) + { + SendOperationResult? parsedOutput; + value = new SendOperationResult(); + + if (string.IsNullOrEmpty(input)) + { + return false; + } + + try + { + parsedOutput = Deserialize(input!); + + value = parsedOutput!; + return value.NotificationId != Guid.Empty; + } + catch + { + // try parse, we simply return false if fails + } + + return false; + } +} diff --git a/src/Notifications/Core/Models/NotificationTemplate/EmailTemplate.cs b/src/Notifications/Core/Models/NotificationTemplate/EmailTemplate.cs new file mode 100644 index 00000000..b3b627d5 --- /dev/null +++ b/src/Notifications/Core/Models/NotificationTemplate/EmailTemplate.cs @@ -0,0 +1,52 @@ +#nullable enable +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models.NotificationTemplate; + +/// +/// Template for an email notification +/// +public class EmailTemplate : INotificationTemplate +{ + /// + public NotificationTemplateType Type { get; internal set; } + + /// + /// Gets the from adress of emails created by the template + /// + public string FromAddress { get; internal set; } = string.Empty; + + /// + /// Gets the subject of emails created by the template + /// + public string Subject { get; internal set; } = string.Empty; + + /// + /// Gets the body of emails created by the template + /// + public string Body { get; internal set; } = string.Empty; + + /// + /// Gets the content type of emails created by the template + /// + public EmailContentType ContentType { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + public EmailTemplate(string? fromAddress, string subject, string body, EmailContentType contentType) + { + FromAddress = fromAddress ?? string.Empty; + Subject = subject; + Body = body; + ContentType = contentType; + Type = NotificationTemplateType.Email; + } + + /// + /// Initializes a new instance of the class. + /// + internal EmailTemplate() + { + } +} diff --git a/src/Notifications/Core/Models/NotificationTemplate/INotificationTemplate.cs b/src/Notifications/Core/Models/NotificationTemplate/INotificationTemplate.cs new file mode 100644 index 00000000..106efd09 --- /dev/null +++ b/src/Notifications/Core/Models/NotificationTemplate/INotificationTemplate.cs @@ -0,0 +1,19 @@ +#nullable enable +using System.Text.Json.Serialization; + +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models.NotificationTemplate; + +/// +/// Base class for a notification template +/// +[JsonDerivedType(typeof(EmailTemplate), "email")] +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$")] +public interface INotificationTemplate +{ + /// + /// Gets the type for the template + /// + public NotificationTemplateType Type { get; } +} diff --git a/src/Notifications/Core/Models/Orders/IBaseNotificationOrder.cs b/src/Notifications/Core/Models/Orders/IBaseNotificationOrder.cs new file mode 100644 index 00000000..64d891c6 --- /dev/null +++ b/src/Notifications/Core/Models/Orders/IBaseNotificationOrder.cs @@ -0,0 +1,40 @@ +#nullable enable +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models.Orders; + +/// +/// Class representing the base properties of a notification order +/// +public interface IBaseNotificationOrder +{ + /// + /// Gets the id of the notification order + /// + public Guid Id { get; } + + /// + /// Gets the senders reference of a notification + /// + public string? SendersReference { get; } + + /// + /// Gets the requested send time for the notification(s) + /// + public DateTime RequestedSendTime { get; } + + /// + /// Gets the preferred notification channel + /// + public NotificationChannel NotificationChannel { get; } + + /// + /// Gets the creator of the notification + /// + public Creator Creator { get; } + + /// + /// Gets the date and time for when the notification order was created + /// + public DateTime Created { get; } +} diff --git a/src/Notifications/Core/Models/Orders/NotificationOrder.cs b/src/Notifications/Core/Models/Orders/NotificationOrder.cs new file mode 100644 index 00000000..96dfee79 --- /dev/null +++ b/src/Notifications/Core/Models/Orders/NotificationOrder.cs @@ -0,0 +1,122 @@ +#nullable enable +using System.Text.Json; +using System.Text.Json.Serialization; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.NotificationTemplate; + +namespace Altinn.Notifications.Core.Models.Orders; + +/// +/// Class representing a notification order +/// +public class NotificationOrder : IBaseNotificationOrder +{ + /// > + public Guid Id { get; internal set; } = Guid.Empty; + + /// > + public string? SendersReference { get; internal set; } + + /// > + public DateTime RequestedSendTime { get; internal set; } + + /// > + public NotificationChannel NotificationChannel { get; internal set; } + + /// > + public Creator Creator { get; internal set; } + + /// > + public DateTime Created { get; internal set; } + + /// + /// Gets the templates to create notifications based of + /// + public List Templates { get; internal set; } = new List(); + + /// + /// Gets a list of recipients + /// + public List Recipients { get; internal set; } = new List(); + + /// + /// Initializes a new instance of the class. + /// + public NotificationOrder(Guid id, string? sendersReference, List templates, DateTime requestedSendTime, NotificationChannel notificationChannel, Creator creator, DateTime created, List recipients) + { + Id = id; + SendersReference = sendersReference; + Templates = templates; + RequestedSendTime = requestedSendTime; + NotificationChannel = notificationChannel; + Creator = creator; + Created = created; + Recipients = recipients; + } + + /// + /// Initializes a new instance of the class. + /// + internal NotificationOrder() + { + Creator = new Creator(string.Empty); + } + + /// + /// Json serializes the + /// + public string Serialize() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } + }); + } + + /// + /// Deserialize a json string into the + /// + public static NotificationOrder? Deserialize(string serializedString) + { + return JsonSerializer.Deserialize( + serializedString, + new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }); + } + + /// + /// Try to parse a json string into a + /// + public static bool TryParse(string input, out NotificationOrder value) + { + NotificationOrder? parsedOutput; + value = new NotificationOrder(); + + if (string.IsNullOrEmpty(input)) + { + return false; + } + + try + { + parsedOutput = Deserialize(input!); + + value = parsedOutput!; + return value.Id != Guid.Empty; + } + catch + { + // try parse, we simply return false if fails + } + + return false; + } +} diff --git a/src/Notifications/Core/Models/Orders/NotificationOrderRequest.cs b/src/Notifications/Core/Models/Orders/NotificationOrderRequest.cs new file mode 100644 index 00000000..9f794c3b --- /dev/null +++ b/src/Notifications/Core/Models/Orders/NotificationOrderRequest.cs @@ -0,0 +1,64 @@ +#nullable enable +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.NotificationTemplate; + +namespace Altinn.Notifications.Core.Models.Orders; + +/// +/// Class representing a notification order request +/// +public class NotificationOrderRequest +{ + /// + /// Gets the senders reference of a notification + /// + public string? SendersReference { get; internal set; } + + /// + /// Gets the templates to create notifications based of + /// + public List Templates { get; internal set; } + + /// + /// Gets the requested send time for the notification(s) + /// + public DateTime RequestedSendTime { get; internal set; } + + /// + /// Gets the preferred notification channel + /// + public NotificationChannel NotificationChannel { get; internal set; } + + /// + /// Gets a list of recipients + /// + public List Recipients { get; internal set; } + + /// + /// Gets the creator of the notification request + /// + public Creator Creator { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + public NotificationOrderRequest(string? sendersReference, string creatorShortName, List templates, DateTime requestedSendTime, NotificationChannel notificationChannel, List recipients) + { + SendersReference = sendersReference; + Creator = new(creatorShortName); + Templates = templates; + RequestedSendTime = requestedSendTime; + NotificationChannel = notificationChannel; + Recipients = recipients; + } + + /// + /// Initializes a new instance of the class. + /// + internal NotificationOrderRequest() + { + Creator = new Creator(string.Empty); + Templates = new List(); + Recipients = new List(); + } +} diff --git a/src/Notifications/Core/Models/Orders/NotificationOrderWithStatus.cs b/src/Notifications/Core/Models/Orders/NotificationOrderWithStatus.cs new file mode 100644 index 00000000..5c79f450 --- /dev/null +++ b/src/Notifications/Core/Models/Orders/NotificationOrderWithStatus.cs @@ -0,0 +1,134 @@ +#nullable enable +using System.Text.Json.Serialization; + +using Altinn.Notifications.Core.Enums; + +namespace Altinn.Notifications.Core.Models.Orders; + +/// +/// A class representing a registered notification order with status information. +/// +public class NotificationOrderWithStatus : IBaseNotificationOrder +{ + /// > + public Guid Id { get; internal set; } + + /// > + public string? SendersReference { get; internal set; } + + /// > + public DateTime RequestedSendTime { get; internal set; } + + /// > + public Creator Creator { get; internal set; } = new(string.Empty); + + /// > + public DateTime Created { get; internal set; } + + /// > + public NotificationChannel NotificationChannel { get; internal set; } + + /// + /// Gets the processing status of the notication order + /// + public ProcessingStatus ProcessingStatus { get; internal set; } = new(); + + /// + /// Gets the summary of the notifiications statuses + /// + public Dictionary NotificationStatuses { get; set; } = new(); + + /// + /// Initializes a new instance of the class. + /// + public NotificationOrderWithStatus(Guid id, string? sendersReference, DateTime requestedSendTime, Creator creator, DateTime created, NotificationChannel notificationChannel, ProcessingStatus processingStatus) + { + Id = id; + SendersReference = sendersReference; + RequestedSendTime = requestedSendTime; + Creator = creator; + Created = created; + NotificationChannel = notificationChannel; + ProcessingStatus = processingStatus; + } + + /// + /// Initializes a new instance of the class. + /// + internal NotificationOrderWithStatus() + { + } + + /// + /// Adds an entry to the notification statuses for the provided type + /// + public void SetNotificationStatuses(NotificationTemplateType type, int generated, int succeeded) + { + NotificationStatuses.Add(type, new NotificationStatus() { Generated = generated, Succeeded = succeeded }); + } +} + +/// +/// A class representing a summary of status overviews of all notification channels +/// +/// +/// External representaion to be used in the API. +/// +public class ProcessingStatus +{ + /// + /// Gets the status + /// + [JsonPropertyName("status")] + public OrderProcessingStatus Status { get; internal set; } + + /// + /// Gets the description + /// + [JsonPropertyName("description")] + public string? StatusDescription { get; internal set; } + + /// + /// Gets the date time of when the status was last updated + /// + [JsonPropertyName("lastUpdate")] + public DateTime LastUpdate { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + public ProcessingStatus(OrderProcessingStatus status, DateTime lastUpdate, string? statusDescription = null) + { + Status = status; + StatusDescription = statusDescription; + LastUpdate = lastUpdate; + } + + /// + /// Initializes a new instance of the class. + /// + internal ProcessingStatus() + { + } +} + +/// +/// A class representing a summary of status overviews of all notification channels +/// +/// +/// External representaion to be used in the API. +/// +public class NotificationStatus +{ + /// + /// Gets the number of generated notifications + /// + [JsonPropertyName("generated")] + public int Generated { get; internal set; } + + /// + /// Gets the number of succeeeded notifications + /// + [JsonPropertyName("succeeded")] + public int Succeeded { get; internal set; } +} diff --git a/src/Notifications/Core/Models/Recipient.cs b/src/Notifications/Core/Models/Recipient.cs new file mode 100644 index 00000000..c0517c41 --- /dev/null +++ b/src/Notifications/Core/Models/Recipient.cs @@ -0,0 +1,44 @@ +#nullable enable +using Altinn.Notifications.Core.Models.Address; + +namespace Altinn.Notifications.Core.Models; + +/// +/// Class representing a notification recipient +/// +public class Recipient +{ + /// + /// Gets the recipient id + /// + public string RecipientId { get; set; } = string.Empty; + + /// + /// Gets a list of address points for the recipient + /// + public List AddressInfo { get; set; } = new List(); + + /// + /// Initializes a new instance of the class. + /// + public Recipient(string recipientId, List addressInfo) + { + RecipientId = recipientId; + AddressInfo = addressInfo; + } + + /// + /// Initializes a new instance of the class. + /// + public Recipient(List addressInfo) + { + AddressInfo = addressInfo; + } + + /// + /// Initializes a new instance of the class. + /// + public Recipient() + { + } +} diff --git a/src/Notifications/Core/Models/Recipients/EmailRecipient.cs b/src/Notifications/Core/Models/Recipients/EmailRecipient.cs new file mode 100644 index 00000000..8d9e9216 --- /dev/null +++ b/src/Notifications/Core/Models/Recipients/EmailRecipient.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace Altinn.Notifications.Core.Models.Recipients; + +/// +/// Class representing an email recipient +/// +public class EmailRecipient +{ + /// + /// Gets or sets the recipient id + /// + public string? RecipientId { get; set; } = null; + + /// + /// Gets or sets the toaddress + /// + public string ToAddress { get; set; } = string.Empty; +} diff --git a/src/Notifications/Core/Models/ServiceError.cs b/src/Notifications/Core/Models/ServiceError.cs new file mode 100644 index 00000000..12658322 --- /dev/null +++ b/src/Notifications/Core/Models/ServiceError.cs @@ -0,0 +1,36 @@ +#nullable enable +namespace Altinn.Notifications.Core.Models; + +/// +/// A class representing a service error object used to transfere error information from service to controller. +/// +public class ServiceError +{ + /// + /// The error code + /// + /// An error code translates directly into an HTTP status code + public int ErrorCode { get; private set; } + + /// + /// The error message + /// + public string? ErrorMessage { get; private set; } + + /// + /// Create a new instance of a service error + /// + public ServiceError(int errorCode, string errorMessage) + { + ErrorCode = errorCode; + ErrorMessage = errorMessage; + } + + /// + /// Create a new instance of a service error + /// + public ServiceError(int errorCode) + { + ErrorCode = errorCode; + } +} diff --git a/src/Notifications/Core/Repository/Interfaces/IOrderRepository.cs b/src/Notifications/Core/Repository/Interfaces/IOrderRepository.cs new file mode 100644 index 00000000..e6050b48 --- /dev/null +++ b/src/Notifications/Core/Repository/Interfaces/IOrderRepository.cs @@ -0,0 +1,53 @@ +#nullable enable +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Orders; + +namespace Altinn.Notifications.Core.Repository.Interfaces; + +/// +/// Interface describing all repository actions for notification orders +/// +public interface IOrderRepository +{ + /// + /// Creates a new notification order in the database + /// + /// The order to save + /// The saved notification order + public Task Create(NotificationOrder order); + + /// + /// Gets a list of notification orders where requestedSendTime has passed + /// + /// A list of notification orders + public Task> GetPastDueOrdersAndSetProcessingState(); + + /// + /// Sets processing status of an order + /// + public Task SetProcessingStatus(Guid orderId, OrderProcessingStatus status); + + /// + /// Gets an order based on the provided id within the provided creator scope + /// + /// The order id + /// The short name of the order creator + /// A notification order if it exists + public Task GetOrderById(Guid id, string creator); + + /// + /// Gets an order with process and notification status based on the provided id within the provided creator scope + /// + /// The order id + /// The short name of the order creator + /// A notification order if it exists + public Task GetOrderWithStatusById(Guid id, string creator); + + /// + /// Gets an order based on the provided senders reference within the provided creator scope + /// + /// The senders reference + /// The short name of the order creator + /// A list of notification orders + public Task> GetOrdersBySendersReference(string sendersReference, string creator); +} diff --git a/src/Notifications/Core/Services/DateTimeService.cs b/src/Notifications/Core/Services/DateTimeService.cs new file mode 100644 index 00000000..a48321cd --- /dev/null +++ b/src/Notifications/Core/Services/DateTimeService.cs @@ -0,0 +1,19 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; + +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services; + +/// +/// Implemntation of a dateTime service +/// +[ExcludeFromCodeCoverage] +public class DateTimeService : IDateTimeService +{ + /// + public DateTime UtcNow() + { + return DateTime.UtcNow; + } +} diff --git a/src/Notifications/Core/Services/EmailNotificationOrderService.cs b/src/Notifications/Core/Services/EmailNotificationOrderService.cs new file mode 100644 index 00000000..0535e4a0 --- /dev/null +++ b/src/Notifications/Core/Services/EmailNotificationOrderService.cs @@ -0,0 +1,66 @@ +#nullable enable +using Altinn.Notifications.Core.Configuration; +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.NotificationTemplate; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Repository.Interfaces; +using Altinn.Notifications.Core.Services.Interfaces; + +using Microsoft.Extensions.Options; + +namespace Altinn.Notifications.Core.Services; + +/// +/// Implementation of the . +/// +public class EmailNotificationOrderService : IEmailNotificationOrderService +{ + private readonly IOrderRepository _repository; + private readonly IGuidService _guid; + private readonly IDateTimeService _dateTime; + private readonly string _defaultFromAddress; + + /// + /// Initializes a new instance of the class. + /// + public EmailNotificationOrderService(IOrderRepository repository, IGuidService guid, IDateTimeService dateTime, IOptions config) + { + _repository = repository; + _guid = guid; + _dateTime = dateTime; + _defaultFromAddress = config.Value.DefaultEmailFromAddress; + } + + /// + public async Task<(NotificationOrder? Order, ServiceError? Error)> RegisterEmailNotificationOrder(NotificationOrderRequest orderRequest) + { + Guid orderId = _guid.NewGuid(); + DateTime created = _dateTime.UtcNow(); + + var templates = SetFromAddressIfNotDefined(orderRequest.Templates); + + var order = new NotificationOrder( + orderId, + orderRequest.SendersReference, + templates, + orderRequest.RequestedSendTime, + orderRequest.NotificationChannel, + orderRequest.Creator, + created, + orderRequest.Recipients); + + NotificationOrder savedOrder = await _repository.Create(order); + + return (savedOrder, null); + } + + private List SetFromAddressIfNotDefined(List templates) + { + foreach (var template in templates.OfType().Where(template => string.IsNullOrEmpty(template.FromAddress))) + { + template.FromAddress = _defaultFromAddress; + } + + return templates; + } +} diff --git a/src/Notifications/Core/Services/GuidService.cs b/src/Notifications/Core/Services/GuidService.cs new file mode 100644 index 00000000..d6614803 --- /dev/null +++ b/src/Notifications/Core/Services/GuidService.cs @@ -0,0 +1,19 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; + +using Altinn.Notifications.Core.Services.Interfaces; + +namespace Altinn.Notifications.Core.Services; + +/// +/// Implementation of the GuidServiceS +/// +[ExcludeFromCodeCoverage] +public class GuidService : IGuidService +{ + /// + public Guid NewGuid() + { + return Guid.NewGuid(); + } +} diff --git a/src/Notifications/Core/Services/Interfaces/IDateTimeService.cs b/src/Notifications/Core/Services/Interfaces/IDateTimeService.cs new file mode 100644 index 00000000..8ff6b2b1 --- /dev/null +++ b/src/Notifications/Core/Services/Interfaces/IDateTimeService.cs @@ -0,0 +1,14 @@ +#nullable enable +namespace Altinn.Notifications.Core.Services.Interfaces; + +/// +/// Interface describing a dateTime service +/// +public interface IDateTimeService +{ + /// + /// Provides DateTime UtcNow + /// + /// + public DateTime UtcNow(); +} diff --git a/src/Notifications/Core/Services/Interfaces/IEmailNotificationOrderService.cs b/src/Notifications/Core/Services/Interfaces/IEmailNotificationOrderService.cs new file mode 100644 index 00000000..01d54a36 --- /dev/null +++ b/src/Notifications/Core/Services/Interfaces/IEmailNotificationOrderService.cs @@ -0,0 +1,18 @@ +#nullable enable +using Altinn.Notifications.Core.Models; +using Altinn.Notifications.Core.Models.Orders; + +namespace Altinn.Notifications.Core.Services.Interfaces; + +/// +/// Interface for the email notification order service +/// +public interface IEmailNotificationOrderService +{ + /// + /// Registers a new order + /// + /// The email notification order request + /// The registered notification order + public Task<(NotificationOrder? Order, ServiceError? Error)> RegisterEmailNotificationOrder(NotificationOrderRequest orderRequest); +} diff --git a/src/Notifications/Core/Services/Interfaces/IGuidService.cs b/src/Notifications/Core/Services/Interfaces/IGuidService.cs new file mode 100644 index 00000000..16b81efc --- /dev/null +++ b/src/Notifications/Core/Services/Interfaces/IGuidService.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace Altinn.Notifications.Core.Services.Interfaces; + +/// +/// Interface describing a guid service +/// +public interface IGuidService +{ + /// + /// Generates a new Guid + /// + public Guid NewGuid(); +} diff --git a/src/Notifications/LocalTestNotifications/LocalOrderRepository.cs b/src/Notifications/LocalTestNotifications/LocalOrderRepository.cs new file mode 100644 index 00000000..4b3cab55 --- /dev/null +++ b/src/Notifications/LocalTestNotifications/LocalOrderRepository.cs @@ -0,0 +1,79 @@ +#nullable enable +using System.Text.Json; + +using Altinn.Notifications.Core.Enums; +using Altinn.Notifications.Core.Models.Orders; +using Altinn.Notifications.Core.Repository.Interfaces; + +using LocalTest.Configuration; + +using Microsoft.Extensions.Options; + +namespace LocalTest.Notifications.Persistence.Repository +{ + public class LocalOrderRepository : IOrderRepository + { + private readonly LocalPlatformSettings _localPlatformSettings; + private readonly JsonSerializerOptions _serializerOptions; + + public LocalOrderRepository( + IOptions localPlatformSettings) + { + _localPlatformSettings = localPlatformSettings.Value; + Directory.CreateDirectory(GetNotificationsDbPath()); + + _serializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + } + + public Task Create(NotificationOrder order) + { + string path = GetOrderPath(order.Id); + + string serializedOrder = JsonSerializer.Serialize(order, _serializerOptions); + FileInfo file = new FileInfo(path); + file.Directory?.Create(); + File.WriteAllText(file.FullName, serializedOrder); + + return Task.FromResult(order); + } + + public Task GetOrderById(Guid id, string creator) + { + throw new NotImplementedException(); + } + + public Task> GetOrdersBySendersReference(string sendersReference, string creator) + { + throw new NotImplementedException(); + } + + public Task GetOrderWithStatusById(Guid id, string creator) + { + throw new NotImplementedException(); + } + + public Task> GetPastDueOrdersAndSetProcessingState() + { + throw new NotImplementedException(); + } + + public Task SetProcessingStatus(Guid orderId, OrderProcessingStatus status) + { + throw new NotImplementedException(); + } + + private string GetOrderPath(Guid orderId) + { + return Path.Combine(GetNotificationsDbPath(), "orders", orderId + ".json"); + } + + private string GetNotificationsDbPath() + { + return _localPlatformSettings.LocalTestingStorageBasePath + _localPlatformSettings.NotificationsStorageFolder; + } + } +} diff --git a/src/Notifications/LocalTestNotifications/NotificationsServiceExtentions.cs b/src/Notifications/LocalTestNotifications/NotificationsServiceExtentions.cs new file mode 100644 index 00000000..f9f101e5 --- /dev/null +++ b/src/Notifications/LocalTestNotifications/NotificationsServiceExtentions.cs @@ -0,0 +1,30 @@ +using Altinn.Notifications.Core.Configuration; +using Altinn.Notifications.Core.Repository.Interfaces; +using Altinn.Notifications.Core.Services; +using Altinn.Notifications.Core.Services.Interfaces; +using Altinn.Notifications.Extensions; +using Altinn.Notifications.Models; +using Altinn.Notifications.Validators; +using FluentValidation; +using LocalTest.Notifications.Persistence.Repository; + +namespace LocalTest.Notifications.LocalTestNotifications; + +public static class NotificationsServiceExtentions +{ + public static void AddNotificationServices(this IServiceCollection services, string baseUrl) + { + // Notifications services + ValidatorOptions.Global.LanguageManager.Enabled = false; + ResourceLinkExtensions.Initialize(baseUrl); + + services.Configure((c) => c.DefaultEmailFromAddress = "localtest@altinn.no"); + + services + .AddSingleton, EmailNotificationOrderRequestValidator>() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } +} \ No newline at end of file diff --git a/src/Notifications/README.md b/src/Notifications/README.md new file mode 100644 index 00000000..edbb7d05 --- /dev/null +++ b/src/Notifications/README.md @@ -0,0 +1,10 @@ +## Mock of Altinn Notifications for localtest + +Repository: https://github.com/Altinn/altinn-notifications + +Things to be aware of when copying code: + +- Persistence: code should not be copied directly, rather create a new implementation of the required repository class/method that writes data to disk. - Controllers: code should be copied, but changes might be required related to authorization (e.g., platform access token is not included in local requests) +- Core: code can be copied directly without changes + +Update Startup.cs with required services. \ No newline at end of file diff --git a/src/Startup.cs b/src/Startup.cs index 532a7f28..ef45057f 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -28,6 +28,7 @@ using LocalTest.Clients.CdnAltinnOrgs; using LocalTest.Configuration; using LocalTest.Helpers; +using LocalTest.Notifications.LocalTestNotifications; using LocalTest.Services.Authentication.Implementation; using LocalTest.Services.Authentication.Interface; using LocalTest.Services.Authorization.Implementation; @@ -43,14 +44,7 @@ using LocalTest.Services.TestData; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.StaticFiles.Infrastructure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; @@ -127,6 +121,11 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); + // Notifications services + + GeneralSettings generalSettings = Configuration.GetSection("GeneralSettings").Get(); + services.AddNotificationServices(generalSettings.BaseUrl); + // Storage services services.AddSingleton(); services.AddTransient();