diff --git a/.vscode/launch.json b/.vscode/launch.json index a92157fa1f..2fefdb3195 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -294,18 +294,18 @@ // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": "Run Ches Retry Service", + "name": "Run SMTP Retry Service", "type": "coreclr", "request": "launch", - "preLaunchTask": "build-ches-retry", + "preLaunchTask": "build-smtp-retry", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/services/net/ches-retry/bin/Debug/net9.0/TNO.Services.ChesRetry.dll", + "program": "${workspaceFolder}/services/net/smtp-retry/bin/Debug/net9.0/MMI.Services.SMTPRetry.dll", "args": [], - "cwd": "${workspaceFolder}/services/net/ches-retry", + "cwd": "${workspaceFolder}/services/net/smtp-retry", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false, - "envFile": "${workspaceFolder}/services/net/ches-retry/.env" + "envFile": "${workspaceFolder}/services/net/smtp-retry/.env" }, { // Use IntelliSense to find out which attributes exist for C# debugging diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 42ec776286..27a4dad104 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -645,38 +645,38 @@ "problemMatcher": "$msCompile" }, { - "label": "build-ches-retry", + "label": "build-smtp-retry", "command": "dotnet", "type": "process", "args": [ "build", - "${workspaceFolder}/services/net/ches-retry/TNO.Services.ChesRetry.csproj", + "${workspaceFolder}/services/net/smtp-retry/MMI.Services.SMTPRetry.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { - "label": "publish-ches-retry", + "label": "publish-smtp-retry", "command": "dotnet", "type": "process", "args": [ "publish", - "${workspaceFolder}/services/net/ches-retry/TNO.Services.ChesRetry.csproj", + "${workspaceFolder}/services/net/smtp-retry/MMI.Services.SMTPRetry.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { - "label": "watch-ches-retry", + "label": "watch-smtp-retry", "command": "dotnet", "type": "process", "args": [ "watch", "run", "--project", - "${workspaceFolder}/services/net/ches-retry/TNO.Services.ChesRetry.csproj" + "${workspaceFolder}/services/net/smtp-retry/MMI.Services.SMTPRetry.csproj" ], "problemMatcher": "$msCompile" }, diff --git a/TNO.sln b/TNO.sln index 7fee31e681..94a2bc08b7 100644 --- a/TNO.sln +++ b/TNO.sln @@ -81,10 +81,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TNO.Services.FFmpeg", "serv EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TNO.Services.EventHandler", "services\net\event-handler\TNO.Services.EventHandler.csproj", "{6F1F9B85-B155-4A5A-BB36-10F734F96A12}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TNO.Services.ChesRetry", "services\net\ches-retry\TNO.Services.ChesRetry.csproj", "{067EA7C3-A816-406B-B36A-09FC05A427A1}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TNO.Services.AutoClipper", "services\net\auto-clipper\TNO.Services.AutoClipper.csproj", "{7B8BF924-36BA-422E-85FD-1C590B092F7B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MMI.SmtpEmail", "libs\net\smtp\MMI.SmtpEmail.csproj", "{ED18F7E1-BFC4-4126-8FD6-ABE5CFDA581C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TNO.Services.NLP", "services\net\nlp\TNO.Services.NLP.csproj", "{7DDD3D18-E89A-4297-A5CD-D2FFF72503F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MMI.Services.SmtpRetry", "services\net\smtp-retry\MMI.Services.SmtpRetry.csproj", "{66BC5554-D360-450A-A529-97EAB4D56637}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -218,14 +222,22 @@ Global {6F1F9B85-B155-4A5A-BB36-10F734F96A12}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F1F9B85-B155-4A5A-BB36-10F734F96A12}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F1F9B85-B155-4A5A-BB36-10F734F96A12}.Release|Any CPU.Build.0 = Release|Any CPU - {067EA7C3-A816-406B-B36A-09FC05A427A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {067EA7C3-A816-406B-B36A-09FC05A427A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {067EA7C3-A816-406B-B36A-09FC05A427A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {067EA7C3-A816-406B-B36A-09FC05A427A1}.Release|Any CPU.Build.0 = Release|Any CPU {7B8BF924-36BA-422E-85FD-1C590B092F7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7B8BF924-36BA-422E-85FD-1C590B092F7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B8BF924-36BA-422E-85FD-1C590B092F7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B8BF924-36BA-422E-85FD-1C590B092F7B}.Release|Any CPU.Build.0 = Release|Any CPU + {ED18F7E1-BFC4-4126-8FD6-ABE5CFDA581C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED18F7E1-BFC4-4126-8FD6-ABE5CFDA581C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED18F7E1-BFC4-4126-8FD6-ABE5CFDA581C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED18F7E1-BFC4-4126-8FD6-ABE5CFDA581C}.Release|Any CPU.Build.0 = Release|Any CPU + {7DDD3D18-E89A-4297-A5CD-D2FFF72503F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DDD3D18-E89A-4297-A5CD-D2FFF72503F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DDD3D18-E89A-4297-A5CD-D2FFF72503F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DDD3D18-E89A-4297-A5CD-D2FFF72503F4}.Release|Any CPU.Build.0 = Release|Any CPU + {66BC5554-D360-450A-A529-97EAB4D56637}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66BC5554-D360-450A-A529-97EAB4D56637}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66BC5554-D360-450A-A529-97EAB4D56637}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66BC5554-D360-450A-A529-97EAB4D56637}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {16EA028B-B4C8-416D-BE54-D73D75483668} = {F627B24A-217D-4BF1-BC77-E1A92DBCD07F} @@ -263,7 +275,9 @@ Global {E7444ADF-0137-439B-8E20-917CF2FAFA45} = {448D6DE6-6887-48EC-A202-18C7EB428ACD} {2D455400-0E86-476E-8C42-532D32C10107} = {448D6DE6-6887-48EC-A202-18C7EB428ACD} {6F1F9B85-B155-4A5A-BB36-10F734F96A12} = {448D6DE6-6887-48EC-A202-18C7EB428ACD} - {067EA7C3-A816-406B-B36A-09FC05A427A1} = {448D6DE6-6887-48EC-A202-18C7EB428ACD} {7B8BF924-36BA-422E-85FD-1C590B092F7B} = {448D6DE6-6887-48EC-A202-18C7EB428ACD} + {ED18F7E1-BFC4-4126-8FD6-ABE5CFDA581C} = {890D13F9-A1ED-4B00-8E69-A1AB620F31A9} + {7DDD3D18-E89A-4297-A5CD-D2FFF72503F4} = {448D6DE6-6887-48EC-A202-18C7EB428ACD} + {66BC5554-D360-450A-A529-97EAB4D56637} = {448D6DE6-6887-48EC-A202-18C7EB428ACD} EndGlobalSection EndGlobal diff --git a/api/net/Areas/Admin/Controllers/NotificationInstanceController.cs b/api/net/Areas/Admin/Controllers/NotificationInstanceController.cs index 976af083da..3e0cfe9ba4 100644 --- a/api/net/Areas/Admin/Controllers/NotificationInstanceController.cs +++ b/api/net/Areas/Admin/Controllers/NotificationInstanceController.cs @@ -80,7 +80,7 @@ public IActionResult FindById(long id) [SwaggerOperation(Tags = new[] { "NotificationInstance" })] public IActionResult Add([FromBody] NotificationInstanceModel model) { - var result = _service.AddAndSave(model.ToEntity(_serializerOptions)); + var result = _service.AddAndSave((Entities.NotificationInstance)model); return CreatedAtAction(nameof(FindById), new { id = result.Id }, new NotificationInstanceModel(result, _serializerOptions)); } @@ -96,7 +96,7 @@ public IActionResult Add([FromBody] NotificationInstanceModel model) [SwaggerOperation(Tags = new[] { "NotificationInstance" })] public async Task UpdateAsync([FromBody] NotificationInstanceModel model) { - var notificationInstance = model.ToEntity(_serializerOptions); + var notificationInstance = (Entities.NotificationInstance)model; // TODO: This shouldn't occur, but if it does we want to know. if (notificationInstance.Notification != null) await _watch.AlertNotificationSubscriptionChangedAsync(notificationInstance.Notification, this.User, "API Admin Notification Instance Controller Update endpoint."); @@ -116,7 +116,7 @@ public async Task UpdateAsync([FromBody] NotificationInstanceMode [SwaggerOperation(Tags = new[] { "NotificationInstance" })] public IActionResult Delete([FromBody] NotificationInstanceModel model) { - _service.DeleteAndSave(model.ToEntity(_serializerOptions)); + _service.DeleteAndSave((Entities.NotificationInstance)model); return new JsonResult(model); } #endregion diff --git a/api/net/Areas/Editor/Controllers/ContentController.cs b/api/net/Areas/Editor/Controllers/ContentController.cs index 35261f6e72..9fa3de8651 100644 --- a/api/net/Areas/Editor/Controllers/ContentController.cs +++ b/api/net/Areas/Editor/Controllers/ContentController.cs @@ -721,7 +721,7 @@ public async Task AttachFileAsync([FromRoute] long contentId, [Fr public IActionResult GetNotificationsFor(long id) { var notifications = _contentService.GetNotificationsFor(id); - return new JsonResult(notifications.Select(n => new NotificationInstanceModel(n, _serializerOptions))); + return new JsonResult(notifications.Select(n => new NotificationInstanceModel(n))); } #endregion #endregion diff --git a/api/net/Areas/Helpers/WatchSubscriptionChange.cs b/api/net/Areas/Helpers/WatchSubscriptionChange.cs index a2a70694d8..e42c0280f9 100644 --- a/api/net/Areas/Helpers/WatchSubscriptionChange.cs +++ b/api/net/Areas/Helpers/WatchSubscriptionChange.cs @@ -1,8 +1,9 @@ +using System.Net.Mail; using System.Security.Claims; using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; -using TNO.Ches; +using MMI.SmtpEmail; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.DAL.Services; @@ -21,8 +22,7 @@ public class WatchSubscriptionChange private readonly IReportService _reportService; private readonly IProductService _productService; private readonly INotificationService _notificationService; - private readonly IChesService _chesService; - private readonly Ches.Configuration.ChesOptions _chesOptions; + private readonly IEmailService _emailService; /// /// SubscriptionChange enum, identifies the type of change. @@ -132,8 +132,7 @@ public class WatchOptions /// /// /// - /// - /// + /// /// /// public WatchSubscriptionChange( @@ -141,8 +140,7 @@ public WatchSubscriptionChange( IReportService reportService, IProductService productService, INotificationService notificationService, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, IOptions watchOptions, ILogger logger) { @@ -150,8 +148,7 @@ public WatchSubscriptionChange( _reportService = reportService; _productService = productService; _notificationService = notificationService; - _chesService = chesService; - _chesOptions = chesOptions.Value; + _emailService = emailService; this.Options = watchOptions.Value; _logger = logger; } @@ -165,7 +162,7 @@ public WatchSubscriptionChange( /// The source of the change, generally the user who made the change. /// The location or other details to help trace the source of the change. /// - public Ches.Models.EmailModel GenerateEmail(SubscriptionChange[] changes, string changeSource, string traceInformation) + public MailMessage GenerateEmail(SubscriptionChange[] changes, string changeSource, string traceInformation) { var body = new StringBuilder($"""
This is an alert to identify changes to user subscriptions.
@@ -234,7 +231,7 @@ public Ches.Models.EmailModel GenerateEmail(SubscriptionChange[] changes, string } var to = this.Options.SendTo.Split(','); - return new Ches.Models.EmailModel(_chesOptions.From, to, "MMI - User Subscription Change Alert", body.ToString()); + return _emailService.CreateMailMessage("MMI - User Subscription Change Alert", body.ToString(), to, null, null, null, isHtml: true); } #region Users @@ -285,7 +282,7 @@ public async Task AlertUserSubscriptionChangedAsync(Entities.User user, string c if (changes.Length > 0) { var email = GenerateEmail(changes, changeSource, traceInformation); - await _chesService.SendEmailAsync(email); + await _emailService.SendAsync(email); } } #endregion @@ -335,7 +332,7 @@ public async Task AlertProductSubscriptionChangedAsync(Entities.Product product, if (changes.Length > 0) { var email = GenerateEmail(changes, changeSource, traceInformation); - await _chesService.SendEmailAsync(email); + await _emailService.SendAsync(email); } } #endregion @@ -385,7 +382,7 @@ public async Task AlertReportSubscriptionChangedAsync(Entities.Report report, st if (changes.Length > 0) { var email = GenerateEmail(changes, changeSource, traceInformation); - await _chesService.SendEmailAsync(email); + await _emailService.SendAsync(email); } } #endregion @@ -435,7 +432,7 @@ public async Task AlertNotificationSubscriptionChangedAsync(Entities.Notificatio if (changes.Length > 0) { var email = GenerateEmail(changes, changeSource, traceInformation); - await _chesService.SendEmailAsync(email); + await _emailService.SendAsync(email); } } #endregion diff --git a/api/net/Areas/Services/Controllers/AVOverviewController.cs b/api/net/Areas/Services/Controllers/AVOverviewController.cs index c64a0ffa08..fc059863c1 100644 --- a/api/net/Areas/Services/Controllers/AVOverviewController.cs +++ b/api/net/Areas/Services/Controllers/AVOverviewController.cs @@ -91,6 +91,23 @@ public IActionResult Update([FromBody] AVOverviewInstanceModel model) return new JsonResult(new AVOverviewInstanceModel(instance, _serializerOptions)); } + /// + /// Get all user report instances for the specified instance 'id'. + /// + /// + /// + /// + [HttpGet("{id}/users/{userId}")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(typeof(UserAVOverviewInstanceModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] + [SwaggerOperation(Tags = new[] { "Evening Overview" })] + public IActionResult GetUserAVOverviewInstanceAsync(long id, int userId) + { + var result = _overviewInstanceService.GetUserAVOverviewInstance(id, userId) ?? throw new NoContentException(); + return new JsonResult(new UserAVOverviewInstanceModel(result)); + } + /// /// Get all user report instances for the specified instance 'id'. /// @@ -103,8 +120,8 @@ public IActionResult Update([FromBody] AVOverviewInstanceModel model) [SwaggerOperation(Tags = new[] { "Evening Overview" })] public IActionResult GetUserAVOverviewInstancesAsync(long id) { - var content = _overviewInstanceService.GetUserAVOverviewInstances(id); - return new JsonResult(content.Select(c => new UserAVOverviewInstanceModel(c))); + var result = _overviewInstanceService.GetUserAVOverviewInstances(id); + return new JsonResult(result.Select(c => new UserAVOverviewInstanceModel(c))); } /// diff --git a/api/net/Areas/Services/Controllers/ContentController.cs b/api/net/Areas/Services/Controllers/ContentController.cs index b79c921c68..6cda6c042e 100644 --- a/api/net/Areas/Services/Controllers/ContentController.cs +++ b/api/net/Areas/Services/Controllers/ContentController.cs @@ -511,7 +511,7 @@ public async Task GetImageFile(long id) public IActionResult GetNotificationsFor(long id) { var notifications = _contentService.GetNotificationsFor(id); - return new JsonResult(notifications.Select(n => new NotificationInstanceModel(n, _serializerOptions))); + return new JsonResult(notifications.Select(n => new NotificationInstanceModel(n))); } /// diff --git a/api/net/Areas/Services/Controllers/NotificationController.cs b/api/net/Areas/Services/Controllers/NotificationController.cs index 95b3e45262..b56c32fcb9 100644 --- a/api/net/Areas/Services/Controllers/NotificationController.cs +++ b/api/net/Areas/Services/Controllers/NotificationController.cs @@ -123,12 +123,12 @@ public async Task FindContentForNotificationIdAsync(int id, int? /// [HttpGet("sent/{status}")] [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] [SwaggerOperation(Tags = new[] { "Notification" })] - public IActionResult GetChesMessages(Entities.NotificationStatus status, [FromQuery] DateTime cutOff) + public IActionResult GetSmtpMessages(Entities.NotificationStatus status, [FromQuery] DateTime cutOff) { - var messages = _service.GetChesMessageIds(status, cutOff); + var messages = _service.GetSmtpMessages(status, cutOff); return new JsonResult(messages); } #endregion diff --git a/api/net/Areas/Services/Controllers/NotificationInstanceController.cs b/api/net/Areas/Services/Controllers/NotificationInstanceController.cs index 487c4c346b..249ab4961a 100644 --- a/api/net/Areas/Services/Controllers/NotificationInstanceController.cs +++ b/api/net/Areas/Services/Controllers/NotificationInstanceController.cs @@ -61,7 +61,7 @@ public IActionResult FindById(long id) { var result = _service.FindById(id); if (result == null) return NoContent(); - return new JsonResult(new NotificationInstanceModel(result, _serializerOptions)); + return new JsonResult(new NotificationInstanceModel(result)); } /// @@ -76,8 +76,8 @@ public IActionResult FindById(long id) [SwaggerOperation(Tags = new[] { "NotificationInstance" })] public IActionResult Add([FromBody] NotificationInstanceModel model) { - var result = _service.AddAndSave(model.ToEntity(_serializerOptions)); - return CreatedAtAction(nameof(FindById), new { id = result.Id }, new NotificationInstanceModel(result, _serializerOptions)); + var result = _service.AddAndSave((Entities.NotificationInstance)model); + return CreatedAtAction(nameof(FindById), new { id = result.Id }, new NotificationInstanceModel(result)); } /// @@ -97,7 +97,7 @@ public IActionResult UpdateNotificationInstanceStatus(long id, Entities.Notifica instance.Status = status; _service.UpdateAndSave(instance); - return new JsonResult(new NotificationInstanceModel(instance, _serializerOptions)); + return new JsonResult(new NotificationInstanceModel(instance)); } #endregion } diff --git a/api/net/Areas/Services/Controllers/ReportController.cs b/api/net/Areas/Services/Controllers/ReportController.cs index daf2c692bb..3c317fb5fd 100644 --- a/api/net/Areas/Services/Controllers/ReportController.cs +++ b/api/net/Areas/Services/Controllers/ReportController.cs @@ -151,12 +151,12 @@ public IActionResult ClearFoldersInReportId(int id) /// [HttpGet("sent/{status}")] [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] [SwaggerOperation(Tags = new[] { "Report" })] - public IActionResult GetChesMessages(Entities.ReportStatus status, [FromQuery] DateTime cutOff) + public IActionResult GetSmtpMessages(Entities.ReportStatus status, [FromQuery] DateTime cutOff) { - var messages = _service.GetChesMessageIds(status, cutOff); + var messages = _service.GetSmtpMessageIds(status, cutOff); return new JsonResult(messages); } #endregion diff --git a/api/net/Areas/Services/Controllers/ReportInstanceController.cs b/api/net/Areas/Services/Controllers/ReportInstanceController.cs index 4857bdf7d1..34d8654d43 100644 --- a/api/net/Areas/Services/Controllers/ReportInstanceController.cs +++ b/api/net/Areas/Services/Controllers/ReportInstanceController.cs @@ -169,8 +169,25 @@ public IActionResult GetContentForReportInstanceIdAsync(long id) [SwaggerOperation(Tags = new[] { "Report" })] public IActionResult GetUserReportInstancesAsync(long id) { - var content = _reportInstanceService.GetUserReportInstances(id); - return new JsonResult(content.Select(c => new UserReportInstanceModel(c))); + var result = _reportInstanceService.GetUserReportInstances(id); + return new JsonResult(result.Select(c => new UserReportInstanceModel(c))); + } + + /// + /// Get the user report instance for the specified instance 'id'. + /// + /// + /// + /// + [HttpGet("{id}/users/{userId}")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(typeof(UserReportInstanceModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] + [SwaggerOperation(Tags = new[] { "Report" })] + public IActionResult GetUserReportInstancesAsync(long id, int userId) + { + var result = _reportInstanceService.GetUserReportInstance(id, userId) ?? throw new NoContentException(); + return new JsonResult(new UserReportInstanceModel(result)); } /// diff --git a/api/net/Areas/Subscriber/Controllers/ReportController.cs b/api/net/Areas/Subscriber/Controllers/ReportController.cs index 14092f7bce..6e8e1bb01a 100644 --- a/api/net/Areas/Subscriber/Controllers/ReportController.cs +++ b/api/net/Areas/Subscriber/Controllers/ReportController.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Mime; using System.Text; @@ -7,14 +5,13 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using Swashbuckle.AspNetCore.Annotations; using TNO.API.Areas.Subscriber.Models.Report; using TNO.API.Config; using TNO.API.Helpers; using TNO.API.Models; using TNO.API.Models.SignalR; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.DAL.Services; @@ -54,8 +51,8 @@ public class ReportController : ControllerBase private readonly JsonSerializerOptions _serializerOptions; private readonly ILogger _logger; private readonly ISettingService _settingService; - private readonly ChesOptions _chesOptions; - private readonly IChesService _ches; + private readonly IEmailService _emailService; + private readonly SmtpOptions _smtpOptions; #endregion #region Constructors @@ -74,8 +71,8 @@ public class ReportController : ControllerBase /// /// /// - /// - /// + /// + /// public ReportController( IReportService reportService, @@ -90,8 +87,8 @@ public ReportController( IOptions serializerOptions, ILogger logger, ISettingService settingService, - IOptions chesOptions, - IChesService ches + IOptions smtpOptions, + IEmailService emailService ) { _reportService = reportService; @@ -106,8 +103,8 @@ IChesService ches _serializerOptions = serializerOptions.Value; _logger = logger; _settingService = settingService; - _chesOptions = chesOptions.Value; - _ches = ches; + _smtpOptions = smtpOptions.Value; + _emailService = emailService; } #endregion @@ -614,10 +611,11 @@ public async Task RequestSubscription(int id, string applicantEma { var emailAddresses = productSubscriptionManagerEmail.Value.Split(new char[] { ';', ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - var email = new TNO.Ches.Models.EmailModel(_chesOptions.From, emailAddresses, subject, message.ToString()); - var emailRequest = await _ches.SendEmailAsync(email); - _logger.LogInformation("report subscription request email to [{email}] queued: {txtId}", productSubscriptionManagerEmail.Value, emailRequest.TransactionId); + var email = _emailService.CreateMailMessage(subject, message.ToString(), emailAddresses, null, null, null, true); + await _emailService.SendAsync(email); + + _logger.LogInformation("report subscription request email to [{email}]", productSubscriptionManagerEmail.Value); } else @@ -678,10 +676,11 @@ public async Task RequestUnsubscription(int id, string applicantE if (productSubscriptionManagerEmail != null) { var emailAddresses = productSubscriptionManagerEmail.Value.Split(new char[] { ';', ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - var email = new TNO.Ches.Models.EmailModel(_chesOptions.From, emailAddresses, subject, message.ToString()); - var emailRequest = await _ches.SendEmailAsync(email); - _logger.LogInformation("Report unsubscription request email to [{email}] queued: {txtId}", productSubscriptionManagerEmail.Value, emailRequest.TransactionId); + var email = _emailService.CreateMailMessage(subject, message.ToString(), emailAddresses, null, null, null, true); + await _emailService.SendAsync(email); + + _logger.LogInformation("Report unsubscription request email to [{email}]", productSubscriptionManagerEmail.Value); } else { diff --git a/api/net/Dockerfile b/api/net/Dockerfile index 3f3e9c6547..d51f7f54fe 100644 --- a/api/net/Dockerfile +++ b/api/net/Dockerfile @@ -18,7 +18,7 @@ COPY api/net/ . COPY libs/net/ /src/libs/net/ RUN dotnet restore -RUN dotnet publish "TNO.API.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish +RUN dotnet publish "TNO.API.csproj" -c "$BUILD_CONFIGURATION" -r linux-x64 --self-contained false -o /app/publish /p:PublishTrimmed=false # Runtime image FROM base AS final diff --git a/api/net/Program.cs b/api/net/Program.cs index d18d73bfec..8b5722d2fb 100644 --- a/api/net/Program.cs +++ b/api/net/Program.cs @@ -15,6 +15,7 @@ using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using MMI.SmtpEmail; using Prometheus; using Swashbuckle.AspNetCore.SwaggerGen; using TNO.API.BackgroundWorkItem; @@ -23,7 +24,6 @@ using TNO.API.Keycloak; using TNO.API.Middleware; using TNO.API.SignalR; -using TNO.Ches; using TNO.Core.Extensions; using TNO.Core.Http; using TNO.Core.Storage; @@ -204,7 +204,6 @@ .AddScoped() .AddScoped() .AddScoped() - .AddChesService(config.GetSection("CHES")) .Configure(config.GetSection("S3")) .AddSingleton() .AddTNOServices(config, env) @@ -215,7 +214,8 @@ .AddTransient() .AddScoped() .AddScoped() - .AddKeycloakService(config.GetSection("Keycloak:ServiceAccount")); + .AddKeycloakService(config.GetSection("Keycloak:ServiceAccount")) + .AddSmtpEmail(config.GetSection("Smtp")); builder.Services.AddApiVersioning(options => { diff --git a/api/net/TNO.API.csproj b/api/net/TNO.API.csproj index 5d7f2bf6ba..856e19665f 100644 --- a/api/net/TNO.API.csproj +++ b/api/net/TNO.API.csproj @@ -15,7 +15,6 @@ - @@ -31,7 +30,6 @@ - @@ -39,6 +37,7 @@ + diff --git a/api/net/appsettings.json b/api/net/appsettings.json index 9a3c58c5ac..634c267640 100644 --- a/api/net/appsettings.json +++ b/api/net/appsettings.json @@ -108,12 +108,10 @@ "ViewContentUrl": "https://mmi.gov.bc.ca/view/", "RequestTranscriptUrl": "https://mmi.gov.bc.ca/transcribe/" }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": true + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Watch": { "IsEnabled": false, diff --git a/app/editor/src/features/admin/notifications/dashboard/NotificationCard.tsx b/app/editor/src/features/admin/notifications/dashboard/NotificationCard.tsx index 43418dfffd..c6835af294 100644 --- a/app/editor/src/features/admin/notifications/dashboard/NotificationCard.tsx +++ b/app/editor/src/features/admin/notifications/dashboard/NotificationCard.tsx @@ -50,7 +50,7 @@ export const NotificationCard: React.FC = ({ instance }) })); }} > -

CHES Response

+

SMTP Response

{expandResponse[instance.id] ? : }
diff --git a/app/editor/src/features/admin/reports/dashboard/ReportCard.tsx b/app/editor/src/features/admin/reports/dashboard/ReportCard.tsx index d75d120792..de1546922b 100644 --- a/app/editor/src/features/admin/reports/dashboard/ReportCard.tsx +++ b/app/editor/src/features/admin/reports/dashboard/ReportCard.tsx @@ -85,7 +85,7 @@ export const ReportCard: React.FC = ({ report: initReport }) = setExpandResponse((value) => ({ ...value, [report.id]: !value[report.id] })); }} > -

CHES Response

+

SMTP Response

{expandResponse[report.id] ? : }
diff --git a/libs/net/ches/TNO.Ches.csproj b/libs/net/ches/TNO.Ches.csproj index 2f76e3cfbe..2fc22f931b 100644 --- a/libs/net/ches/TNO.Ches.csproj +++ b/libs/net/ches/TNO.Ches.csproj @@ -16,16 +16,23 @@ - - - - - + + + + + - + - + + + + + + + + diff --git a/libs/net/dal/Configuration/AVOverviewInstanceConfiguration.cs b/libs/net/dal/Configuration/AVOverviewInstanceConfiguration.cs index 0374ef4b3c..4237f75d8e 100644 --- a/libs/net/dal/Configuration/AVOverviewInstanceConfiguration.cs +++ b/libs/net/dal/Configuration/AVOverviewInstanceConfiguration.cs @@ -13,6 +13,9 @@ public override void Configure(EntityTypeBuilder builder) builder.Property(m => m.TemplateType).IsRequired(); builder.Property(m => m.PublishedOn).IsRequired(); builder.Property(m => m.IsPublished).IsRequired(); + builder.Property(m => m.Status).IsRequired(); + builder.Property(m => m.Subject).IsRequired().HasColumnType("text"); + builder.Property(m => m.Body).IsRequired().HasColumnType("text"); builder.Property(m => m.Response).HasColumnType("jsonb").HasDefaultValueSql("'{}'::jsonb"); builder.HasOne(m => m.Template).WithMany(m => m.Instances).HasForeignKey(m => m.TemplateType).OnDelete(DeleteBehavior.Cascade); diff --git a/libs/net/dal/Configuration/ReportInstanceConfiguration.cs b/libs/net/dal/Configuration/ReportInstanceConfiguration.cs index a7e97b0861..dbf1a765de 100644 --- a/libs/net/dal/Configuration/ReportInstanceConfiguration.cs +++ b/libs/net/dal/Configuration/ReportInstanceConfiguration.cs @@ -17,6 +17,7 @@ public override void Configure(EntityTypeBuilder builder) builder.Property(m => m.Status).IsRequired(); builder.Property(m => m.Subject).IsRequired().HasColumnType("text"); builder.Property(m => m.Body).IsRequired().HasColumnType("text"); + builder.Property(m => m.LinkOnlyBody).IsRequired().HasColumnType("text"); builder.Property(m => m.Response).IsRequired().HasColumnType("jsonb").HasDefaultValueSql("'{}'::jsonb"); builder.HasOne(m => m.Report).WithMany(m => m.Instances).HasForeignKey(m => m.ReportId).OnDelete(DeleteBehavior.Cascade); diff --git a/libs/net/dal/Migrations/20260220195903_1.5.Designer.cs b/libs/net/dal/Migrations/20260220195903_1.5.Designer.cs new file mode 100644 index 0000000000..4deb640f82 --- /dev/null +++ b/libs/net/dal/Migrations/20260220195903_1.5.Designer.cs @@ -0,0 +1,7840 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TNO.DAL; +using TNO.Entities.Models; + +#nullable disable + +namespace TNO.DAL.Migrations +{ + [DbContext(typeof(TNOContext))] + [Migration("20260220195903_1.5")] + partial class _15 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TNO.Entities.AVOverviewInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.Property("Response") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("response") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text") + .HasColumnName("subject"); + + b.Property("TemplateType") + .HasColumnType("integer") + .HasColumnName("template_type"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("PublishedOn") + .IsUnique(); + + b.HasIndex("TemplateType", "PublishedOn"); + + b.ToTable("av_overview_instance"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Anchors") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("anchors"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("InstanceId") + .HasColumnType("bigint") + .HasColumnName("av_overview_instance_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OtherSource") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("other_source"); + + b.Property("SeriesId") + .HasColumnType("integer") + .HasColumnName("series_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("StartTime") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasColumnName("start_time"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("SourceId"); + + b.ToTable("av_overview_section"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewSectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ItemType") + .HasColumnType("integer") + .HasColumnName("item_type"); + + b.Property("SectionId") + .HasColumnType("integer") + .HasColumnName("av_overview_section_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("summary"); + + b.Property("Time") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasColumnName("time"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.HasIndex("SectionId"); + + b.ToTable("av_overview_section_item"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewTemplate", b => + { + b.Property("TemplateType") + .HasColumnType("integer") + .HasColumnName("template_type"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("TemplateId") + .HasColumnType("integer") + .HasColumnName("report_template_id"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("TemplateType"); + + b.HasIndex("TemplateId"); + + b.ToTable("av_overview_template"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewTemplateSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Anchors") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("anchors"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OtherSource") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("other_source"); + + b.Property("SeriesId") + .HasColumnType("integer") + .HasColumnName("series_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("StartTime") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasColumnName("start_time"); + + b.Property("TemplateType") + .HasColumnType("integer") + .HasColumnName("av_overview_template_id"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("SourceId"); + + b.HasIndex("TemplateType"); + + b.ToTable("av_overview_template_section"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewTemplateSectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ItemType") + .HasColumnType("integer") + .HasColumnName("item_type"); + + b.Property("SectionId") + .HasColumnType("integer") + .HasColumnName("av_overview_template_section_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("summary"); + + b.Property("Time") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasColumnName("time"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("av_overview_template_section_item"); + }); + + modelBuilder.Entity("TNO.Entities.Action", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DefaultValue") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("default_value") + .HasDefaultValueSql("''"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ValueLabel") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("value_label") + .HasDefaultValueSql("''"); + + b.Property("ValueType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("value_type"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ValueType", "ValueLabel" }, "IX_action"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_action_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_name") + .IsUnique(); + + b.ToTable("action"); + }); + + modelBuilder.Entity("TNO.Entities.CBRAReportStaffSummary", b => + { + b.Property("CbraHours") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cbra_hours"); + + b.Property("Staff") + .IsRequired() + .HasColumnType("text") + .HasColumnName("staff"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("TNO.Entities.CBRAReportTotalEntries", b => + { + b.Property("DayOfWeek") + .IsRequired() + .HasColumnType("text") + .HasColumnName("day_of_week"); + + b.Property("TotalCbra") + .HasColumnType("numeric") + .HasColumnName("total_cbra"); + + b.Property("TotalCount") + .HasColumnType("numeric") + .HasColumnName("total_count"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("TNO.Entities.CBRAReportTotalExcerpts", b => + { + b.Property("Category") + .IsRequired() + .HasColumnType("text") + .HasColumnName("category"); + + b.Property("Totals") + .HasColumnType("integer") + .HasColumnName("totals"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("TNO.Entities.CBRAReportTotalsByBroadcaster", b => + { + b.Property("PercentageOfTotalRunningTime") + .HasColumnType("numeric") + .HasColumnName("percentage_of_total_running_time"); + + b.Property("SourceType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("source_type"); + + b.Property("TotalRunningTime") + .IsRequired() + .HasColumnType("text") + .HasColumnName("total_running_time"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("TNO.Entities.CBRAReportTotalsByProgram", b => + { + b.Property("MediaType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("media_type"); + + b.Property("PercentageOfTotalRunningTime") + .HasColumnType("numeric") + .HasColumnName("percentage_of_total_running_time"); + + b.Property("Series") + .IsRequired() + .HasColumnType("text") + .HasColumnName("series"); + + b.Property("SourceType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("source_type"); + + b.Property("TotalCount") + .HasColumnType("numeric") + .HasColumnName("total_count"); + + b.Property("TotalRunningTime") + .IsRequired() + .HasColumnType("text") + .HasColumnName("total_running_time"); + + b.ToTable((string)null); + + b.ToView(null, (string)null); + }); + + modelBuilder.Entity("TNO.Entities.Cache", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("key"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Key"); + + b.HasIndex(new[] { "Key", "Value" }, "IX_cache"); + + b.ToTable("cache"); + }); + + modelBuilder.Entity("TNO.Entities.ChartTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("Template") + .IsRequired() + .HasColumnType("text") + .HasColumnName("template"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_charttemplate_is_enabled"); + + b.ToTable("chart_template"); + }); + + modelBuilder.Entity("TNO.Entities.Connection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Configuration") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ConnectionType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("connection_type"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsReadOnly") + .HasColumnType("boolean") + .HasColumnName("is_read_only"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_connection_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_name") + .IsUnique() + .HasDatabaseName("IX_name1"); + + b.ToTable("connection"); + }); + + modelBuilder.Entity("TNO.Entities.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("Byline") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("byline"); + + b.Property("ContentType") + .HasColumnType("integer") + .HasColumnName("content_type"); + + b.Property("ContributorId") + .HasColumnType("integer") + .HasColumnName("contributor_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Edition") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("edition"); + + b.Property("ExternalUid") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasDefaultValue("") + .HasColumnName("external_uid"); + + b.Property("Headline") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("headline"); + + b.Property("IngestTypeId") + .HasColumnType("integer"); + + b.Property("IsApproved") + .HasColumnType("boolean") + .HasColumnName("is_approved"); + + b.Property("IsHidden") + .HasColumnType("boolean") + .HasColumnName("is_hidden"); + + b.Property("IsPrivate") + .HasColumnType("boolean") + .HasColumnName("is_private"); + + b.Property("LicenseId") + .HasColumnType("integer") + .HasColumnName("license_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.Property("OtherSource") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("Page") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("page"); + + b.Property("PostedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("posted_on"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.Property("Section") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("section"); + + b.Property("SeriesId") + .HasColumnType("integer") + .HasColumnName("series_id"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("SourceUrl") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("source_url"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("text") + .HasColumnName("summary"); + + b.Property("Uid") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("uid"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.Property>("Versions") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("versions") + .HasDefaultValueSql("'{}'::jsonb"); + + b.HasKey("Id"); + + b.HasIndex("ContributorId"); + + b.HasIndex("IngestTypeId"); + + b.HasIndex("LicenseId"); + + b.HasIndex("MediaTypeId"); + + b.HasIndex("OwnerId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("SourceId"); + + b.HasIndex(new[] { "ContentType", "OtherSource", "Uid", "Page", "Status", "IsHidden" }, "IX_content"); + + b.HasIndex(new[] { "PublishedOn", "CreatedOn" }, "IX_content_dates"); + + b.HasIndex(new[] { "PublishedOn" }, "IX_content_published_on") + .IsDescending(); + + b.HasIndex(new[] { "PublishedOn", "Status" }, "IX_content_published_on_status") + .IsDescending(true, false); + + b.HasIndex(new[] { "Headline" }, "IX_headline"); + + b.HasIndex(new[] { "Edition", "Section", "Byline" }, "IX_print_content"); + + b.ToTable("content"); + }); + + modelBuilder.Entity("TNO.Entities.ContentAction", b => + { + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("ActionId") + .HasColumnType("integer") + .HasColumnName("action_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("ContentId", "ActionId"); + + b.HasIndex("ActionId"); + + b.ToTable("content_action"); + }); + + modelBuilder.Entity("TNO.Entities.ContentLabel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("key"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.HasIndex(new[] { "Key", "Value" }, "IX_content_label"); + + b.ToTable("content_label"); + }); + + modelBuilder.Entity("TNO.Entities.ContentLink", b => + { + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("LinkId") + .HasColumnType("bigint") + .HasColumnName("link_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("ContentId", "LinkId"); + + b.HasIndex("LinkId"); + + b.ToTable("content_link"); + }); + + modelBuilder.Entity("TNO.Entities.ContentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("message"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.ToTable("content_log"); + }); + + modelBuilder.Entity("TNO.Entities.ContentReference", b => + { + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("source"); + + b.Property("Uid") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("uid"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Metadata") + .HasColumnType("jsonb") + .HasColumnName("metadata"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.Property("SourceUpdateOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("source_updated_on"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Topic") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("topic"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Source", "Uid"); + + b.HasIndex(new[] { "PublishedOn", "Status" }, "IX_content_reference"); + + b.HasIndex(new[] { "Source", "Uid" }, "IX_source_uid"); + + b.ToTable("content_reference"); + }); + + modelBuilder.Entity("TNO.Entities.ContentTag", b => + { + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("TagId") + .HasColumnType("integer") + .HasColumnName("tag_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("ContentId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("content_tag"); + }); + + modelBuilder.Entity("TNO.Entities.ContentTonePool", b => + { + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("TonePoolId") + .HasColumnType("integer") + .HasColumnName("tone_pool_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Value") + .HasColumnType("integer") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("ContentId", "TonePoolId"); + + b.HasIndex("TonePoolId"); + + b.ToTable("content_tone"); + }); + + modelBuilder.Entity("TNO.Entities.ContentTopic", b => + { + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("TopicId") + .HasColumnType("integer") + .HasColumnName("topic_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("ContentId", "TopicId"); + + b.HasIndex("TopicId"); + + b.ToTable("content_topic"); + }); + + modelBuilder.Entity("TNO.Entities.ContentTypeAction", b => + { + b.Property("ContentType") + .HasColumnType("integer") + .HasColumnName("content_type"); + + b.Property("ActionId") + .HasColumnType("integer") + .HasColumnName("action_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("ContentType", "ActionId"); + + b.HasIndex("ActionId"); + + b.ToTable("content_type_action"); + }); + + modelBuilder.Entity("TNO.Entities.Contributor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Aliases") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("aliases"); + + b.Property("AutoTranscribe") + .HasColumnType("boolean") + .HasColumnName("auto_transcribe"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsPress") + .HasColumnType("boolean") + .HasColumnName("is_press"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("SourceId"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_contributor_is_enabled"); + + b.ToTable("contributor"); + }); + + modelBuilder.Entity("TNO.Entities.DataLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ConnectionId"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_datalocation_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_name") + .IsUnique() + .HasDatabaseName("IX_name2"); + + b.ToTable("data_location"); + }); + + modelBuilder.Entity("TNO.Entities.EarnedMedia", b => + { + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("ContentType") + .HasColumnType("integer") + .HasColumnName("content_type"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LengthOfContent") + .HasColumnType("integer") + .HasColumnName("length_of_content"); + + b.Property("Rate") + .HasColumnType("real") + .HasColumnName("rate"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("SourceId", "ContentType"); + + b.ToTable("earned_media"); + }); + + modelBuilder.Entity("TNO.Entities.EventSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("EventType") + .HasColumnType("integer") + .HasColumnName("event_type"); + + b.Property("FolderId") + .HasColumnType("integer") + .HasColumnName("folder_id"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("LastRanOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_ran_on"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("NotificationId") + .HasColumnType("integer") + .HasColumnName("notification_id"); + + b.Property("ReportId") + .HasColumnType("integer") + .HasColumnName("report_id"); + + b.Property("RequestSentOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("request_sent_on"); + + b.Property("ScheduleId") + .HasColumnType("integer") + .HasColumnName("schedule_id"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("FolderId"); + + b.HasIndex("NotificationId"); + + b.HasIndex("ReportId"); + + b.HasIndex("ScheduleId"); + + b.ToTable("event_schedule"); + }); + + modelBuilder.Entity("TNO.Entities.FileReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("content_type"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("file_name"); + + b.Property("IsSyncedToS3") + .HasColumnType("boolean") + .HasColumnName("is_synced_to_s3"); + + b.Property("IsUploaded") + .HasColumnType("boolean") + .HasColumnName("is_uploaded"); + + b.Property("LastSyncedToS3On") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced_to_s3_on"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("path"); + + b.Property("RunningTime") + .HasColumnType("bigint") + .HasColumnName("running_time"); + + b.Property("S3Path") + .HasColumnType("text") + .HasColumnName("s3_path"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.ToTable("file_reference"); + }); + + modelBuilder.Entity("TNO.Entities.Filter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("Query") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("query") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId", "Name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_filter_is_enabled"); + + b.ToTable("filter"); + }); + + modelBuilder.Entity("TNO.Entities.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("FilterId") + .HasColumnType("integer") + .HasColumnName("filter_id"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("FilterId"); + + b.HasIndex("OwnerId", "Name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_folder_is_enabled"); + + b.ToTable("folder"); + }); + + modelBuilder.Entity("TNO.Entities.FolderContent", b => + { + b.Property("FolderId") + .HasColumnType("integer") + .HasColumnName("folder_id"); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("FolderId", "ContentId"); + + b.HasIndex("ContentId"); + + b.ToTable("folder_content"); + }); + + modelBuilder.Entity("TNO.Entities.Ingest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Configuration") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("DestinationConnectionId") + .HasColumnType("integer") + .HasColumnName("destination_connection_id"); + + b.Property("IngestTypeId") + .HasColumnType("integer") + .HasColumnName("ingest_type_id"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("name"); + + b.Property("ResetRetryAfterDelayMs") + .HasColumnType("integer") + .HasColumnName("reset_retry_after_delay_ms"); + + b.Property("RetryLimit") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(3) + .HasColumnName("retry_limit"); + + b.Property("ScheduleType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("schedule_type"); + + b.Property("SourceConnectionId") + .HasColumnType("integer") + .HasColumnName("source_connection_id"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("Topic") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("topic"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("DestinationConnectionId"); + + b.HasIndex("MediaTypeId"); + + b.HasIndex("SourceConnectionId"); + + b.HasIndex("SourceId"); + + b.HasIndex(new[] { "IngestTypeId", "SourceId", "Topic" }, "IX_ingest"); + + b.HasIndex(new[] { "Name" }, "IX_name") + .IsUnique() + .HasDatabaseName("IX_name3"); + + b.ToTable("ingest"); + }); + + modelBuilder.Entity("TNO.Entities.IngestDataLocation", b => + { + b.Property("IngestId") + .HasColumnType("integer") + .HasColumnName("ingest_id"); + + b.Property("DataLocationId") + .HasColumnType("integer") + .HasColumnName("data_location_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("IngestId", "DataLocationId"); + + b.HasIndex("DataLocationId"); + + b.ToTable("ingest_data_location"); + }); + + modelBuilder.Entity("TNO.Entities.IngestSchedule", b => + { + b.Property("IngestId") + .HasColumnType("integer") + .HasColumnName("ingest_id"); + + b.Property("ScheduleId") + .HasColumnType("integer") + .HasColumnName("schedule_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("IngestId", "ScheduleId"); + + b.HasIndex("ScheduleId"); + + b.ToTable("ingest_schedule"); + }); + + modelBuilder.Entity("TNO.Entities.IngestState", b => + { + b.Property("IngestId") + .HasColumnType("integer") + .HasColumnName("ingest_id"); + + b.Property("CreationDateOfLastItem") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_date_of_last_item"); + + b.Property("FailedAttempts") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("failed_attempts"); + + b.Property("LastRanOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_ran_on"); + + b.HasKey("IngestId"); + + b.ToTable("ingest_state"); + }); + + modelBuilder.Entity("TNO.Entities.IngestType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoTranscribe") + .HasColumnType("boolean") + .HasColumnName("auto_transcribe"); + + b.Property("ContentType") + .HasColumnType("integer") + .HasColumnName("content_type"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("DisableTranscribe") + .HasColumnType("boolean") + .HasColumnName("disable_transcribe"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_ingesttype_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_name") + .IsUnique() + .HasDatabaseName("IX_name4"); + + b.ToTable("ingest_type"); + }); + + modelBuilder.Entity("TNO.Entities.License", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("TTL") + .HasColumnType("integer") + .HasColumnName("ttl"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_license_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_name") + .IsUnique() + .HasDatabaseName("IX_name5"); + + b.ToTable("license"); + }); + + modelBuilder.Entity("TNO.Entities.MediaAnalytics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgeGroup1") + .HasColumnType("real") + .HasColumnName("age_group1"); + + b.Property("AgeGroup1Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("age_group1_label"); + + b.Property("AgeGroup2") + .HasColumnType("real") + .HasColumnName("age_group2"); + + b.Property("AgeGroup2Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("age_group2_label"); + + b.Property("AgeGroup3") + .HasColumnType("real") + .HasColumnName("age_group3"); + + b.Property("AgeGroup3Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("age_group3_label"); + + b.Property("AgeGroup4") + .HasColumnType("real") + .HasColumnName("age_group4"); + + b.Property("AgeGroup4Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("age_group4_label"); + + b.Property("AverageViews") + .HasColumnType("real") + .HasColumnName("average_views"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("MaleViewers") + .HasColumnType("real") + .HasColumnName("male_viewers"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("PageViews1") + .HasColumnType("real") + .HasColumnName("page_views1"); + + b.Property("PageViews1Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("page_views1_label"); + + b.Property("PageViews2") + .HasColumnType("real") + .HasColumnName("page_views2"); + + b.Property("PageViews3") + .HasColumnType("real") + .HasColumnName("page_views3"); + + b.Property("PageViews4") + .HasColumnType("real") + .HasColumnName("page_views4"); + + b.Property("Page_Views2_Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("page_views2_label"); + + b.Property("Page_Views3_Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("page_views3_label"); + + b.Property("Page_Views4_Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("page_views4_label"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("TotalViews") + .HasColumnType("integer") + .HasColumnName("total_views"); + + b.Property("UniqueViews") + .HasColumnType("integer") + .HasColumnName("unique_views"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.Property("WatchTime1") + .HasColumnType("real") + .HasColumnName("watch_time1"); + + b.Property("WatchTime1Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("watch_time1_label"); + + b.Property("WatchTime2") + .HasColumnType("real") + .HasColumnName("watch_time2"); + + b.Property("WatchTime2Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("watch_time2_label"); + + b.Property("WatchTime3") + .HasColumnType("real") + .HasColumnName("watch_time3"); + + b.Property("WatchTime3Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("watch_time3_label"); + + b.Property("WatchTime4") + .HasColumnType("real") + .HasColumnName("watch_time4"); + + b.Property("WatchTime4Label") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("watch_time4_label"); + + b.HasKey("Id"); + + b.HasIndex("MediaTypeId"); + + b.HasIndex("SourceId"); + + b.HasIndex("PublishedOn", "SourceId", "MediaTypeId") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_mediaanalytics_is_enabled"); + + b.ToTable("media_analytics"); + }); + + modelBuilder.Entity("TNO.Entities.MediaType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoTranscribe") + .HasColumnType("boolean") + .HasColumnName("auto_transcribe"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("ListOption") + .HasColumnType("integer") + .HasColumnName("list_option"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_mediatype_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_name") + .IsUnique() + .HasDatabaseName("IX_name6"); + + b.ToTable("media_type"); + }); + + modelBuilder.Entity("TNO.Entities.Metric", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_metric_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_name") + .IsUnique() + .HasDatabaseName("IX_name7"); + + b.ToTable("metric"); + }); + + modelBuilder.Entity("TNO.Entities.Minister", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Aliases") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("aliases") + .HasDefaultValueSql("''"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("OrganizationId") + .HasColumnType("integer") + .HasColumnName("organization_id"); + + b.Property("Position") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("position") + .HasDefaultValueSql("''"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_minister_is_enabled"); + + b.ToTable("minister"); + }); + + modelBuilder.Entity("TNO.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlertOnIndex") + .HasColumnType("boolean") + .HasColumnName("alert_on_index"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("NotificationType") + .HasColumnType("integer") + .HasColumnName("notification_type"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("Query") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("query") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("Resend") + .HasColumnType("integer") + .HasColumnName("resend"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("TemplateId") + .HasColumnType("integer") + .HasColumnName("notification_template_id"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId"); + + b.HasIndex("OwnerId", "Name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_notification_is_enabled"); + + b.ToTable("notification"); + }); + + modelBuilder.Entity("TNO.Entities.NotificationInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("NotificationId") + .HasColumnType("integer") + .HasColumnName("notification_id"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("Response") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("response") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SentOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text") + .HasColumnName("subject"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.HasIndex("NotificationId"); + + b.HasIndex("Status", "SentOn"); + + b.ToTable("notification_instance"); + }); + + modelBuilder.Entity("TNO.Entities.NotificationTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text") + .HasColumnName("subject"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("IsPublic", "IsEnabled"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_notificationtemplate_is_enabled"); + + b.ToTable("notification_template"); + }); + + modelBuilder.Entity("TNO.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("ParentId") + .HasColumnType("integer") + .HasColumnName("parent_id"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ParentId", "Name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_organization_is_enabled"); + + b.ToTable("organization"); + }); + + modelBuilder.Entity("TNO.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("ProductType") + .HasColumnType("integer") + .HasColumnName("product_type"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("TargetProductId") + .HasColumnType("integer") + .HasColumnName("target_product_id"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("Name", "TargetProductId", "ProductType") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_product_is_enabled"); + + b.ToTable("product"); + }); + + modelBuilder.Entity("TNO.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Byline") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("byline"); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsRelevant") + .HasColumnType("boolean") + .HasColumnName("is_relevant"); + + b.Property("Statement") + .IsRequired() + .HasColumnType("text") + .HasColumnName("statement"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.HasIndex(new[] { "Statement" }, "IX_statement"); + + b.ToTable("quote"); + }); + + modelBuilder.Entity("TNO.Entities.Report", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("TemplateId") + .HasColumnType("integer") + .HasColumnName("report_template_id"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId"); + + b.HasIndex("OwnerId", "Name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_report_is_enabled"); + + b.ToTable("report"); + }); + + modelBuilder.Entity("TNO.Entities.ReportInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LinkOnlyBody") + .IsRequired() + .HasColumnType("text") + .HasColumnName("link_body"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("PublishedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_on"); + + b.Property("ReportId") + .HasColumnType("integer") + .HasColumnName("report_id"); + + b.Property("Response") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("response") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SentOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text") + .HasColumnName("subject"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ReportId"); + + b.HasIndex(new[] { "PublishedOn", "CreatedOn" }, "IX_report_dates"); + + b.ToTable("report_instance"); + }); + + modelBuilder.Entity("TNO.Entities.ReportInstanceContent", b => + { + b.Property("InstanceId") + .HasColumnType("bigint") + .HasColumnName("report_instance_id"); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("SectionName") + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("section_name") + .HasDefaultValueSql("''"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("InstanceId", "ContentId", "SectionName"); + + b.HasIndex("ContentId"); + + b.ToTable("report_instance_content"); + }); + + modelBuilder.Entity("TNO.Entities.ReportSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("FilterId") + .HasColumnType("integer") + .HasColumnName("filter_id"); + + b.Property("FolderId") + .HasColumnType("integer") + .HasColumnName("folder_id"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("LinkedReportId") + .HasColumnType("integer") + .HasColumnName("linked_report_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("ReportId") + .HasColumnType("integer") + .HasColumnName("report_id"); + + b.Property("SectionType") + .HasColumnType("integer") + .HasColumnName("section_type"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("FilterId"); + + b.HasIndex("FolderId"); + + b.HasIndex("LinkedReportId"); + + b.HasIndex("ReportId", "Name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_reportsection_is_enabled"); + + b.ToTable("report_section"); + }); + + modelBuilder.Entity("TNO.Entities.ReportSectionChartTemplate", b => + { + b.Property("ReportSectionId") + .HasColumnType("integer") + .HasColumnName("report_section_id"); + + b.Property("ChartTemplateId") + .HasColumnType("integer") + .HasColumnName("chart_template_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("ReportSectionId", "ChartTemplateId"); + + b.HasIndex("ChartTemplateId"); + + b.ToTable("report_section_chart_template"); + }); + + modelBuilder.Entity("TNO.Entities.ReportTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("ReportType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("report_type") + .HasDefaultValueSql("0"); + + b.Property("Settings") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("settings") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text") + .HasColumnName("subject"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("IsPublic", "IsEnabled"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_reporttemplate_is_enabled"); + + b.ToTable("report_template"); + }); + + modelBuilder.Entity("TNO.Entities.ReportTemplateChartTemplate", b => + { + b.Property("ReportTemplateId") + .HasColumnType("integer") + .HasColumnName("report_template_id"); + + b.Property("ChartTemplateId") + .HasColumnType("integer") + .HasColumnName("chart_template_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("ReportTemplateId", "ChartTemplateId"); + + b.HasIndex("ChartTemplateId"); + + b.ToTable("report_template_chart_template"); + }); + + modelBuilder.Entity("TNO.Entities.Schedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DayOfMonth") + .HasColumnType("integer") + .HasColumnName("day_of_month"); + + b.Property("DelayMS") + .HasColumnType("integer") + .HasColumnName("delay_ms"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Repeat") + .HasColumnType("boolean") + .HasColumnName("repeat"); + + b.Property("RequestedById") + .HasColumnType("integer") + .HasColumnName("requested_by_id"); + + b.Property("RunOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("run_on"); + + b.Property("RunOnMonths") + .HasColumnType("integer") + .HasColumnName("run_on_months"); + + b.Property("RunOnWeekDays") + .HasColumnType("integer") + .HasColumnName("run_on_week_days"); + + b.Property("RunOnlyOnce") + .HasColumnType("boolean") + .HasColumnName("run_only_once"); + + b.Property("StartAt") + .HasColumnType("interval") + .HasColumnName("start_at"); + + b.Property("StopAt") + .HasColumnType("interval") + .HasColumnName("stop_at"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("RequestedById"); + + b.HasIndex(new[] { "Name", "IsEnabled" }, "IX_schedule"); + + b.ToTable("schedule"); + }); + + modelBuilder.Entity("TNO.Entities.Sentiment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("Rate") + .HasColumnType("real") + .HasColumnName("rate"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Value") + .HasColumnType("real") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_sentiment_is_enabled"); + + b.ToTable("sentiment"); + }); + + modelBuilder.Entity("TNO.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoTranscribe") + .HasColumnType("boolean") + .HasColumnName("auto_transcribe"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsCBRASource") + .HasColumnType("boolean") + .HasColumnName("is_cbra_source"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsOther") + .HasColumnType("boolean") + .HasColumnName("is_other"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UseInTopics") + .HasColumnType("boolean") + .HasColumnName("use_in_topics"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("SourceId"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_series_is_enabled"); + + b.ToTable("series"); + }); + + modelBuilder.Entity("TNO.Entities.SeriesMediaTypeSearchMapping", b => + { + b.Property("SeriesId") + .HasColumnType("integer") + .HasColumnName("series_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("SeriesId", "MediaTypeId"); + + b.HasIndex("MediaTypeId"); + + b.ToTable("series_media_type_search_mapping"); + }); + + modelBuilder.Entity("TNO.Entities.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Value") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("value") + .HasDefaultValueSql("''"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_setting_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_setting_name") + .IsUnique(); + + b.ToTable("setting"); + }); + + modelBuilder.Entity("TNO.Entities.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoTranscribe") + .HasColumnType("boolean") + .HasColumnName("auto_transcribe"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("code"); + + b.Property("Configuration") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("DisableTranscribe") + .HasColumnType("boolean") + .HasColumnName("disable_transcribe"); + + b.Property("IsCBRASource") + .HasColumnType("boolean") + .HasColumnName("is_cbra_source"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("LicenseId") + .HasColumnType("integer") + .HasColumnName("license_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("ShortName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("short_name") + .HasDefaultValueSql("''"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UseInTopics") + .HasColumnType("boolean") + .HasColumnName("use_in_topics"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("LicenseId"); + + b.HasIndex("MediaTypeId"); + + b.HasIndex("OwnerId"); + + b.HasIndex(new[] { "Code" }, "IX_source_code") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_source_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_source_name") + .IsUnique(); + + b.ToTable("source"); + }); + + modelBuilder.Entity("TNO.Entities.SourceMediaTypeSearchMapping", b => + { + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("SourceId", "MediaTypeId"); + + b.HasIndex("MediaTypeId"); + + b.ToTable("source_media_type_search_mapping"); + }); + + modelBuilder.Entity("TNO.Entities.SourceMetric", b => + { + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("MetricId") + .HasColumnType("integer") + .HasColumnName("metric_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Earned") + .ValueGeneratedOnAdd() + .HasColumnType("real") + .HasDefaultValue(0f) + .HasColumnName("earned"); + + b.Property("Impression") + .ValueGeneratedOnAdd() + .HasColumnType("real") + .HasDefaultValue(0f) + .HasColumnName("impression"); + + b.Property("Reach") + .ValueGeneratedOnAdd() + .HasColumnType("real") + .HasDefaultValue(0f) + .HasColumnName("reach"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("SourceId", "MetricId"); + + b.HasIndex("MetricId"); + + b.ToTable("source_metric"); + }); + + modelBuilder.Entity("TNO.Entities.SystemMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.Property("Version") + .HasColumnType("bigint") + .HasColumnName("version"); + + b.HasKey("Id"); + + b.ToTable("system_message"); + }); + + modelBuilder.Entity("TNO.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)") + .HasColumnName("code"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Code" }, "IX_tag_code") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_tag_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_tag_name") + .IsUnique(); + + b.ToTable("tag"); + }); + + modelBuilder.Entity("TNO.Entities.TimeTracking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Activity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("activity"); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Effort") + .HasColumnType("real") + .HasColumnName("effort"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.HasIndex("UserId"); + + b.ToTable("time_tracking"); + }); + + modelBuilder.Entity("TNO.Entities.TonePool", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "OwnerId", "Name" }, "IX_tone_pool_name") + .IsUnique(); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_tonepool_is_enabled"); + + b.ToTable("tone_pool"); + }); + + modelBuilder.Entity("TNO.Entities.Topic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("TopicType") + .HasColumnType("integer") + .HasColumnName("topic_type"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsEnabled", "Name" }, "IX_topic_is_enabled"); + + b.HasIndex(new[] { "Name" }, "IX_topic_name") + .IsUnique(); + + b.ToTable("topic"); + }); + + modelBuilder.Entity("TNO.Entities.TopicScoreRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CharacterMax") + .HasColumnType("integer") + .HasColumnName("char_max"); + + b.Property("CharacterMin") + .HasColumnType("integer") + .HasColumnName("char_min"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("HasImage") + .HasColumnType("boolean") + .HasColumnName("has_image"); + + b.Property("PageMax") + .HasMaxLength(5) + .HasColumnType("character varying(5)") + .HasColumnName("page_max"); + + b.Property("PageMin") + .HasMaxLength(5) + .HasColumnType("character varying(5)") + .HasColumnName("page_min"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("Section") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("section"); + + b.Property("SeriesId") + .HasColumnType("integer") + .HasColumnName("series_id"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasColumnName("sort_order"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("TimeMax") + .HasColumnType("interval") + .HasColumnName("time_max"); + + b.Property("TimeMin") + .HasColumnType("interval") + .HasColumnName("time_min"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex(new[] { "SourceId", "SeriesId", "Section" }, "IX_source_id_series_id_section"); + + b.ToTable("topic_score_rule"); + }); + + modelBuilder.Entity("TNO.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("account_type"); + + b.Property("Code") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasColumnName("code") + .HasDefaultValueSql("''"); + + b.Property("CodeCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("code_created_on"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DisplayName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("display_name") + .HasDefaultValueSql("''"); + + b.Property("Email") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("email") + .HasDefaultValueSql("''"); + + b.Property("EmailVerified") + .HasColumnType("boolean") + .HasColumnName("email_verified"); + + b.Property("FirstName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("first_name") + .HasDefaultValueSql("''"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is_enabled"); + + b.Property("IsSystemAccount") + .HasColumnType("boolean") + .HasColumnName("is_system_account"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("key"); + + b.Property("LastLoginOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_login_on"); + + b.Property("LastName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("last_name") + .HasDefaultValueSql("''"); + + b.Property("Note") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("note") + .HasDefaultValueSql("''"); + + b.Property("Preferences") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("preferences") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("PreferredEmail") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("preferred_email") + .HasDefaultValueSql("''"); + + b.Property("Roles") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("roles") + .HasDefaultValueSql("''"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("status"); + + b.Property("UniqueLogins") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unique_logins") + .HasDefaultValueSql("0"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("username"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Email" }, "IX_email"); + + b.HasIndex(new[] { "Key" }, "IX_key") + .IsUnique(); + + b.HasIndex(new[] { "LastName", "FirstName" }, "IX_last_first_name"); + + b.HasIndex(new[] { "Username" }, "IX_username") + .IsUnique(); + + b.ToTable("user"); + }); + + modelBuilder.Entity("TNO.Entities.UserAVOverview", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("TemplateType") + .HasColumnType("integer") + .HasColumnName("av_overview_template_type"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsSubscribed") + .HasColumnType("boolean") + .HasColumnName("is_subscribed"); + + b.Property("SendTo") + .HasColumnType("integer") + .HasColumnName("send_to"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "TemplateType"); + + b.HasIndex("TemplateType"); + + b.ToTable("user_av_overview"); + }); + + modelBuilder.Entity("TNO.Entities.UserAVOverviewInstance", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("InstanceId") + .HasColumnType("bigint") + .HasColumnName("report_instance_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Response") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("response") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("SentOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("sent_on"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "InstanceId"); + + b.HasIndex("InstanceId"); + + b.HasIndex(new[] { "SentOn", "Status" }, "IX_user_av_overview_instance"); + + b.ToTable("user_av_overview_instance"); + }); + + modelBuilder.Entity("TNO.Entities.UserColleague", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("ColleagueId") + .HasColumnType("integer") + .HasColumnName("colleague_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "ColleagueId"); + + b.HasIndex("ColleagueId"); + + b.ToTable("user_colleague"); + }); + + modelBuilder.Entity("TNO.Entities.UserContentNotification", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsSubscribed") + .HasColumnType("boolean") + .HasColumnName("is_subscribed"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "ContentId"); + + b.HasIndex("ContentId"); + + b.ToTable("user_content_notification"); + }); + + modelBuilder.Entity("TNO.Entities.UserDistribution", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LinkedUserId") + .HasColumnType("integer") + .HasColumnName("linked_user_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "LinkedUserId"); + + b.HasIndex("LinkedUserId"); + + b.ToTable("user_distribution"); + }); + + modelBuilder.Entity("TNO.Entities.UserMediaType", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "MediaTypeId"); + + b.HasIndex("MediaTypeId"); + + b.ToTable("user_media_type"); + }); + + modelBuilder.Entity("TNO.Entities.UserNotification", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("NotificationId") + .HasColumnType("integer") + .HasColumnName("notification_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsSubscribed") + .HasColumnType("boolean") + .HasColumnName("is_subscribed"); + + b.Property("Resend") + .HasColumnType("integer") + .HasColumnName("resend"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("user_notification"); + }); + + modelBuilder.Entity("TNO.Entities.UserOrganization", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("OrganizationId") + .HasColumnType("integer") + .HasColumnName("organization_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "OrganizationId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("user_organization"); + }); + + modelBuilder.Entity("TNO.Entities.UserProduct", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("ProductId") + .HasColumnType("integer") + .HasColumnName("product_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("user_product"); + }); + + modelBuilder.Entity("TNO.Entities.UserReport", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("ReportId") + .HasColumnType("integer") + .HasColumnName("report_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Format") + .HasColumnType("integer") + .HasColumnName("format"); + + b.Property("IsSubscribed") + .HasColumnType("boolean") + .HasColumnName("is_subscribed"); + + b.Property("SendTo") + .HasColumnType("integer") + .HasColumnName("send_to"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "ReportId"); + + b.HasIndex("ReportId"); + + b.ToTable("user_report"); + }); + + modelBuilder.Entity("TNO.Entities.UserReportInstance", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("InstanceId") + .HasColumnType("bigint") + .HasColumnName("report_instance_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LinkResponse") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("link_response") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("LinkSentOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("link_sent_on"); + + b.Property("LinkStatus") + .HasColumnType("integer") + .HasColumnName("link_status"); + + b.Property("TextResponse") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("text_response") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("TextSentOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("text_sent_on"); + + b.Property("TextStatus") + .HasColumnType("integer") + .HasColumnName("text_status"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "InstanceId"); + + b.HasIndex("InstanceId"); + + b.HasIndex(new[] { "LinkSentOn", "LinkStatus" }, "IX_user_report_instance_link"); + + b.HasIndex(new[] { "TextSentOn", "TextStatus" }, "IX_user_report_instance_text"); + + b.ToTable("user_report_instance"); + }); + + modelBuilder.Entity("TNO.Entities.UserSource", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("SourceId") + .HasColumnType("integer") + .HasColumnName("source_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("UserId", "SourceId"); + + b.HasIndex("SourceId"); + + b.ToTable("user_source"); + }); + + modelBuilder.Entity("TNO.Entities.UserUpdateHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChangeType") + .HasColumnType("integer") + .HasColumnName("change_type"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DateOfChange") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_of_change"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("value"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_update_history"); + }); + + modelBuilder.Entity("TNO.Entities.WorkOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedId") + .HasColumnType("integer") + .HasColumnName("assigned_id"); + + b.Property("Configuration") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasColumnName("configuration") + .HasDefaultValueSql("'{}'::jsonb"); + + b.Property("ContentId") + .HasColumnType("bigint") + .HasColumnName("content_id"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("created_by"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("RequestorId") + .HasColumnType("integer") + .HasColumnName("requestor_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)") + .HasColumnName("updated_by"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("version") + .HasDefaultValueSql("0"); + + b.Property("WorkType") + .HasColumnType("integer") + .HasColumnName("work_type"); + + b.HasKey("Id"); + + b.HasIndex("AssignedId"); + + b.HasIndex("ContentId"); + + b.HasIndex("RequestorId"); + + b.HasIndex(new[] { "WorkType", "Status", "CreatedOn", "RequestorId", "AssignedId" }, "IX_work_order"); + + b.ToTable("work_order"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewInstance", b => + { + b.HasOne("TNO.Entities.AVOverviewTemplate", "Template") + .WithMany("Instances") + .HasForeignKey("TemplateType") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewSection", b => + { + b.HasOne("TNO.Entities.AVOverviewInstance", "Instance") + .WithMany("Sections") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Instance"); + + b.Navigation("Series"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewSectionItem", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany() + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.AVOverviewSection", "Section") + .WithMany("Items") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewTemplate", b => + { + b.HasOne("TNO.Entities.ReportTemplate", "Template") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewTemplateSection", b => + { + b.HasOne("TNO.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.AVOverviewTemplate", "Template") + .WithMany("Sections") + .HasForeignKey("TemplateType") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("Source"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewTemplateSectionItem", b => + { + b.HasOne("TNO.Entities.AVOverviewTemplateSection", "Section") + .WithMany("Items") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("TNO.Entities.Content", b => + { + b.HasOne("TNO.Entities.Contributor", "Contributor") + .WithMany("Contents") + .HasForeignKey("ContributorId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.IngestType", null) + .WithMany("Contents") + .HasForeignKey("IngestTypeId"); + + b.HasOne("TNO.Entities.License", "License") + .WithMany("Contents") + .HasForeignKey("LicenseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.MediaType", "MediaType") + .WithMany("Contents") + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "Owner") + .WithMany("Contents") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TNO.Entities.Series", "Series") + .WithMany("Contents") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("Contents") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Contributor"); + + b.Navigation("License"); + + b.Navigation("MediaType"); + + b.Navigation("Owner"); + + b.Navigation("Series"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.ContentAction", b => + { + b.HasOne("TNO.Entities.Action", "Action") + .WithMany("ContentsManyToMany") + .HasForeignKey("ActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("ActionsManyToMany") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Action"); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("TNO.Entities.ContentLabel", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("Labels") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("TNO.Entities.ContentLink", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("Links") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Content", "Link") + .WithMany() + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("TNO.Entities.ContentLog", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("Logs") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("TNO.Entities.ContentTag", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("TagsManyToMany") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Tag", "Tag") + .WithMany("ContentsManyToMany") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("TNO.Entities.ContentTonePool", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("TonePoolsManyToMany") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.TonePool", "TonePool") + .WithMany("ContentsManyToMany") + .HasForeignKey("TonePoolId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("TonePool"); + }); + + modelBuilder.Entity("TNO.Entities.ContentTopic", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("TopicsManyToMany") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Topic", "Topic") + .WithMany("ContentsManyToMany") + .HasForeignKey("TopicId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("Topic"); + }); + + modelBuilder.Entity("TNO.Entities.ContentTypeAction", b => + { + b.HasOne("TNO.Entities.Action", "Action") + .WithMany("ContentTypes") + .HasForeignKey("ActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Action"); + }); + + modelBuilder.Entity("TNO.Entities.Contributor", b => + { + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("Contributors") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.DataLocation", b => + { + b.HasOne("TNO.Entities.Connection", "Connection") + .WithMany("DataLocations") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("TNO.Entities.EarnedMedia", b => + { + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("EarnedMedia") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.EventSchedule", b => + { + b.HasOne("TNO.Entities.Folder", "Folder") + .WithMany("Events") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Notification", "Notification") + .WithMany("Schedules") + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Report", "Report") + .WithMany("Events") + .HasForeignKey("ReportId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Schedule", "Schedule") + .WithMany("Events") + .HasForeignKey("ScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Folder"); + + b.Navigation("Notification"); + + b.Navigation("Report"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("TNO.Entities.FileReference", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("FileReferences") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("TNO.Entities.Filter", b => + { + b.HasOne("TNO.Entities.User", "Owner") + .WithMany("Filters") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TNO.Entities.Folder", b => + { + b.HasOne("TNO.Entities.Filter", "Filter") + .WithMany("Folders") + .HasForeignKey("FilterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.User", "Owner") + .WithMany("Folders") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Filter"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TNO.Entities.FolderContent", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("FoldersManyToMany") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Folder", "Folder") + .WithMany("ContentManyToMany") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("Folder"); + }); + + modelBuilder.Entity("TNO.Entities.Ingest", b => + { + b.HasOne("TNO.Entities.Connection", "DestinationConnection") + .WithMany("DestinationIngests") + .HasForeignKey("DestinationConnectionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TNO.Entities.IngestType", "IngestType") + .WithMany("Ingests") + .HasForeignKey("IngestTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TNO.Entities.MediaType", "MediaType") + .WithMany("Ingests") + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TNO.Entities.Connection", "SourceConnection") + .WithMany("SourceIngests") + .HasForeignKey("SourceConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("Ingests") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DestinationConnection"); + + b.Navigation("IngestType"); + + b.Navigation("MediaType"); + + b.Navigation("Source"); + + b.Navigation("SourceConnection"); + }); + + modelBuilder.Entity("TNO.Entities.IngestDataLocation", b => + { + b.HasOne("TNO.Entities.DataLocation", "DataLocation") + .WithMany("IngestsManyToMany") + .HasForeignKey("DataLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Ingest", "Ingest") + .WithMany("DataLocationsManyToMany") + .HasForeignKey("IngestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DataLocation"); + + b.Navigation("Ingest"); + }); + + modelBuilder.Entity("TNO.Entities.IngestSchedule", b => + { + b.HasOne("TNO.Entities.Ingest", "Ingest") + .WithMany("SchedulesManyToMany") + .HasForeignKey("IngestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Schedule", "Schedule") + .WithMany("IngestsManyToMany") + .HasForeignKey("ScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingest"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("TNO.Entities.IngestState", b => + { + b.HasOne("TNO.Entities.Ingest", "Ingest") + .WithOne("State") + .HasForeignKey("TNO.Entities.IngestState", "IngestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingest"); + }); + + modelBuilder.Entity("TNO.Entities.MediaAnalytics", b => + { + b.HasOne("TNO.Entities.MediaType", "MediaType") + .WithMany() + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaType"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.Minister", b => + { + b.HasOne("TNO.Entities.Organization", "Organization") + .WithMany("Ministers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("TNO.Entities.Notification", b => + { + b.HasOne("TNO.Entities.User", "Owner") + .WithMany("Notifications") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.NotificationTemplate", "Template") + .WithMany("Notifications") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("TNO.Entities.NotificationInstance", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("NotificationsManyToMany") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Notification", "Notification") + .WithMany("Instances") + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("Notification"); + }); + + modelBuilder.Entity("TNO.Entities.Organization", b => + { + b.HasOne("TNO.Entities.Organization", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("TNO.Entities.Quote", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("Quotes") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); + + modelBuilder.Entity("TNO.Entities.Report", b => + { + b.HasOne("TNO.Entities.User", "Owner") + .WithMany("Reports") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.ReportTemplate", "Template") + .WithMany("Reports") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("TNO.Entities.ReportInstance", b => + { + b.HasOne("TNO.Entities.User", "Owner") + .WithMany("ReportInstances") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Report", "Report") + .WithMany("Instances") + .HasForeignKey("ReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Report"); + }); + + modelBuilder.Entity("TNO.Entities.ReportInstanceContent", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("ReportsManyToMany") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.ReportInstance", "Instance") + .WithMany("ContentManyToMany") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("TNO.Entities.ReportSection", b => + { + b.HasOne("TNO.Entities.Filter", "Filter") + .WithMany("ReportSections") + .HasForeignKey("FilterId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Folder", "Folder") + .WithMany("ReportSections") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Report", "LinkedReport") + .WithMany() + .HasForeignKey("LinkedReportId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Report", "Report") + .WithMany("Sections") + .HasForeignKey("ReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Filter"); + + b.Navigation("Folder"); + + b.Navigation("LinkedReport"); + + b.Navigation("Report"); + }); + + modelBuilder.Entity("TNO.Entities.ReportSectionChartTemplate", b => + { + b.HasOne("TNO.Entities.ChartTemplate", "ChartTemplate") + .WithMany("ReportSectionsManyToMany") + .HasForeignKey("ChartTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.ReportSection", "ReportSection") + .WithMany("ChartTemplatesManyToMany") + .HasForeignKey("ReportSectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChartTemplate"); + + b.Navigation("ReportSection"); + }); + + modelBuilder.Entity("TNO.Entities.ReportTemplateChartTemplate", b => + { + b.HasOne("TNO.Entities.ChartTemplate", "ChartTemplate") + .WithMany("ReportTemplatesManyToMany") + .HasForeignKey("ChartTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.ReportTemplate", "ReportTemplate") + .WithMany("ChartTemplatesManyToMany") + .HasForeignKey("ReportTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChartTemplate"); + + b.Navigation("ReportTemplate"); + }); + + modelBuilder.Entity("TNO.Entities.Schedule", b => + { + b.HasOne("TNO.Entities.User", "RequestedBy") + .WithMany() + .HasForeignKey("RequestedById") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("RequestedBy"); + }); + + modelBuilder.Entity("TNO.Entities.Series", b => + { + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("Series") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.SeriesMediaTypeSearchMapping", b => + { + b.HasOne("TNO.Entities.MediaType", "MediaType") + .WithMany("SeriesSearchMappingsManyToMany") + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Series", "Series") + .WithMany("MediaTypeSearchMappingsManyToMany") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaType"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("TNO.Entities.Source", b => + { + b.HasOne("TNO.Entities.License", "License") + .WithMany("Sources") + .HasForeignKey("LicenseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TNO.Entities.MediaType", "MediaType") + .WithMany("Sources") + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("TNO.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("License"); + + b.Navigation("MediaType"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TNO.Entities.SourceMediaTypeSearchMapping", b => + { + b.HasOne("TNO.Entities.MediaType", "MediaType") + .WithMany("SourceSearchMappingsManyToMany") + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("MediaTypeSearchMappingsManyToMany") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaType"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.SourceMetric", b => + { + b.HasOne("TNO.Entities.Metric", "Metric") + .WithMany("SourcesManyToMany") + .HasForeignKey("MetricId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("MetricsManyToMany") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Metric"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.TimeTracking", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("TimeTrackings") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("TimeTrackings") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.TonePool", b => + { + b.HasOne("TNO.Entities.User", "Owner") + .WithMany("TonePools") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TNO.Entities.TopicScoreRule", b => + { + b.HasOne("TNO.Entities.Series", "Series") + .WithMany("ScoreRules") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("ScoreRules") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("TNO.Entities.UserAVOverview", b => + { + b.HasOne("TNO.Entities.AVOverviewTemplate", "Template") + .WithMany("SubscribersManyToMany") + .HasForeignKey("TemplateType") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("AVOverviewSubscriptionsManyToMany") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserAVOverviewInstance", b => + { + b.HasOne("TNO.Entities.AVOverviewInstance", "Instance") + .WithMany("UserInstances") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserColleague", b => + { + b.HasOne("TNO.Entities.User", "Colleague") + .WithMany() + .HasForeignKey("ColleagueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("ColleaguesManyToMany") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Colleague"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserContentNotification", b => + { + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("UserNotifications") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("ContentNotifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserDistribution", b => + { + b.HasOne("TNO.Entities.User", "LinkedUser") + .WithMany() + .HasForeignKey("LinkedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("Distribution") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LinkedUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserMediaType", b => + { + b.HasOne("TNO.Entities.MediaType", "MediaType") + .WithMany("UsersManyToMany") + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("MediaTypesManyToMany") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaType"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserNotification", b => + { + b.HasOne("TNO.Entities.Notification", "Notification") + .WithMany("SubscribersManyToMany") + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("NotificationSubscriptionsManyToMany") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserOrganization", b => + { + b.HasOne("TNO.Entities.Organization", "Organization") + .WithMany("UsersManyToMany") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("OrganizationsManyToMany") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserProduct", b => + { + b.HasOne("TNO.Entities.Product", "Product") + .WithMany("SubscribersManyToMany") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("ProductSubscriptionsManyToMany") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserReport", b => + { + b.HasOne("TNO.Entities.Report", "Report") + .WithMany("SubscribersManyToMany") + .HasForeignKey("ReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("ReportSubscriptionsManyToMany") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Report"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserReportInstance", b => + { + b.HasOne("TNO.Entities.ReportInstance", "Instance") + .WithMany("UserInstances") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserSource", b => + { + b.HasOne("TNO.Entities.Source", "Source") + .WithMany("UsersManyToMany") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TNO.Entities.User", "User") + .WithMany("SourcesManyToMany") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Source"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.UserUpdateHistory", b => + { + b.HasOne("TNO.Entities.User", "User") + .WithMany("UserUpdateHistory") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TNO.Entities.WorkOrder", b => + { + b.HasOne("TNO.Entities.User", "Assigned") + .WithMany("WorkOrdersAssigned") + .HasForeignKey("AssignedId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("TNO.Entities.Content", "Content") + .WithMany("WorkOrders") + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("TNO.Entities.User", "Requestor") + .WithMany("WorkOrderRequests") + .HasForeignKey("RequestorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Assigned"); + + b.Navigation("Content"); + + b.Navigation("Requestor"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewInstance", b => + { + b.Navigation("Sections"); + + b.Navigation("UserInstances"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewSection", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewTemplate", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + + b.Navigation("SubscribersManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.AVOverviewTemplateSection", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("TNO.Entities.Action", b => + { + b.Navigation("ContentTypes"); + + b.Navigation("ContentsManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.ChartTemplate", b => + { + b.Navigation("ReportSectionsManyToMany"); + + b.Navigation("ReportTemplatesManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Connection", b => + { + b.Navigation("DataLocations"); + + b.Navigation("DestinationIngests"); + + b.Navigation("SourceIngests"); + }); + + modelBuilder.Entity("TNO.Entities.Content", b => + { + b.Navigation("ActionsManyToMany"); + + b.Navigation("FileReferences"); + + b.Navigation("FoldersManyToMany"); + + b.Navigation("Labels"); + + b.Navigation("Links"); + + b.Navigation("Logs"); + + b.Navigation("NotificationsManyToMany"); + + b.Navigation("Quotes"); + + b.Navigation("ReportsManyToMany"); + + b.Navigation("TagsManyToMany"); + + b.Navigation("TimeTrackings"); + + b.Navigation("TonePoolsManyToMany"); + + b.Navigation("TopicsManyToMany"); + + b.Navigation("UserNotifications"); + + b.Navigation("WorkOrders"); + }); + + modelBuilder.Entity("TNO.Entities.Contributor", b => + { + b.Navigation("Contents"); + }); + + modelBuilder.Entity("TNO.Entities.DataLocation", b => + { + b.Navigation("IngestsManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Filter", b => + { + b.Navigation("Folders"); + + b.Navigation("ReportSections"); + }); + + modelBuilder.Entity("TNO.Entities.Folder", b => + { + b.Navigation("ContentManyToMany"); + + b.Navigation("Events"); + + b.Navigation("ReportSections"); + }); + + modelBuilder.Entity("TNO.Entities.Ingest", b => + { + b.Navigation("DataLocationsManyToMany"); + + b.Navigation("SchedulesManyToMany"); + + b.Navigation("State"); + }); + + modelBuilder.Entity("TNO.Entities.IngestType", b => + { + b.Navigation("Contents"); + + b.Navigation("Ingests"); + }); + + modelBuilder.Entity("TNO.Entities.License", b => + { + b.Navigation("Contents"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("TNO.Entities.MediaType", b => + { + b.Navigation("Contents"); + + b.Navigation("Ingests"); + + b.Navigation("SeriesSearchMappingsManyToMany"); + + b.Navigation("SourceSearchMappingsManyToMany"); + + b.Navigation("Sources"); + + b.Navigation("UsersManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Metric", b => + { + b.Navigation("SourcesManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Notification", b => + { + b.Navigation("Instances"); + + b.Navigation("Schedules"); + + b.Navigation("SubscribersManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.NotificationTemplate", b => + { + b.Navigation("Notifications"); + }); + + modelBuilder.Entity("TNO.Entities.Organization", b => + { + b.Navigation("Children"); + + b.Navigation("Ministers"); + + b.Navigation("UsersManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Product", b => + { + b.Navigation("SubscribersManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Report", b => + { + b.Navigation("Events"); + + b.Navigation("Instances"); + + b.Navigation("Sections"); + + b.Navigation("SubscribersManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.ReportInstance", b => + { + b.Navigation("ContentManyToMany"); + + b.Navigation("UserInstances"); + }); + + modelBuilder.Entity("TNO.Entities.ReportSection", b => + { + b.Navigation("ChartTemplatesManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.ReportTemplate", b => + { + b.Navigation("ChartTemplatesManyToMany"); + + b.Navigation("Reports"); + }); + + modelBuilder.Entity("TNO.Entities.Schedule", b => + { + b.Navigation("Events"); + + b.Navigation("IngestsManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Series", b => + { + b.Navigation("Contents"); + + b.Navigation("MediaTypeSearchMappingsManyToMany"); + + b.Navigation("ScoreRules"); + }); + + modelBuilder.Entity("TNO.Entities.Source", b => + { + b.Navigation("Contents"); + + b.Navigation("Contributors"); + + b.Navigation("EarnedMedia"); + + b.Navigation("Ingests"); + + b.Navigation("MediaTypeSearchMappingsManyToMany"); + + b.Navigation("MetricsManyToMany"); + + b.Navigation("ScoreRules"); + + b.Navigation("Series"); + + b.Navigation("UsersManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Tag", b => + { + b.Navigation("ContentsManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.TonePool", b => + { + b.Navigation("ContentsManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.Topic", b => + { + b.Navigation("ContentsManyToMany"); + }); + + modelBuilder.Entity("TNO.Entities.User", b => + { + b.Navigation("AVOverviewSubscriptionsManyToMany"); + + b.Navigation("ColleaguesManyToMany"); + + b.Navigation("ContentNotifications"); + + b.Navigation("Contents"); + + b.Navigation("Distribution"); + + b.Navigation("Filters"); + + b.Navigation("Folders"); + + b.Navigation("MediaTypesManyToMany"); + + b.Navigation("NotificationSubscriptionsManyToMany"); + + b.Navigation("Notifications"); + + b.Navigation("OrganizationsManyToMany"); + + b.Navigation("ProductSubscriptionsManyToMany"); + + b.Navigation("ReportInstances"); + + b.Navigation("ReportSubscriptionsManyToMany"); + + b.Navigation("Reports"); + + b.Navigation("SourcesManyToMany"); + + b.Navigation("TimeTrackings"); + + b.Navigation("TonePools"); + + b.Navigation("UserUpdateHistory"); + + b.Navigation("WorkOrderRequests"); + + b.Navigation("WorkOrdersAssigned"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/libs/net/dal/Migrations/20260220195903_1.5.cs b/libs/net/dal/Migrations/20260220195903_1.5.cs new file mode 100644 index 0000000000..0930c08537 --- /dev/null +++ b/libs/net/dal/Migrations/20260220195903_1.5.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using TNO.DAL; + +#nullable disable + +namespace TNO.DAL.Migrations +{ + /// + public partial class _15 : SeedMigration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + PreUp(migrationBuilder); + migrationBuilder.AddColumn( + name: "link_body", + table: "report_instance", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "body", + table: "av_overview_instance", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "status", + table: "av_overview_instance", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "subject", + table: "av_overview_instance", + type: "text", + nullable: false, + defaultValue: ""); + PostUp(migrationBuilder); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + PreDown(migrationBuilder); + migrationBuilder.DropColumn( + name: "link_body", + table: "report_instance"); + + migrationBuilder.DropColumn( + name: "body", + table: "av_overview_instance"); + + migrationBuilder.DropColumn( + name: "status", + table: "av_overview_instance"); + + migrationBuilder.DropColumn( + name: "subject", + table: "av_overview_instance"); + PostDown(migrationBuilder); + } + } +} diff --git a/libs/net/dal/Migrations/TNOContextModelSnapshot.cs b/libs/net/dal/Migrations/TNOContextModelSnapshot.cs index 252dfe1cad..fc90d50051 100644 --- a/libs/net/dal/Migrations/TNOContextModelSnapshot.cs +++ b/libs/net/dal/Migrations/TNOContextModelSnapshot.cs @@ -34,6 +34,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Body") + .IsRequired() + .HasColumnType("text") + .HasColumnName("body"); + b.Property("CreatedBy") .IsRequired() .HasMaxLength(250) @@ -60,6 +65,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("response") .HasDefaultValueSql("'{}'::jsonb"); + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text") + .HasColumnName("subject"); + b.Property("TemplateType") .HasColumnType("integer") .HasColumnName("template_type"); @@ -3833,6 +3847,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("created_on") .HasDefaultValueSql("CURRENT_TIMESTAMP"); + b.Property("LinkOnlyBody") + .IsRequired() + .HasColumnType("text") + .HasColumnName("link_body"); + b.Property("OwnerId") .HasColumnType("integer") .HasColumnName("owner_id"); diff --git a/libs/net/dal/Services/Interfaces/INotificationService.cs b/libs/net/dal/Services/Interfaces/INotificationService.cs index 7c0a83711c..c76f6edf5c 100644 --- a/libs/net/dal/Services/Interfaces/INotificationService.cs +++ b/libs/net/dal/Services/Interfaces/INotificationService.cs @@ -55,5 +55,5 @@ public interface INotificationService : IBaseService /// /// /// - IEnumerable GetChesMessageIds(NotificationStatus status, DateTime cutOff); + IEnumerable GetSmtpMessages(NotificationStatus status, DateTime cutOff); } diff --git a/libs/net/dal/Services/Interfaces/IReportService.cs b/libs/net/dal/Services/Interfaces/IReportService.cs index c95aae6131..d9d7974b8d 100644 --- a/libs/net/dal/Services/Interfaces/IReportService.cs +++ b/libs/net/dal/Services/Interfaces/IReportService.cs @@ -188,12 +188,12 @@ public Task GenerateReportInstanceAsync( Dictionary GetAllContentInMyReports(int userId); /// - /// get all CHES message Ids for reports at the specified 'status' and that were sent on or after 'cutOff' date and time. + /// get all SMTP message Ids for reports at the specified 'status' and that were sent on or after 'cutOff' date and time. /// /// /// /// - IEnumerable GetChesMessageIds(ReportStatus status, DateTime cutOff); + IEnumerable GetSmtpMessageIds(ReportStatus status, DateTime cutOff); /// /// Add the user report. diff --git a/libs/net/dal/Services/NotificationService.cs b/libs/net/dal/Services/NotificationService.cs index 9e7172434f..e6a6f0de70 100644 --- a/libs/net/dal/Services/NotificationService.cs +++ b/libs/net/dal/Services/NotificationService.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Text.Json; using Microsoft.EntityFrameworkCore; @@ -429,24 +428,22 @@ public IEnumerable GetDashboard(DashboardFilter filter) /// /// /// - public IEnumerable GetChesMessageIds(NotificationStatus status, DateTime cutOff) + public IEnumerable GetSmtpMessages(NotificationStatus status, DateTime cutOff) { var notifications = this.Context.NotificationInstances.Where(r => r.Status == status && r.SentOn >= cutOff) .Select(r => new { r.NotificationId, InstanceId = r.Id, r.SentOn, r.Response }).ToArray(); - var messages = new List(); + var messages = new List(); foreach (var notification in notifications) { - var response = JsonSerializer.Deserialize(notification.Response.ToJson(), _serializerOptions); - var messageIds = response?.Messages.Select(m => m.MessageId).ToArray() ?? []; - messages.Add(new API.Areas.Services.Models.Notification.ChesNotificationMessagesModel() + messages.Add(new API.Areas.Services.Models.Notification.SmtpNotificationMessagesModel() { NotificationId = notification.NotificationId, InstanceId = notification.InstanceId, SentOn = notification.SentOn, Status = status, - MessageIds = messageIds, + Responses = notification.Response, }); } diff --git a/libs/net/dal/Services/ProductService.cs b/libs/net/dal/Services/ProductService.cs index 9aeca43671..762076a940 100644 --- a/libs/net/dal/Services/ProductService.cs +++ b/libs/net/dal/Services/ProductService.cs @@ -3,8 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; +using MMI.SmtpEmail; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.DAL.Extensions; @@ -16,8 +15,8 @@ namespace TNO.DAL.Services; public class ProductService : BaseService, IProductService { #region Variables - private readonly IChesService _chesService; - private readonly ChesOptions _chesOptions; + private readonly IEmailService _emailService; + private readonly SmtpOptions _smtpOptions; #endregion #region Constructors @@ -26,20 +25,20 @@ public class ProductService : BaseService, IProductService /// /// /// - /// - /// + /// + /// /// /// public ProductService( TNOContext dbContext, ClaimsPrincipal principal, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IServiceProvider serviceProvider, ILogger logger) : base(dbContext, principal, serviceProvider, logger) { - _chesService = chesService; - _chesOptions = chesOptions.Value; + _emailService = emailService; + _smtpOptions = smtpOptions.Value; } #endregion @@ -610,9 +609,9 @@ public async Task SendSubscriptionRequestEmailAsync(UserProduct subscription) if (productSubscriptionManagerEmail != null) { var emailAddresses = productSubscriptionManagerEmail.Value.Split(new char[] { ';', ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - var email = new TNO.Ches.Models.EmailModel(_chesOptions.From, emailAddresses, subject, message.ToString()); - var response = await _chesService.SendEmailAsync(email); - this.Logger.LogInformation("Product subscription request email to [${email}] queued: ${transactionId}", productSubscriptionManagerEmail.Value, response.TransactionId); + var email = _emailService.CreateMailMessage(subject, message.ToString(), emailAddresses, null, null, null, true); + await _emailService.SendAsync(email); + this.Logger.LogInformation("Product subscription request email to [${email}]", productSubscriptionManagerEmail.Value); } else { diff --git a/libs/net/dal/Services/ReportService.cs b/libs/net/dal/Services/ReportService.cs index 4c21be4c05..1b98857850 100644 --- a/libs/net/dal/Services/ReportService.cs +++ b/libs/net/dal/Services/ReportService.cs @@ -1909,24 +1909,22 @@ orderby ri.Id descending } /// - /// get all CHES message Ids for reports at the specified 'status' and that were sent on or after 'cutOff' date and time. + /// get all SMTP message Ids for reports at the specified 'status' and that were sent on or after 'cutOff' date and time. /// /// /// /// - public IEnumerable GetChesMessageIds(ReportStatus status, DateTime cutOff) + public IEnumerable GetSmtpMessageIds(ReportStatus status, DateTime cutOff) { var linkReports = this.Context.UserReportInstances.Include(i => i.Instance).Where(r => r.LinkStatus == status && r.LinkSentOn >= cutOff).Select(r => new { r.Instance!.ReportId, r.InstanceId, r.UserId, SentOn = r.LinkSentOn, r.LinkResponse }).ToArray(); var textReports = this.Context.UserReportInstances.Include(i => i.Instance).Where(r => r.TextStatus == status && r.TextSentOn >= cutOff).Select(r => new { r.Instance!.ReportId, r.InstanceId, r.UserId, SentOn = r.TextSentOn, r.TextResponse }).ToArray(); var avReports = this.Context.UserAVOverviewInstances.Where(r => r.Status == status && r.SentOn >= cutOff).Select(r => new { r.InstanceId, r.UserId, r.SentOn, r.Response }).ToArray(); - var messages = new List(); + var messages = new List(); foreach (var report in linkReports) { - var response = JsonSerializer.Deserialize(report.LinkResponse.ToJson(), _serializerOptions); - var messageIds = response?.Messages.Select(m => m.MessageId).ToArray() ?? []; - messages.Add(new API.Areas.Services.Models.Report.ChesReportMessagesModel() + messages.Add(new API.Areas.Services.Models.Report.SmtpReportMessagesModel() { ReportType = ReportType.Content, Format = ReportDistributionFormat.LinkOnly, @@ -1935,15 +1933,13 @@ orderby ri.Id descending UserId = report.UserId, SentOn = report.SentOn, Status = status, - MessageIds = messageIds, + Response = report.LinkResponse, }); } foreach (var report in textReports) { - var response = JsonSerializer.Deserialize(report.TextResponse.ToJson(), _serializerOptions); - var messageIds = response?.Messages.Select(m => m.MessageId).ToArray() ?? []; - messages.Add(new API.Areas.Services.Models.Report.ChesReportMessagesModel() + messages.Add(new API.Areas.Services.Models.Report.SmtpReportMessagesModel() { ReportType = ReportType.Content, Format = ReportDistributionFormat.FullText, @@ -1952,15 +1948,13 @@ orderby ri.Id descending UserId = report.UserId, SentOn = report.SentOn, Status = status, - MessageIds = messageIds, + Response = report.TextResponse, }); } foreach (var report in avReports) { - var response = JsonSerializer.Deserialize(report.Response.ToJson(), _serializerOptions); - var messageIds = response?.Messages.Select(m => m.MessageId).ToArray() ?? []; - messages.Add(new API.Areas.Services.Models.Report.ChesReportMessagesModel() + messages.Add(new API.Areas.Services.Models.Report.SmtpReportMessagesModel() { ReportType = ReportType.AVOverview, Format = ReportDistributionFormat.FullText, @@ -1968,7 +1962,7 @@ orderby ri.Id descending UserId = report.UserId, SentOn = report.SentOn, Status = status, - MessageIds = messageIds, + Response = report.Response, }); } diff --git a/libs/net/dal/TNO.DAL.csproj b/libs/net/dal/TNO.DAL.csproj index b7d1bf2f25..9cc9da3a7f 100644 --- a/libs/net/dal/TNO.DAL.csproj +++ b/libs/net/dal/TNO.DAL.csproj @@ -19,7 +19,7 @@ - + diff --git a/libs/net/entities/AVOverviewInstance.cs b/libs/net/entities/AVOverviewInstance.cs index 6bb3c296df..02f5973d8f 100644 --- a/libs/net/entities/AVOverviewInstance.cs +++ b/libs/net/entities/AVOverviewInstance.cs @@ -40,6 +40,26 @@ public class AVOverviewInstance : AuditColumns ///
public bool IsPublished { get; set; } + /// + /// get/set - The compiled subject of the report. + /// Used to recreate the report. + /// + [Column("subject")] + public string Subject { get; set; } = ""; + + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + [Column("body")] + public string Body { get; set; } = ""; + + /// + /// get/set - The status of this report. + /// + [Column("status")] + public ReportStatus Status { get; set; } + /// /// get/set - The response. /// diff --git a/libs/net/entities/ReportInstance.cs b/libs/net/entities/ReportInstance.cs index 9731d6b4a2..16ae916c85 100644 --- a/libs/net/entities/ReportInstance.cs +++ b/libs/net/entities/ReportInstance.cs @@ -66,6 +66,13 @@ public class ReportInstance : AuditColumns [Column("body")] public string Body { get; set; } = ""; + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + [Column("link_body")] + public string LinkOnlyBody { get; set; } = ""; + /// /// get/set - The status of this report. /// diff --git a/libs/net/models/Areas/Admin/NotificationInstance/NotificationInstanceModel.cs b/libs/net/models/Areas/Admin/NotificationInstance/NotificationInstanceModel.cs index edecd76d0a..c419115425 100644 --- a/libs/net/models/Areas/Admin/NotificationInstance/NotificationInstanceModel.cs +++ b/libs/net/models/Areas/Admin/NotificationInstance/NotificationInstanceModel.cs @@ -36,9 +36,9 @@ public class NotificationInstanceModel : AuditColumnsModel public int? OwnerId { get; set; } /// - /// get/set - CHES response containing keys to find the status of a notification. + /// get/set - SMTP response for each email sent for this notification instance. /// - public Dictionary Response { get; set; } = new Dictionary(); + public JsonDocument Response { get; set; } = JsonDocument.Parse("{}"); /// /// get - The compiled subject of the notification. @@ -78,7 +78,7 @@ public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSeria this.NotificationId = entity.NotificationId; this.ContentId = entity.ContentId; this.OwnerId = entity.OwnerId; - this.Response = JsonSerializer.Deserialize>(entity.Response, options) ?? new Dictionary(); + this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; this.Status = entity.Status; @@ -88,17 +88,6 @@ public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSeria #endregion #region Methods - /// - /// Creates a new instance of a NotificationInstance object. - /// - /// - public Entities.NotificationInstance ToEntity(JsonSerializerOptions options) - { - var entity = (Entities.NotificationInstance)this; - entity.Response = JsonDocument.Parse(JsonSerializer.Serialize(this.Response, options)); - return entity; - } - /// /// Explicit conversion to entity. /// @@ -108,7 +97,7 @@ public static explicit operator Entities.NotificationInstance(NotificationInstan var entity = new Entities.NotificationInstance(model.NotificationId, model.ContentId, model.OwnerId) { Id = model.Id, - Response = JsonDocument.Parse(JsonSerializer.Serialize(model.Response)), + Response = model.Response, Subject = model.Subject, Body = model.Body, Version = model.Version ?? 0 diff --git a/libs/net/models/Areas/Admin/Report/ReportInstanceModel.cs b/libs/net/models/Areas/Admin/Report/ReportInstanceModel.cs index 0f2d82293e..c10ca52b8a 100644 --- a/libs/net/models/Areas/Admin/Report/ReportInstanceModel.cs +++ b/libs/net/models/Areas/Admin/Report/ReportInstanceModel.cs @@ -57,6 +57,12 @@ public class ReportInstanceModel : AuditColumnsModel /// public string Body { get; set; } = ""; + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string LinkOnlyBody { get; set; } = ""; + /// /// get/set - CHES response containing keys to find the status of a report. /// @@ -85,6 +91,7 @@ public ReportInstanceModel(Entities.ReportInstance entity) : base(entity) this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; + this.LinkOnlyBody = entity.LinkOnlyBody; } #endregion } diff --git a/libs/net/models/Areas/Admin/ReportInstance/ReportInstanceModel.cs b/libs/net/models/Areas/Admin/ReportInstance/ReportInstanceModel.cs index 4244e5643c..dad6de16dd 100644 --- a/libs/net/models/Areas/Admin/ReportInstance/ReportInstanceModel.cs +++ b/libs/net/models/Areas/Admin/ReportInstance/ReportInstanceModel.cs @@ -51,6 +51,12 @@ public class ReportInstanceModel : AuditColumnsModel ///
public string Body { get; set; } = ""; + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string LinkOnlyBody { get; set; } = ""; + /// /// get/set - CHES response containing keys to find the status of a report. /// @@ -83,6 +89,7 @@ public ReportInstanceModel(Entities.ReportInstance entity) : base(entity) this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; + this.LinkOnlyBody = entity.LinkOnlyBody; this.Content = entity.ContentManyToMany.OrderBy(c => c.SectionName).ThenBy(c => c.SortOrder).Select(m => new ReportInstanceContentModel(m)); } @@ -105,6 +112,7 @@ public static explicit operator Entities.ReportInstance(ReportInstanceModel mode Response = model.Response, Subject = model.Subject, Body = model.Body, + LinkOnlyBody = model.LinkOnlyBody, Version = model.Version ?? 0 }; return entity; diff --git a/libs/net/models/Areas/Editor/Models/AVOverview/AVOverviewInstanceModel.cs b/libs/net/models/Areas/Editor/Models/AVOverview/AVOverviewInstanceModel.cs index e49bb4772f..f6aaf6e0ca 100644 --- a/libs/net/models/Areas/Editor/Models/AVOverview/AVOverviewInstanceModel.cs +++ b/libs/net/models/Areas/Editor/Models/AVOverview/AVOverviewInstanceModel.cs @@ -31,6 +31,23 @@ public class AVOverviewInstanceModel : AuditColumnsModel ///
public IEnumerable Sections { get; set; } = Array.Empty(); + /// + /// get/set - The report status. + /// + public Entities.ReportStatus Status { get; set; } + + /// + /// get/set - The compiled subject of the report. + /// Used to recreate the report. + /// + public string Subject { get; set; } = ""; + + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string Body { get; set; } = ""; + // // get/set - The response. // @@ -54,6 +71,9 @@ public AVOverviewInstanceModel(Entities.AVOverviewInstance entity) : base(entity this.PublishedOn = entity.PublishedOn; this.IsPublished = entity.IsPublished; this.Sections = entity.Sections.OrderBy(s => s.SortOrder).Select(s => new AVOverviewSectionModel(s)); + this.Status = entity.Status; + this.Subject = entity.Subject; + this.Body = entity.Body; this.Response = entity.Response; } @@ -82,6 +102,9 @@ public static explicit operator Entities.AVOverviewInstance(AVOverviewInstanceMo { Id = model.Id, IsPublished = model.IsPublished, + Status = model.Status, + Subject = model.Subject, + Body = model.Body, Response = model.Response, Version = model.Version ?? 0 }; diff --git a/libs/net/models/Areas/Editor/Models/Content/NotificationInstanceModel.cs b/libs/net/models/Areas/Editor/Models/Content/NotificationInstanceModel.cs index d6c710cc00..cc0a642669 100644 --- a/libs/net/models/Areas/Editor/Models/Content/NotificationInstanceModel.cs +++ b/libs/net/models/Areas/Editor/Models/Content/NotificationInstanceModel.cs @@ -40,9 +40,9 @@ public class NotificationInstanceModel public NotificationStatus Status { get; set; } /// - /// get/set - CHES response containing keys to find the status of a notification. + /// get/set - SMTP response for each email sent for this notification instance. /// - public Dictionary Response { get; set; } = new Dictionary(); + public JsonDocument Response { get; set; } = JsonDocument.Parse("{}"); #endregion #region Constructors @@ -55,8 +55,7 @@ public NotificationInstanceModel() { } /// Creates a new instance of an NotificationInstanceModel, initializes with specified parameter. ///
/// - /// - public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSerializerOptions options) + public NotificationInstanceModel(Entities.NotificationInstance entity) { this.Id = entity.Id; this.NotificationId = entity.NotificationId; @@ -64,7 +63,7 @@ public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSeria this.OwnerId = entity.OwnerId; this.SentOn = entity.SentOn; this.Status = entity.Status; - this.Response = JsonSerializer.Deserialize>(entity.Response, options) ?? new Dictionary(); + this.Response = entity.Response; } #endregion } diff --git a/libs/net/models/Areas/Editor/Models/ReportInstance/ReportInstanceModel.cs b/libs/net/models/Areas/Editor/Models/ReportInstance/ReportInstanceModel.cs index 995bef5dec..9884d6f00a 100644 --- a/libs/net/models/Areas/Editor/Models/ReportInstance/ReportInstanceModel.cs +++ b/libs/net/models/Areas/Editor/Models/ReportInstance/ReportInstanceModel.cs @@ -50,6 +50,12 @@ public class ReportInstanceModel ///
public string Body { get; set; } = ""; + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string LinkOnlyBody { get; set; } = ""; + /// /// get/set - CHES response containing keys to find the status of a report. /// @@ -82,6 +88,7 @@ public ReportInstanceModel(Entities.ReportInstance entity) this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; + this.LinkOnlyBody = entity.LinkOnlyBody; this.Content = entity.ContentManyToMany.OrderBy(c => c.SectionName).ThenBy(c => c.SortOrder).Select(m => new ReportInstanceContentModel(m)); } diff --git a/libs/net/models/Areas/Services/Models/AVOverview/AVOverviewInstanceModel.cs b/libs/net/models/Areas/Services/Models/AVOverview/AVOverviewInstanceModel.cs index 02f4a07c53..b543e5eebf 100644 --- a/libs/net/models/Areas/Services/Models/AVOverview/AVOverviewInstanceModel.cs +++ b/libs/net/models/Areas/Services/Models/AVOverview/AVOverviewInstanceModel.cs @@ -36,6 +36,23 @@ public class AVOverviewInstanceModel : AuditColumnsModel ///
public IEnumerable Sections { get; set; } = Array.Empty(); + /// + /// get/set - The report status. + /// + public Entities.ReportStatus Status { get; set; } + + /// + /// get/set - The compiled subject of the report. + /// Used to recreate the report. + /// + public string Subject { get; set; } = ""; + + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string Body { get; set; } = ""; + // // get/set - The response. // @@ -67,6 +84,9 @@ public AVOverviewInstanceModel(Entities.AVOverviewInstance instance, JsonSeriali this.PublishedOn = instance.PublishedOn; this.IsPublished = instance.IsPublished; this.Sections = instance.Sections.OrderBy(s => s.SortOrder).Select(s => new AVOverviewSectionModel(s)); + this.Status = instance.Status; + this.Subject = instance.Subject; + this.Body = instance.Body; this.Response = instance.Response; this.Subscribers = template.SubscribersManyToMany.Where(s => s.User != null).Select(s => new UserModel(s.User!, s.IsSubscribed)).ToArray(); } @@ -84,6 +104,9 @@ public static explicit operator Entities.AVOverviewInstance(AVOverviewInstanceMo { Id = model.Id, IsPublished = model.IsPublished, + Status = model.Status, + Subject = model.Subject, + Body = model.Body, Response = model.Response, Version = model.Version ?? 0 }; diff --git a/libs/net/models/Areas/Services/Models/Content/NotificationInstanceModel.cs b/libs/net/models/Areas/Services/Models/Content/NotificationInstanceModel.cs index ee967989df..d1f5a0479b 100644 --- a/libs/net/models/Areas/Services/Models/Content/NotificationInstanceModel.cs +++ b/libs/net/models/Areas/Services/Models/Content/NotificationInstanceModel.cs @@ -39,9 +39,9 @@ public class NotificationInstanceModel public Entities.NotificationStatus Status { get; set; } /// - /// get/set - CHES response containing keys to find the status of a notification. + /// get/set - SMTP response for each email sent for this notification instance. /// - public Dictionary Response { get; set; } = new Dictionary(); + public JsonDocument Response { get; set; } = JsonDocument.Parse("{}"); #endregion #region Constructors @@ -54,8 +54,7 @@ public NotificationInstanceModel() { } /// Creates a new instance of an NotificationInstanceModel, initializes with specified parameter. /// /// - /// - public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSerializerOptions options) + public NotificationInstanceModel(Entities.NotificationInstance entity) { this.Id = entity.Id; this.NotificationId = entity.NotificationId; @@ -63,23 +62,11 @@ public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSeria this.OwnerId = entity.OwnerId; this.SentOn = entity.SentOn; this.Status = entity.Status; - this.Response = JsonSerializer.Deserialize>(entity.Response, options) ?? new Dictionary(); + this.Response = entity.Response; } #endregion #region Methods - /// - /// Creates a new instance of a NotificationInstance object. - /// - /// - /// - public Entities.NotificationInstance ToEntity(JsonSerializerOptions options) - { - var entity = (Entities.NotificationInstance)this; - entity.Response = JsonDocument.Parse(JsonSerializer.Serialize(this.Response, options)); - return entity; - } - /// /// Explicit conversion to entity. /// @@ -91,7 +78,7 @@ public static explicit operator Entities.NotificationInstance(NotificationInstan Id = model.Id, SentOn = model.SentOn, Status = model.Status, - Response = JsonDocument.Parse(JsonSerializer.Serialize(model.Response)), + Response = model.Response, }; return entity; } diff --git a/libs/net/models/Areas/Services/Models/Notification/NotificationInstanceModel.cs b/libs/net/models/Areas/Services/Models/Notification/NotificationInstanceModel.cs index add4176e26..aba4c46ed1 100644 --- a/libs/net/models/Areas/Services/Models/Notification/NotificationInstanceModel.cs +++ b/libs/net/models/Areas/Services/Models/Notification/NotificationInstanceModel.cs @@ -39,9 +39,9 @@ public class NotificationInstanceModel public Entities.NotificationStatus Status { get; set; } /// - /// get/set - CHES response containing keys to find the status of a notification. + /// get/set - SMTP response for each email sent for this notification instance. /// - public Dictionary Response { get; set; } = new Dictionary(); + public JsonDocument Response { get; set; } = JsonDocument.Parse("{}"); /// /// get - The compiled subject of the notification. @@ -64,8 +64,7 @@ public NotificationInstanceModel() { } /// Creates a new instance of an NotificationInstanceModel, initializes with specified parameter. /// /// - /// - public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSerializerOptions options) + public NotificationInstanceModel(Entities.NotificationInstance entity) { this.Id = entity.Id; this.NotificationId = entity.NotificationId; @@ -73,25 +72,13 @@ public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSeria this.OwnerId = entity.OwnerId; this.SentOn = entity.SentOn; this.Status = entity.Status; - this.Response = JsonSerializer.Deserialize>(entity.Response, options) ?? new Dictionary(); + this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; } #endregion #region Methods - /// - /// Creates a new instance of a NotificationInstance object. - /// - /// - /// - public Entities.NotificationInstance ToEntity(JsonSerializerOptions options) - { - var entity = (Entities.NotificationInstance)this; - entity.Response = JsonDocument.Parse(JsonSerializer.Serialize(this.Response, options)); - return entity; - } - /// /// Explicit conversion to entity. /// @@ -103,7 +90,7 @@ public static explicit operator Entities.NotificationInstance(NotificationInstan Id = model.Id, SentOn = model.SentOn, Status = model.Status, - Response = JsonDocument.Parse(JsonSerializer.Serialize(model.Response)), + Response = model.Response, Subject = model.Subject, Body = model.Body }; diff --git a/libs/net/models/Areas/Services/Models/Notification/NotificationModel.cs b/libs/net/models/Areas/Services/Models/Notification/NotificationModel.cs index 2268a82e83..dca036a24d 100644 --- a/libs/net/models/Areas/Services/Models/Notification/NotificationModel.cs +++ b/libs/net/models/Areas/Services/Models/Notification/NotificationModel.cs @@ -91,7 +91,7 @@ public NotificationModel(Entities.Notification entity, JsonSerializerOptions opt this.Query = entity.Query; this.Subscribers = entity.SubscribersManyToMany.Select(m => new UserNotificationModel(m)); - this.Instances = entity.Instances.Select(i => new NotificationInstanceModel(i, options)); + this.Instances = entity.Instances.Select(i => new NotificationInstanceModel(i)); } #endregion diff --git a/libs/net/models/Areas/Services/Models/Notification/ChesNotificationMessagesModel.cs b/libs/net/models/Areas/Services/Models/Notification/SmtpNotificationMessagesModel.cs similarity index 64% rename from libs/net/models/Areas/Services/Models/Notification/ChesNotificationMessagesModel.cs rename to libs/net/models/Areas/Services/Models/Notification/SmtpNotificationMessagesModel.cs index fc6d07109b..6ade39636c 100644 --- a/libs/net/models/Areas/Services/Models/Notification/ChesNotificationMessagesModel.cs +++ b/libs/net/models/Areas/Services/Models/Notification/SmtpNotificationMessagesModel.cs @@ -1,9 +1,11 @@ +using System.Text.Json; + namespace TNO.API.Areas.Services.Models.Notification; /// -/// ChesNotificationMessagesModel class, provides a model to pass CHES notification response message Ids. +/// SmtpNotificationMessagesModel class, provides a model to pass SMTP notification response message Ids. /// -public class ChesNotificationMessagesModel +public class SmtpNotificationMessagesModel { /// /// get/set - The primary key to the notification. @@ -21,12 +23,12 @@ public class ChesNotificationMessagesModel public DateTime? SentOn { get; set; } /// - /// get/set - The status of this CHES request. + /// get/set - The status of this SMTP request. /// public Entities.NotificationStatus Status { get; set; } /// - /// get/set - The CHES message Ids. + /// get/set - The SMTP response messages. /// - public IEnumerable MessageIds { get; set; } = []; + public JsonDocument Responses { get; set; } = JsonDocument.Parse("[]"); } diff --git a/libs/net/models/Areas/Services/Models/NotificationInstance/NotificationInstanceModel.cs b/libs/net/models/Areas/Services/Models/NotificationInstance/NotificationInstanceModel.cs index 4b6ba970a5..dea4bc5d6d 100644 --- a/libs/net/models/Areas/Services/Models/NotificationInstance/NotificationInstanceModel.cs +++ b/libs/net/models/Areas/Services/Models/NotificationInstance/NotificationInstanceModel.cs @@ -40,9 +40,9 @@ public class NotificationInstanceModel : AuditColumnsModel public Entities.NotificationStatus Status { get; set; } /// - /// get/set - CHES response containing keys to find the status of a notification. + /// get/set - SMTP response for each email sent for this notification instance. /// - public Dictionary Response { get; set; } = new Dictionary(); + public JsonDocument Response { get; set; } = JsonDocument.Parse("{}"); /// /// get - The compiled subject of the notification. @@ -65,8 +65,7 @@ public NotificationInstanceModel() { } /// Creates a new instance of an NotificationInstanceModel, initializes with specified parameter. /// /// - /// - public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSerializerOptions options) : base(entity) + public NotificationInstanceModel(Entities.NotificationInstance entity) : base(entity) { this.Id = entity.Id; this.NotificationId = entity.NotificationId; @@ -74,24 +73,13 @@ public NotificationInstanceModel(Entities.NotificationInstance entity, JsonSeria this.OwnerId = entity.OwnerId; this.SentOn = entity.SentOn; this.Status = entity.Status; - this.Response = JsonSerializer.Deserialize>(entity.Response, options) ?? new Dictionary(); + this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; } #endregion #region Methods - /// - /// Creates a new instance of a NotificationInstance object. - /// - /// - public Entities.NotificationInstance ToEntity(JsonSerializerOptions options) - { - var entity = (Entities.NotificationInstance)this; - entity.Response = JsonDocument.Parse(JsonSerializer.Serialize(this.Response, options)); - return entity; - } - /// /// Explicit conversion to entity. /// @@ -103,7 +91,7 @@ public static explicit operator Entities.NotificationInstance(NotificationInstan Id = model.Id, SentOn = model.SentOn, Status = model.Status, - Response = JsonDocument.Parse(JsonSerializer.Serialize(model.Response)), + Response = model.Response, Subject = model.Subject, Body = model.Body, Version = model.Version ?? 0 diff --git a/services/net/reporting/Models/ReportEmailResponseModel.cs b/libs/net/models/Areas/Services/Models/Report/ReportEmailResponseModel.cs similarity index 94% rename from services/net/reporting/Models/ReportEmailResponseModel.cs rename to libs/net/models/Areas/Services/Models/Report/ReportEmailResponseModel.cs index ab60eb10e8..b6d6fe9a30 100644 --- a/services/net/reporting/Models/ReportEmailResponseModel.cs +++ b/libs/net/models/Areas/Services/Models/Report/ReportEmailResponseModel.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace TNO.Services.Reporting.Models; +namespace TNO.API.Areas.Services.Models.Report; /// /// ReportEmailResponseModel class, provides a model to keep CHES email responses. diff --git a/libs/net/models/Areas/Services/Models/Report/ReportInstanceModel.cs b/libs/net/models/Areas/Services/Models/Report/ReportInstanceModel.cs index 1257646446..32b22979b2 100644 --- a/libs/net/models/Areas/Services/Models/Report/ReportInstanceModel.cs +++ b/libs/net/models/Areas/Services/Models/Report/ReportInstanceModel.cs @@ -56,6 +56,12 @@ public class ReportInstanceModel : AuditColumnsModel /// public string Body { get; set; } = ""; + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string LinkOnlyBody { get; set; } = ""; + /// /// get/set - CHES response containing keys to find the status of a report. /// @@ -90,6 +96,7 @@ public ReportInstanceModel(Entities.ReportInstance entity, JsonSerializerOptions this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; + this.LinkOnlyBody = entity.LinkOnlyBody; this.Content = entity.ContentManyToMany.OrderBy(c => c.SectionName).ThenBy(c => c.SortOrder).Select(m => new ReportInstanceContentModel(m)).ToArray(); } @@ -111,6 +118,7 @@ public static explicit operator Entities.ReportInstance(ReportInstanceModel mode Response = model.Response, Subject = model.Subject, Body = model.Body, + LinkOnlyBody = model.LinkOnlyBody, Version = model.Version ?? 0 }; return entity; diff --git a/libs/net/models/Areas/Services/Models/Report/ChesReportMessagesModel.cs b/libs/net/models/Areas/Services/Models/Report/SmtpReportMessagesModel.cs similarity index 63% rename from libs/net/models/Areas/Services/Models/Report/ChesReportMessagesModel.cs rename to libs/net/models/Areas/Services/Models/Report/SmtpReportMessagesModel.cs index 39b73ae640..629b4f566f 100644 --- a/libs/net/models/Areas/Services/Models/Report/ChesReportMessagesModel.cs +++ b/libs/net/models/Areas/Services/Models/Report/SmtpReportMessagesModel.cs @@ -1,10 +1,13 @@ +using System.Text.Json; + namespace TNO.API.Areas.Services.Models.Report; /// -/// ChesReportMessagesModel class, provides a model to pass CHES report response message Ids. +/// SmtpReportMessagesModel class, provides a model to pass SMTP report response message Ids. /// -public class ChesReportMessagesModel +public class SmtpReportMessagesModel { + #region Properties /// /// get/set - The report type. /// @@ -28,7 +31,7 @@ public class ChesReportMessagesModel /// /// get/set - The primary key to the user. /// - public int UserId { get; set; } + public int? UserId { get; set; } /// /// get/set - When the report was sent. @@ -41,7 +44,10 @@ public class ChesReportMessagesModel public Entities.ReportStatus Status { get; set; } /// - /// get/set - The CHES message Ids. + /// get/set - The mail response that was saved for this report. + /// If UserId is nul, this is the response for the whole report which will normally include two arrays for the link and full text report responses. + /// If UserId is not null, this is the response for the individual user report instances. /// - public IEnumerable MessageIds { get; set; } = []; + public JsonDocument Response { get; set; } = JsonDocument.Parse("{}"); + #endregion } diff --git a/libs/net/models/Areas/Services/Models/ReportInstance/ReportInstanceModel.cs b/libs/net/models/Areas/Services/Models/ReportInstance/ReportInstanceModel.cs index de5028153a..32cac7000e 100644 --- a/libs/net/models/Areas/Services/Models/ReportInstance/ReportInstanceModel.cs +++ b/libs/net/models/Areas/Services/Models/ReportInstance/ReportInstanceModel.cs @@ -56,6 +56,12 @@ public class ReportInstanceModel : AuditColumnsModel /// public string Body { get; set; } = ""; + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string LinkOnlyBody { get; set; } = ""; + /// /// get/set - CHES response containing keys to find the status of a report. /// @@ -90,6 +96,7 @@ public ReportInstanceModel(Entities.ReportInstance entity, JsonSerializerOptions this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; + this.LinkOnlyBody = entity.LinkOnlyBody; this.Content = entity.ContentManyToMany.OrderBy(c => c.SectionName).ThenBy(c => c.SortOrder).Select(m => new ReportInstanceContentModel(m)).ToArray(); } @@ -111,6 +118,7 @@ public static explicit operator Entities.ReportInstance(ReportInstanceModel mode Response = model.Response, Subject = model.Subject, Body = model.Body, + LinkOnlyBody = model.LinkOnlyBody, Version = model.Version ?? 0 }; return entity; diff --git a/libs/net/models/Areas/Subscriber/Models/Report/ReportInstanceModel.cs b/libs/net/models/Areas/Subscriber/Models/Report/ReportInstanceModel.cs index 406f22127b..b7aaf05b5a 100644 --- a/libs/net/models/Areas/Subscriber/Models/Report/ReportInstanceModel.cs +++ b/libs/net/models/Areas/Subscriber/Models/Report/ReportInstanceModel.cs @@ -56,6 +56,12 @@ public class ReportInstanceModel : AuditColumnsModel /// public string Body { get; set; } = ""; + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string LinkOnlyBody { get; set; } = ""; + /// /// get/set - CHES response containing keys to find the status of a report. /// @@ -89,6 +95,7 @@ public ReportInstanceModel(Entities.ReportInstance entity) : base(entity) this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; + this.LinkOnlyBody = entity.LinkOnlyBody; this.Content = entity.ContentManyToMany.OrderBy(c => c.SectionName).ThenBy(c => c.SortOrder).Select(c => new ReportInstanceContentModel(c)).ToArray(); } @@ -111,6 +118,7 @@ public static explicit operator Entities.ReportInstance(ReportInstanceModel mode Response = model.Response, Subject = model.Subject, Body = model.Body, + LinkOnlyBody = model.LinkOnlyBody, Version = model.Version ?? 0 }; return entity; diff --git a/libs/net/models/Areas/Subscriber/Models/ReportInstance/ReportInstanceModel.cs b/libs/net/models/Areas/Subscriber/Models/ReportInstance/ReportInstanceModel.cs index b30dde6d82..9d2664903e 100644 --- a/libs/net/models/Areas/Subscriber/Models/ReportInstance/ReportInstanceModel.cs +++ b/libs/net/models/Areas/Subscriber/Models/ReportInstance/ReportInstanceModel.cs @@ -51,6 +51,12 @@ public class ReportInstanceModel : AuditColumnsModel /// public string Body { get; set; } = ""; + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string LinkOnlyBody { get; set; } = ""; + /// /// get/set - CHES response containing keys to find the status of a report. /// @@ -83,6 +89,7 @@ public ReportInstanceModel(Entities.ReportInstance entity) : base(entity) this.Response = entity.Response; this.Subject = entity.Subject; this.Body = entity.Body; + this.LinkOnlyBody = entity.LinkOnlyBody; this.Content = entity.ContentManyToMany.OrderBy(c => c.SectionName).ThenBy(c => c.SortOrder).Select(m => new ReportInstanceContentModel(m)); } @@ -105,6 +112,7 @@ public static explicit operator Entities.ReportInstance(ReportInstanceModel mode Response = model.Response, Subject = model.Subject, Body = model.Body, + LinkOnlyBody = model.LinkOnlyBody, Version = model.Version ?? 0 }; return entity; diff --git a/libs/net/services/Actions/Managers/IngestActionManager`.cs b/libs/net/services/Actions/Managers/IngestActionManager`.cs index 5260735763..facd91bb83 100644 --- a/libs/net/services/Actions/Managers/IngestActionManager`.cs +++ b/libs/net/services/Actions/Managers/IngestActionManager`.cs @@ -1,8 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Ingest; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Extensions; using TNO.Entities; using TNO.Models.Extensions; @@ -39,20 +38,18 @@ public class IngestActionManager : ServiceActionManager, IIn /// /// /// - /// - /// + /// /// /// /// public IngestActionManager( IngestModel ingest, IApiService api, - IChesService ches, - IOptions chesOptions, + IEmailService emailService, IIngestAction action, IOptions options, ILogger logger) - : base(ches, chesOptions, action, options, logger) + : base(emailService, action, options, logger) { this.Ingest = ingest; this.Api = api; diff --git a/libs/net/services/Actions/Managers/ServiceActionManager`.cs b/libs/net/services/Actions/Managers/ServiceActionManager`.cs index ba3db1f91c..84c915274e 100644 --- a/libs/net/services/Actions/Managers/ServiceActionManager`.cs +++ b/libs/net/services/Actions/Managers/ServiceActionManager`.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; +using MMI.SmtpEmail; using TNO.Core.Extensions; using TNO.Services.Config; @@ -15,8 +14,7 @@ public abstract class ServiceActionManager : IServiceActionManager { #region Variables private readonly IServiceAction _action; - private readonly IChesService _ches; - private readonly ChesOptions _chesOptions; + private readonly IEmailService _emailService; #endregion #region Properties @@ -40,20 +38,17 @@ public abstract class ServiceActionManager : IServiceActionManager /// /// Creates a new instance of a ServiceActionManager object, initializes with specified parameters. /// - /// - /// + /// /// /// /// public ServiceActionManager( - IChesService ches, - IOptions chesOptions, + IEmailService emailService, IServiceAction action, IOptions options, ILogger logger) { - _ches = ches; - _chesOptions = chesOptions.Value; + _emailService = emailService; _action = action; this.Options = options.Value; this.Logger = logger; @@ -181,8 +176,8 @@ public async Task SendEmailAsync(string subject, string message) try { var emailToList = this.Options.EmailTo?.Split(',').Where(v => !String.IsNullOrWhiteSpace(v)).Select(v => v.Trim()).ToArray() ?? Array.Empty(); - var email = new TNO.Ches.Models.EmailModel(_chesOptions.From, emailToList, subject, message); - await _ches.SendEmailAsync(email); + var email = _emailService.CreateMailMessage(subject, message, emailToList, null, null, null, true); + await _emailService.SendAsync(email); } catch (Exception ex) { diff --git a/libs/net/services/Helpers/ApiService.cs b/libs/net/services/Helpers/ApiService.cs index 061c0154c9..9770ba6ad7 100644 --- a/libs/net/services/Helpers/ApiService.cs +++ b/libs/net/services/Helpers/ApiService.cs @@ -773,10 +773,21 @@ public async Task UploadFilesToS3Async( /// /// /// - public async Task?> GetChesMessagesAsync(Entities.NotificationStatus status, DateTime cutOff) + public async Task?> GetSmtpMessagesAsync(Entities.NotificationStatus status, DateTime cutOff) { var url = this.Options.ApiUrl.Append($"services/notifications/sent/{status}?cutOff={cutOff.ToUniversalTime():o}"); - return await RetryRequestAsync(async () => await this.OpenClient.GetAsync>(url)); + return await RetryRequestAsync(async () => await this.OpenClient.GetAsync>(url)); + } + + /// + /// Get the specified notification instance. + /// + /// + /// + public async Task GetNotificationInstanceAsync(long instanceId) + { + var url = this.Options.ApiUrl.Append($"services/notification/instances/{instanceId}"); + return await RetryRequestAsync(async () => await this.OpenClient.GetAsync(url)); } /// @@ -888,6 +899,18 @@ public async Task UploadFilesToS3Async( return await RetryRequestAsync(async () => await this.OpenClient.PutAsync(url, JsonContent.Create(instance))); } + /// + /// Get user report instance for the specified 'id'. + /// + /// + /// + /// + public async Task GetUserReportInstanceAsync(long instanceId, int userId) + { + var url = this.Options.ApiUrl.Append($"services/report/instances/{instanceId}/users/{userId}"); + return await RetryRequestAsync(async () => await this.OpenClient.GetAsync(url)); + } + /// /// Get user report instance for the specified 'id'. /// @@ -923,15 +946,15 @@ public async Task UploadFilesToS3Async( } /// - /// Get the CHES message Ids for the reports in the specified 'status' and that were sent on or after the 'cutOff' date and time. + /// Get the SMTP message Ids for the reports in the specified 'status' and that were sent on or after the 'cutOff' date and time. /// /// /// /// - public async Task?> GetChesMessagesAsync(Entities.ReportStatus status, DateTime cutOff) + public async Task?> GetSmtpMessagesAsync(Entities.ReportStatus status, DateTime cutOff) { var url = this.Options.ApiUrl.Append($"services/reports/sent/{status}?cutOff={cutOff.ToUniversalTime():o}"); - return await RetryRequestAsync(async () => await this.OpenClient.GetAsync>(url)); + return await RetryRequestAsync(async () => await this.OpenClient.GetAsync>(url)); } /// @@ -1075,6 +1098,18 @@ public async Task UploadFilesToS3Async( return await RetryRequestAsync(async () => await this.OpenClient.PutAsync(url, JsonContent.Create(model))); } + /// + /// Get user report instance for the specified 'id'. + /// + /// + /// + /// + public async Task GetUserAVOverviewInstanceAsync(long id, int userId) + { + var url = this.Options.ApiUrl.Append($"services/reports/av/overviews/{id}/users/{userId}"); + return await RetryRequestAsync(async () => await this.OpenClient.GetAsync(url)); + } + /// /// Get user report instance for the specified 'id'. /// diff --git a/libs/net/services/Helpers/IApiService.cs b/libs/net/services/Helpers/IApiService.cs index 4be65e8c72..2303839fc0 100644 --- a/libs/net/services/Helpers/IApiService.cs +++ b/libs/net/services/Helpers/IApiService.cs @@ -378,12 +378,19 @@ public Task UploadFilesToS3Async( Task?> FindContentForNotificationIdAsync(int id, int? requestorId); /// - /// Get the CHES message Ids for the notifications in the specified 'status' and that were sent on or after the 'cutOff' date and time. + /// Get the SMTP message Ids for the notifications in the specified 'status' and that were sent on or after the 'cutOff' date and time. /// /// /// /// - Task?> GetChesMessagesAsync(Entities.NotificationStatus status, DateTime cutOff); + Task?> GetSmtpMessagesAsync(Entities.NotificationStatus status, DateTime cutOff); + + /// + /// Get the specified notification instance. + /// + /// + /// + Task GetNotificationInstanceAsync(long instanceId); /// /// Update the status of the specified notification instance. @@ -455,6 +462,14 @@ public Task UploadFilesToS3Async( /// Task ClearFoldersInReport(int reportId); + /// + /// Get user report instance for the specified 'id'. + /// + /// + /// + /// + Task GetUserReportInstanceAsync(long instanceId, int userId); + /// /// Get user report instance for the specified 'id'. /// @@ -477,12 +492,12 @@ public Task UploadFilesToS3Async( Task> AddOrUpdateUserReportInstancesAsync(IEnumerable instances); /// - /// Get the CHES message Ids for the reports in the specified 'status' and that were sent on or after the 'cutOff' date and time. + /// Get the SMTP message Ids for the reports in the specified 'status' and that were sent on or after the 'cutOff' date and time. /// /// /// /// - Task?> GetChesMessagesAsync(Entities.ReportStatus status, DateTime cutOff); + Task?> GetSmtpMessagesAsync(Entities.ReportStatus status, DateTime cutOff); /// /// Update the status of the specified report instance. @@ -550,6 +565,13 @@ public Task UploadFilesToS3Async( /// Task UpdateAVOverviewInstanceAsync(API.Areas.Services.Models.AVOverview.AVOverviewInstanceModel model); + /// + /// Get user report instance for the specified 'id'. + /// + /// + /// + /// + Task GetUserAVOverviewInstanceAsync(long id, int userId); /// /// Get user report instance for the specified 'id'. diff --git a/libs/net/services/Managers/IngestManager`.cs b/libs/net/services/Managers/IngestManager`.cs index 634f84de42..79a4db38df 100644 --- a/libs/net/services/Managers/IngestManager`.cs +++ b/libs/net/services/Managers/IngestManager`.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Ingest; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Services.Config; @@ -36,20 +35,20 @@ public abstract class IngestManager : ServiceManager /// Service to communicate with the api. - /// - /// + /// + /// /// Ingest manager factory. /// Configuration options. /// Logging client. public IngestManager( IServiceProvider serviceProvider, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IngestManagerFactory factory, IOptions options, ILogger> logger) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { _factory = factory; _serviceScope = serviceProvider.CreateScope(); diff --git a/libs/net/services/Managers/ServiceManager`.cs b/libs/net/services/Managers/ServiceManager`.cs index d30d683c27..3aee40b2f7 100644 --- a/libs/net/services/Managers/ServiceManager`.cs +++ b/libs/net/services/Managers/ServiceManager`.cs @@ -1,8 +1,7 @@ +using System.Net.Mail; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; -using TNO.Core.Exceptions; +using MMI.SmtpEmail; using TNO.Services.Config; namespace TNO.Services.Managers; @@ -53,12 +52,12 @@ public abstract class ServiceManager : IServiceManager /// /// get - CHES service. /// - protected IChesService Ches { get; } + protected IEmailService EmailService { get; } /// - /// get - CHES options. + /// get - Smtp options for sending emails. /// - protected ChesOptions ChesOptions { get; } + protected SmtpOptions SmtpOptions { get; } #endregion #region Constructors @@ -66,22 +65,22 @@ public abstract class ServiceManager : IServiceManager /// Creates a new instance of a ServiceManager object, initializes with specified parameters. /// /// Service to communicate with the api. - /// - /// + /// + /// /// Configuration options. /// Logging client. public ServiceManager( IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger> logger) { this.Api = api; // All requests will be identified by the service type name. this.Api.OpenClient.Client.DefaultRequestHeaders.Add("User-Agent", GetType().FullName); - this.Ches = chesService; - this.ChesOptions = chesOptions.Value; + this.EmailService = emailService; + this.SmtpOptions = smtpOptions.Value; this.Options = options.Value; this.Logger = logger; this.State = new ServiceState(this.Options); @@ -117,17 +116,17 @@ public async Task SendEmailAsync(string subject, string message, EmailToType ema } if (emailToList.Any()) { - var email = new TNO.Ches.Models.EmailModel(this.ChesOptions.From, emailToList, subject, message); - await this.Ches.SendEmailAsync(email); + var email = this.EmailService.CreateMailMessage(subject, message, emailToList, null, null, null, true); + await this.EmailService.SendAsync(email); } else { this.Logger.LogDebug("No email addresses configured to receive errors"); } } - catch (ChesException ex) + catch (SmtpException ex) { - this.Logger.LogError(ex, "Ches exception while sending email. {response}", ex.Data.Contains("body") ? ex.Data["body"] : ex.Message); + this.Logger.LogError(ex, "Smtp exception while sending email. {response}", ex.Data.Contains("body") ? ex.Data["body"] : ex.Message); } catch (Exception ex) { diff --git a/libs/net/services/Runners/BaseService.cs b/libs/net/services/Runners/BaseService.cs index 0576df8619..1b08f8e860 100644 --- a/libs/net/services/Runners/BaseService.cs +++ b/libs/net/services/Runners/BaseService.cs @@ -11,9 +11,8 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using Serilog; -using Serilog.Formatting.Compact; -using TNO.Ches; using TNO.Core.Http; using TNO.Core.Http.Configuration; using TNO.Services.Config; @@ -137,7 +136,7 @@ protected virtual IServiceCollection ConfigureServices(IServiceCollection servic builder.AddSerilog(dispose: true); }) .AddTransient() - .AddChesSingletonService(this.Configuration.GetSection("CHES")) + .AddSmtpEmail(this.Configuration.GetSection("Smtp")) .AddSingleton(new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Email, "") }))) .Configure(this.Configuration.GetSection("Auth:Keycloak")) .Configure(this.Configuration.GetSection("Auth:OIDC")) diff --git a/libs/net/services/TNO.Services.csproj b/libs/net/services/TNO.Services.csproj index afeeaf6fae..c32e0f274c 100644 --- a/libs/net/services/TNO.Services.csproj +++ b/libs/net/services/TNO.Services.csproj @@ -25,7 +25,7 @@ - + diff --git a/libs/net/smtp/Configuration/SmtpOptions.cs b/libs/net/smtp/Configuration/SmtpOptions.cs new file mode 100644 index 0000000000..127e8e59c5 --- /dev/null +++ b/libs/net/smtp/Configuration/SmtpOptions.cs @@ -0,0 +1,72 @@ +namespace MMI.SmtpEmail; + +/// +/// Represents the configuration options for the SMTP email service. This class contains properties for configuring the SMTP server connection, default from address, email sending behavior, and recipient overrides. These options can be configured through dependency injection using the ServicesExtensions methods, allowing for flexible configuration of the email service in different environments and scenarios. +/// +public class SmtpOptions +{ + /// + /// get/set - The SMTP server host. Default is "localhost". + /// + public string Host { get; set; } = "localhost"; + + /// + /// get/set - The SMTP server port. Default is 25. + /// + public int Port { get; set; } = 25; + + /// + /// get/set - Whether SSL should be used when connecting to the SMTP server. Default is false. + /// + public bool EnableSsl { get; set; } = true; + + /// + /// get/set - Whether the default credentials should be used when connecting to the SMTP server. Default is false. + /// + public bool UseDefaultCredentials { get; set; } = false; + + /// + /// get/set - The username to use when connecting to the SMTP server. Default is empty string. + /// + public string Username { get; set; } = string.Empty; + + /// + /// get/set - The password to use when connecting to the SMTP server. Default is empty string. + /// + public string Password { get; set; } = string.Empty; + + /// + /// get/set - The default from address to use when sending email. Default is empty string, which will require the from address to be specified on each email. If set, this will be used as the from address for any email that does not specify a from address. + /// + public string From { get; set; } = string.Empty; + + /// + /// get/set - The timeout to use when sending email. Default is null, which uses the SmtpClient default timeout of 100 seconds. + /// + public TimeSpan? Timeout { get; set; } + + /// + /// get/set - Whether email sending is enabled. Default is true. If false, the EmailService will not send emails and will instead log the email content at the Information level. This can be used to disable email sending in development or testing environments without having to change the code that sends emails. + /// + public bool EmailEnabled { get; set; } = true; + + /// + /// get/set - Whether email sending is authorized. Default is true. If false, the EmailService will only send emails to the user who initiaded the email (determined by Thread.CurrentPrincipal.Identity.Name) and to the email address specified in AlwaysBcc. This can be used to prevent users from sending emails to unintended recipients while still allowing them to receive a copy of the emails they generate. + /// + public bool EmailAuthorized { get; set; } = true; + + /// + /// get/set - Always BCC the user who generated the email. Default is false. If true, the EmailService will attempt to add the current user to the BCC of every email sent. The current user is determined by checking Thread.CurrentPrincipal.Identity.Name, so this will only work if the application is using authentication and the user's identity name is set to their email address. This can be used to ensure that the user receives a copy of every email they generate, even if they are not in the To or CC fields. + /// + public bool BccUser { get; set; } + + /// + /// get/set - Always BCC the specified email address. Default is null. If set, the EmailService will add this email address to the BCC of every email sent. This can be used to ensure that a specific email address receives a copy of every email sent, regardless of the recipients specified in the To and CC fields. + /// + public string? AlwaysBcc { get; set; } + + /// + /// get/set - Send all emails to this email address instead of their original recipients. Default is null. If set, the EmailService will ignore the To and CC fields of the email and instead send the email to this address. This can be used in development or testing environments to prevent emails from being sent to real recipients while still allowing the email sending code to be exercised and the email content to be reviewed by sending it to a test address. + /// + public string? OverrideTo { get; set; } +} diff --git a/libs/net/smtp/EmailService.cs b/libs/net/smtp/EmailService.cs new file mode 100644 index 0000000000..550458b10d --- /dev/null +++ b/libs/net/smtp/EmailService.cs @@ -0,0 +1,682 @@ +using System.Net; +using System.Net.Mail; +using System.Security.Claims; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MMI.SmtpEmail.Models; +using TNO.Core.Extensions; + +namespace MMI.SmtpEmail; + +/// +/// EmailService class provides methods to send emails using SMTP. It can be configured with SmtpOptions and can use an injected SmtpClient or create its own. Implements IDisposable to dispose the SmtpClient if it owns it. +/// +public partial class EmailService : IEmailService, IDisposable +{ + #region Variables + [GeneratedRegex("src=\\\"data:(image\\/[a-zA-Z]*);base64,([^\\\"]*)\\\"", RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-CA")] + private static partial Regex Base64InlineImageRegex(); + private static readonly Regex ExtractBase64InlineImageRegex = Base64InlineImageRegex(); + private SmtpClient? _client; + private readonly bool _ownsClient; + private readonly ILogger _logger; + private readonly ClaimsPrincipal _user; + #endregion + + #region Properties + /// + /// get - The SmtpOptions used to configure the EmailService and its SmtpClient. This is exposed as a property so it can be accessed when creating MailMessage objects to apply any relevant options such as OverrideTo and BccUser. The options are set through the constructor and are typically provided via dependency injection using IOptions. If options is null, default SmtpOptions will be used. + /// + protected SmtpOptions Options { get; } + #endregion + + #region Constructors + /// + /// Default constructor initializes with default SmtpOptions and creates its own SmtpClient. + /// + /// + /// + public EmailService(ClaimsPrincipal user, ILogger logger) + { + _user = user; + this.Options = new SmtpOptions(); + _ownsClient = true; + _logger = logger; + } + + /// + /// Constructor that accepts IOptions to configure the SmtpClient. The EmailService will create and own the SmtpClient instance. If options is null, default SmtpOptions will be used. + /// + /// + /// + /// + public EmailService(ClaimsPrincipal user, IOptions options, ILogger logger) + { + _user = user; + this.Options = options?.Value ?? new SmtpOptions(); + _ownsClient = true; + _logger = logger; + } + + /// + /// Constructor that accepts an existing SmtpClient and optional IOptions to configure the EmailService. The EmailService will not own the SmtpClient instance. + /// + /// + /// + /// + /// + /// + public EmailService(ClaimsPrincipal user, SmtpClient client, IOptions? options = null, ILogger logger = null!) + { + _user = user; + this.Options = options?.Value ?? new SmtpOptions(); + _client = client ?? throw new ArgumentNullException(nameof(client)); + _ownsClient = false; + _logger = logger; + } + #endregion + + #region Methods + /// + /// Sends an email asynchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task SendAsync( + string subject, + string body, + IEnumerable to, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null, + CancellationToken cancellationToken = default) + { + await SendAsync(subject, body, to, null, null, from, isHtml, attachments, cancellationToken).ConfigureAwait(false); + + return new MailResponseModel(to); + } + + /// + /// Sends an email asynchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task SendAsync( + string subject, + string body, + IEnumerable to, + IEnumerable? cc, + IEnumerable? bcc, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null, + CancellationToken cancellationToken = default) + { + _client ??= CreateSmtpClient(); + + try + { + using var message = CreateMailMessage(subject, body, to, cc, bcc, from, isHtml, attachments); + if (this.Options.EmailEnabled) + { + await _client.SendMailAsync(message, cancellationToken).WaitAsync(cancellationToken).ConfigureAwait(false); + return new MailResponseModel(to, cc, bcc); + } + + _logger.LogWarning("Email sending is disabled. Email content: {EmailContent}", new + { + Subject = message.Subject, + To = message.To.Select(r => r.Address).ToArray(), + From = message.From?.Address, + }); + return new MailResponseModel(to, cc, bcc) + { + StatusCode = SmtpStatusCode.CommandNotImplemented + }; + } + finally + { + if (_ownsClient) + { + _client?.Dispose(); + _client = null; + } + } + } + + /// + /// Sends an email asynchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// Each context provided will generate a separate email with the subject and body merged with the context using MailMergeModel.Merge. This allows for sending personalized emails to multiple recipients in a single call. The to, cc, and bcc parameters will be applied to all generated emails, but the subject and body can be customized for each email based on the context. + /// Email merging is done by replacing placeholders in the subject and body with values from the context. Placeholders are defined as {{PropertyName}} where PropertyName is a property of the MailMergeModel. For example, if the context has a property called FirstName, you could use {{FirstName}} in the subject or body and it would be replaced with the value of that property for each email sent. This allows for dynamic generation of email content based on the provided contexts. + /// + /// An array of MailMergeModel objects containing the context for each email to be sent. + /// + /// + /// + /// + /// + /// + /// An array of tuples containing the context and any error that occurred while sending the email. + public async Task<(bool success, MailResponseModel[] responses)> SendAsync( + IEnumerable contexts, + string subject, + string body, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null, + CancellationToken cancellationToken = default) + { + var success = true; + var results = new List(); + foreach (var context in contexts) + { + // For each context, create a + var mergedSubject = context.Merge(subject); + var mergedBody = context.Merge(body); + await SendAsync(mergedSubject, mergedBody, context.To, context.CC, context.Bcc, from, isHtml, attachments, cancellationToken).WaitAsync(cancellationToken).ConfigureAwait(false); + results.Add(new MailResponseModel(context)); + } + return (success, results.ToArray()); + } + + /// + /// Sends an email asynchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// Each context provided will generate a separate email with the subject and body merged with the context using MailMergeModel.Merge. This allows for sending personalized emails to multiple recipients in a single call. The to, cc, and bcc parameters will be applied to all generated emails, but the subject and body can be customized for each email based on the context. + /// Email merging is done by replacing placeholders in the subject and body with values from the context. Placeholders are defined as {{PropertyName}} where PropertyName is a property of the MailMergeModel. For example, if the context has a property called FirstName, you could use {{FirstName}} in the subject or body and it would be replaced with the value of that property for each email sent. This allows for dynamic generation of email content based on the provided contexts. + /// + /// An array of MailMergeModel objects containing the context for each email to be sent. + /// + /// + /// + /// + /// + /// + /// An array of tuples containing the context and any error that occurred while sending the email. + public async Task<(bool success, MailResponseModel[] responses)> TrySendAsync( + IEnumerable contexts, + string subject, + string body, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null, + CancellationToken cancellationToken = default) + { + var success = true; + var results = new List(); + foreach (var context in contexts) + { + // For each context, create a + var mergedSubject = context.Merge(subject); + var mergedBody = context.Merge(body); + try + { + await SendAsync(mergedSubject, mergedBody, context.To, context.CC, context.Bcc, from, isHtml, attachments, cancellationToken).WaitAsync(cancellationToken).ConfigureAwait(false); + results.Add(new MailResponseModel(context)); + } + catch (SmtpException ex) + { + _logger.LogError(ex, "Error sending email for context {context}", String.Join(',', context.To)); + success = false; + results.Add(new MailResponseModel(context, ex)); + } + } + return (success, results.ToArray()); + } + + /// + /// Sends an email asynchronously using a pre-constructed MailMessage object. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. The MailMessage should be fully constructed with from, to, subject, body, and any other necessary properties before calling this method. + /// + /// + /// + /// + public async Task SendAsync(MailMessage message, CancellationToken cancellationToken = default) + { + _client ??= CreateSmtpClient(); + + try + { + if (this.Options.EmailEnabled) + { + await _client.SendMailAsync(message, cancellationToken).WaitAsync(cancellationToken).ConfigureAwait(false); + return new MailResponseModel(message); + } + + _logger.LogWarning("Email sending is disabled. Email content: {EmailContent}", new + { + Subject = message.Subject, + To = message.To.Select(r => r.Address).ToArray(), + From = message.From?.Address, + }); + return new MailResponseModel(message) + { + StatusCode = SmtpStatusCode.CommandNotImplemented + }; + } + finally + { + if (_ownsClient) + { + _client?.Dispose(); + _client = null; + } + } + } + + /// + /// Sends multiple emails asynchronously using a collection of pre-constructed MailMessage objects. Uses the configured SmtpClient to send each email. If cancellation is requested, the operation will be cancelled. Each MailMessage in the collection should be fully constructed with from, to, subject, body, and any other necessary properties before calling this method. If the messages collection is null, no action will be taken. If any individual message is null, it will be skipped. + /// + /// + /// + /// A tuple containing the message and every response for each email. + public async Task<(bool success, MailResponseModel[] responses)> SendAsync(IEnumerable messages, CancellationToken cancellationToken = default) + { + if (messages == null) return (false, []); + var success = true; + var results = new List(); + foreach (var msg in messages) + { + if (msg == null) continue; + await SendAsync(msg, cancellationToken).WaitAsync(cancellationToken).ConfigureAwait(false); + results.Add(new MailResponseModel(msg)); + } + return (success, [.. results]); + } + + /// + /// Sends multiple emails asynchronously using a collection of pre-constructed MailMessage objects. Uses the configured SmtpClient to send each email. If cancellation is requested, the operation will be cancelled. Each MailMessage in the collection should be fully constructed with from, to, subject, body, and any other necessary properties before calling this method. If the messages collection is null, no action will be taken. If any individual message is null, it will be skipped. + /// + /// + /// + /// A tuple containing the message and every response for each email. + public async Task<(bool success, MailResponseModel[] responses)> TrySendAsync(IEnumerable messages, CancellationToken cancellationToken = default) + { + if (messages == null) return (false, []); + var success = true; + var results = new List(); + foreach (var msg in messages) + { + if (msg == null) continue; + try + { + await SendAsync(msg, cancellationToken).WaitAsync(cancellationToken).ConfigureAwait(false); + results.Add(new MailResponseModel(msg)); + } + catch (SmtpException ex) + { + _logger.LogError(ex, "Error sending email for context {message}", msg.To.ToString()); + success = false; + results.Add(new MailResponseModel(msg, ex)); + } + } + return (success, results.ToArray()); + } + + /// + /// Sends an email synchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// + /// + /// + /// + /// + /// + public MailResponseModel Send( + string subject, + string body, + IEnumerable to, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null) + { + Send(subject, body, to, null, null, from, isHtml, attachments); + return new MailResponseModel(to); + } + + /// + /// Sends an email synchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// + /// + /// + /// param name="cc"> + /// + /// + /// + /// + public MailResponseModel Send( + string subject, + string body, + IEnumerable to, + IEnumerable? cc, + IEnumerable? bcc, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null) + { + _client ??= CreateSmtpClient(); + + try + { + using var message = CreateMailMessage(subject, body, to, cc, bcc, from, isHtml, attachments); + + if (this.Options.EmailEnabled) + { + _client.Send(message); + return new MailResponseModel(message); + } + + _logger.LogWarning("Email sending is disabled. Email content: {EmailContent}", new + { + Subject = message.Subject, + To = message.To.Select(r => r.Address).ToArray(), + From = message.From?.Address, + }); + return new MailResponseModel(message) + { + StatusCode = SmtpStatusCode.CommandNotImplemented + }; + } + finally + { + if (_ownsClient) + { + _client?.Dispose(); + _client = null; + } + } + } + + /// + /// Sends an email synchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// An array of MailMergeModel objects containing the context for each email to be sent. + /// + /// + /// + /// + /// + /// + public (bool success, MailResponseModel[] responses) Send( + IEnumerable contexts, + string subject, + string body, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null) + { + var success = true; + var results = new List(); + foreach (var context in contexts) + { + // For each context, create a + var mergedSubject = context.Merge(subject); + var mergedBody = context.Merge(body); + Send(mergedSubject, mergedBody, context.To, context.CC, context.Bcc, from, isHtml, attachments); + results.Add(new MailResponseModel(context)); + } + return (success, [.. results]); + } + + /// + /// Sends an email synchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// An array of MailMergeModel objects containing the context for each email to be sent. + /// + /// + /// + /// + /// + public (bool success, MailResponseModel[] responses) TrySend( + IEnumerable contexts, + string subject, + string body, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null) + { + var success = true; + var results = new List(); + foreach (var context in contexts) + { + // For each context, create a + var mergedSubject = context.Merge(subject); + var mergedBody = context.Merge(body); + try + { + Send(mergedSubject, mergedBody, context.To, context.CC, context.Bcc, from, isHtml, attachments); + results.Add(new MailResponseModel(context)); + } + catch (SmtpException ex) + { + _logger.LogError(ex, "Error sending email for context {context}", String.Join(',', context.To)); + success = false; + results.Add(new MailResponseModel(context, ex)); + } + } + return (success, [.. results]); + } + + /// + /// Creates a MailMessage object based on the provided parameters and the default from address in options. If from address is not specified and no default from address is available, an exception will be thrown. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public MailMessage CreateMailMessage( + string subject, + string body, + IEnumerable to, + IEnumerable? cc = null, + IEnumerable? bcc = null, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null) + { + var message = new MailMessage(); + var fromAddress = string.IsNullOrWhiteSpace(from) ? this.Options.From : from!; + if (string.IsNullOrWhiteSpace(fromAddress)) throw new ArgumentException("No from address specified."); + message.From = new MailAddress(fromAddress); + foreach (var recipient in to.NotNullOrWhiteSpace()) + { + if (!string.IsNullOrWhiteSpace(recipient)) message.To.Add(recipient); + } + if (cc != null) + { + foreach (var recipient in cc.NotNullOrWhiteSpace()) + { + if (!string.IsNullOrWhiteSpace(recipient)) message.CC.Add(recipient); + } + } + if (bcc != null) + { + foreach (var recipient in bcc.NotNullOrWhiteSpace()) + { + if (!string.IsNullOrWhiteSpace(recipient)) message.Bcc.Add(recipient); + } + } + if (this.Options.BccUser) + { + // Always BCC the user who generated the email, if the option is enabled and the user's email can be determined. This is done by checking Thread.CurrentPrincipal.Identity.Name for an email address, so it will only work if the application is using authentication and the user's identity name is set to their email address. + var userEmail = _user.GetEmail(); + if (!string.IsNullOrWhiteSpace(userEmail)) + message.Bcc.Add(userEmail); + } + + if (!String.IsNullOrWhiteSpace(this.Options.AlwaysBcc)) + { + // Always BCC the specified email address, if the option is set. This will add the email address(es) specified in AlwaysBcc to the BCC of every email sent. The AlwaysBcc option can contain multiple email addresses separated by commas or semicolons, and they will be parsed and added individually to the BCC. + var alwaysBcc = this.Options.AlwaysBcc.Split([',', ';']).Select(e => e?.Trim()).NotNullOrWhiteSpace(); + foreach (var recipient in alwaysBcc) + { + if (!string.IsNullOrWhiteSpace(recipient) && !message.Bcc.Any(r => r.Address.Equals(recipient, StringComparison.OrdinalIgnoreCase))) + message.Bcc.Add(recipient); + } + } + if (!String.IsNullOrWhiteSpace(this.Options.OverrideTo) || !this.Options.EmailAuthorized) + { + // If OverrideTo is set, send all emails to the specified address(es) instead of the original recipients. This is typically used in development or testing environments to prevent emails from being sent to real recipients while still allowing the email sending code to be exercised and the email content to be reviewed by sending it to a test address. If EmailAuthorized is false, only send emails to the user who generated the email (determined by Thread.CurrentPrincipal.Identity.Name) and to the email address specified in AlwaysBcc, regardless of what is specified in the To and CC fields. This can be used in production environments to prevent users from sending emails to unintended recipients while still allowing them to receive a copy of the emails they generate. + var overrideTo = !String.IsNullOrWhiteSpace(this.Options.OverrideTo) + ? this.Options.OverrideTo.Split(";").NotNullOrWhiteSpace().Select(e => e.Trim()) + : new[] { _user.GetEmail() }.Where(e => !String.IsNullOrWhiteSpace(e)).Select(e => e!); + foreach (var recipient in overrideTo) + { + if (!string.IsNullOrWhiteSpace(recipient) && !message.To.Any(r => r.Address.Equals(recipient, StringComparison.OrdinalIgnoreCase))) + message.To.Add(recipient); + } + message.CC.Clear(); + message.Bcc.Clear(); + } + + message.Subject = subject ?? string.Empty; + message.Body = body ?? string.Empty; + message.IsBodyHtml = isHtml; + + // convert any embedded base64 images into attachments + Dictionary inlineImageMatches = GetImagesFromEmailBody(message.Body); + if (inlineImageMatches.Count != 0) + { + foreach (KeyValuePair m in inlineImageMatches) + { + message.Body = message.Body.Replace(m.Key, $"src=\"cid:{m.Value.Filename}\""); + } + // Add inline images as attachments with ContentId set to the filename so they can be referenced via cid:filename in the HTML body. + foreach (var am in inlineImageMatches.Values) + { + try + { + var bytes = Convert.FromBase64String(am.Content); + var ms = new System.IO.MemoryStream(bytes); + var attachment = new Attachment(ms, am.Filename) + { + ContentId = am.Filename, + ContentType = new System.Net.Mime.ContentType(am.ContentType ?? "application/octet-stream"), + }; + if (attachment.ContentDisposition != null) + { + attachment.ContentDisposition.Inline = true; + attachment.ContentDisposition.DispositionType = System.Net.Mime.DispositionTypeNames.Inline; + } + message.Attachments.Add(attachment); + } + catch + { + // ignore invalid inline images + _logger.LogWarning("Failed to parse inline image for email. Content will be ignored. ContentType: {ContentType}, Filename: {Filename}", am.ContentType, am.Filename); + } + } + } + + // Add explicit attachments passed in by the caller. + if (attachments != null) + { + foreach (var am in attachments) + { + if (am == null) continue; + try + { + var bytes = string.IsNullOrWhiteSpace(am.Content) ? Array.Empty() : Convert.FromBase64String(am.Content); + var ms = new System.IO.MemoryStream(bytes); + var attachment = new Attachment(ms, am.Filename ?? string.Empty) + { + ContentType = new System.Net.Mime.ContentType(am.ContentType ?? "application/octet-stream") + }; + message.Attachments.Add(attachment); + } + catch + { + // ignore attachment errors + _logger.LogWarning("Failed to parse attachment for email. Attachment will be ignored. ContentType: {ContentType}, Filename: {Filename}", am.ContentType, am.Filename); + } + } + } + + return message; + } + + /// + /// Creates and configures an SmtpClient based on the SmtpOptions. If username is specified in options, it will set the credentials for the client. If timeout is specified, it will set the timeout for the client. The client is configured to use Network delivery method and SSL and default credentials based on the options. + /// + /// + private SmtpClient CreateSmtpClient() + { + var client = new SmtpClient(this.Options.Host, this.Options.Port) + { + EnableSsl = this.Options.EnableSsl, + DeliveryMethod = SmtpDeliveryMethod.Network, + UseDefaultCredentials = this.Options.UseDefaultCredentials + }; + + if (!string.IsNullOrWhiteSpace(this.Options.Username)) + { + client.Credentials = new NetworkCredential(this.Options.Username, this.Options.Password); + } + + if (this.Options.Timeout.HasValue) + client.Timeout = (int)this.Options.Timeout.Value.TotalMilliseconds; + + return client; + } + + /// + /// parses email markup string checking for base64 encoded images + /// + /// email body as html markup - possibly containing base64 encoded images + /// dictionary of the images as attachments and the 'key' to use to search and replace them in the markup + private static Dictionary GetImagesFromEmailBody(string emailBody) + { + var imageDictionary = new Dictionary(); + + var inlineImageMatches = ExtractBase64InlineImageRegex.Matches(emailBody); + if (inlineImageMatches.Count != 0) + { + foreach (var m in inlineImageMatches.Cast()) + { + var imageMediaType = m.Groups[1].Value; + var base64Image = m.Groups[2].Value; + var attachment = new AttachmentModel + { + ContentType = imageMediaType, + Encoding = "base64", + Filename = Guid.NewGuid().ToString(), + Content = base64Image + }; + imageDictionary.TryAdd(m.Value, attachment); + } + } + return imageDictionary; + } + + /// + /// Disposes the SmtpClient if this EmailService instance owns it. If the SmtpClient was injected and is not owned by this instance, it will not be disposed. Suppresses finalization after disposing. + /// + public void Dispose() + { + if (_ownsClient) + { + try { _client?.Dispose(); } catch { } + } + GC.SuppressFinalize(this); + } + #endregion +} + diff --git a/libs/net/smtp/IEmailService.cs b/libs/net/smtp/IEmailService.cs new file mode 100644 index 0000000000..f52e2f26d9 --- /dev/null +++ b/libs/net/smtp/IEmailService.cs @@ -0,0 +1,203 @@ +using System.Net.Mail; +using MMI.SmtpEmail.Models; + +namespace MMI.SmtpEmail; + +/// +/// Defines an interface for an email service that can send emails with various options including subject, body, recipients, attachments, and support for mail merge functionality. The service provides both asynchronous and synchronous methods for sending emails, as well as a method for creating a MailMessage object based on the provided parameters and default options. The SendAsync methods allow for cancellation through a CancellationToken, and the mail merge functionality allows for dynamic generation of email content based on a set of variables provided in the context. The CreateMailMessage method can be used to create a MailMessage object that can be further customized or sent using the SendAsync method that accepts a MailMessage parameter. +/// +public interface IEmailService +{ + /// + /// Sends an email asynchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// + /// + /// + /// + /// + /// + /// + Task SendAsync(string subject, string body, IEnumerable to, string? from = null, bool isHtml = true, IEnumerable? attachments = null, CancellationToken cancellationToken = default); + + /// + /// Sends an email asynchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task SendAsync( + string subject, + string body, + IEnumerable to, + IEnumerable? cc, + IEnumerable? bcc, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null, + CancellationToken cancellationToken = default); + + /// + /// Sends an email asynchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// Each context provided will generate a separate email with the subject and body merged with the context using MailMergeModel.Merge. This allows for sending personalized emails to multiple recipients in a single call. The to, cc, and bcc parameters will be applied to all generated emails, but the subject and body can be customized for each email based on the context. + /// Email merging is done by replacing placeholders in the subject and body with values from the context. Placeholders are defined as {{PropertyName}} where PropertyName is a property of the MailMergeModel. For example, if the context has a property called FirstName, you could use {{FirstName}} in the subject or body and it would be replaced with the value of that property for each email sent. This allows for dynamic generation of email content based on the provided contexts. + /// + /// An array of MailMergeModel objects containing the context for each email to be sent. + /// + /// + /// + /// + /// + /// + /// An array of tuples containing the context and any error that occurred while sending the email. + Task<(bool success, MailResponseModel[] responses)> SendAsync( + IEnumerable contexts, + string subject, + string body, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null, + CancellationToken cancellationToken = default); + + /// + /// Sends an email asynchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// Each context provided will generate a separate email with the subject and body merged with the context using MailMergeModel.Merge. This allows for sending personalized emails to multiple recipients in a single call. The to, cc, and bcc parameters will be applied to all generated emails, but the subject and body can be customized for each email based on the context. + /// Email merging is done by replacing placeholders in the subject and body with values from the context. Placeholders are defined as {{PropertyName}} where PropertyName is a property of the MailMergeModel. For example, if the context has a property called FirstName, you could use {{FirstName}} in the subject or body and it would be replaced with the value of that property for each email sent. This allows for dynamic generation of email content based on the provided contexts. + /// + /// An array of MailMergeModel objects containing the context for each email to be sent. + /// + /// + /// + /// + /// + /// + /// An array of tuples containing the context and any error that occurred while sending the email. + Task<(bool success, MailResponseModel[] responses)> TrySendAsync( + IEnumerable contexts, + string subject, + string body, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null, + CancellationToken cancellationToken = default); + + /// + /// Sends an email asynchronously using a pre-constructed MailMessage object. Uses the configured SmtpClient to send the email. If cancellation is requested, the operation will be cancelled. The MailMessage should be fully constructed with from, to, subject, body, and any other necessary properties before calling this method. + /// + /// + /// + /// + Task SendAsync(MailMessage message, CancellationToken cancellationToken = default); + + /// + /// Sends multiple emails asynchronously using a collection of pre-constructed MailMessage objects. Uses the configured SmtpClient to send each email. If cancellation is requested, the operation will be cancelled. Each MailMessage in the collection should be fully constructed with from, to, subject, body, and any other necessary properties before calling this method. If the messages collection is null, no action will be taken. If any individual message is null, it will be skipped. + /// + /// + /// + /// An array of tuples containing the message and any error that occurred while sending each email. + Task<(bool success, MailResponseModel[] responses)> SendAsync(IEnumerable messages, CancellationToken cancellationToken = default); + + /// + /// Sends multiple emails asynchronously using a collection of pre-constructed MailMessage objects. Uses the configured SmtpClient to send each email. If cancellation is requested, the operation will be cancelled. Each MailMessage in the collection should be fully constructed with from, to, subject, body, and any other necessary properties before calling this method. If the messages collection is null, no action will be taken. If any individual message is null, it will be skipped. + /// + /// + /// + /// An array of tuples containing the message and any error that occurred while sending each email. + Task<(bool success, MailResponseModel[] responses)> TrySendAsync(IEnumerable messages, CancellationToken cancellationToken = default); + + /// + /// Sends an email synchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// + /// + /// + /// + /// + /// + MailResponseModel Send(string subject, string body, IEnumerable to, string? from = null, bool isHtml = true, IEnumerable? attachments = null); + + /// + /// Sends an email synchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// + /// + /// + /// param name="cc"> + /// + /// + /// + /// + MailResponseModel Send( + string subject, + string body, + IEnumerable to, + IEnumerable? cc, + IEnumerable? bcc, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null); + + /// + /// Sends an email synchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// An array of MailMergeModel objects containing the context for each email to be sent. + /// + /// + /// + /// + /// + /// An array of tuples containing the context and any error that occurred while sending each email. + (bool success, MailResponseModel[] responses) Send( + IEnumerable contexts, + string subject, + string body, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null); + + /// + /// Sends an email synchronously with the specified subject, body, recipients, and optional from address and HTML flag. Uses the configured SmtpClient to send the email. If from address is not specified, it will use the default from address from options. If no from address is available, an exception will be thrown. + /// + /// An array of MailMergeModel objects containing the context for each email to be sent. + /// + /// + /// + /// + /// + /// An array of tuples containing the context and any error that occurred while sending each email. + (bool success, MailResponseModel[] responses) TrySend( + IEnumerable contexts, + string subject, + string body, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null); + + /// + /// Creates a MailMessage object based on the provided parameters and the default from address in options. If from address is not specified and no default from address is available, an exception will be thrown. + /// + /// + /// + /// + /// + /// + /// + /// + /// + MailMessage CreateMailMessage( + string subject, + string body, + IEnumerable to, + IEnumerable? cc = null, + IEnumerable? bcc = null, + string? from = null, + bool isHtml = true, + IEnumerable? attachments = null); +} diff --git a/libs/net/smtp/MMI.SmtpEmail.csproj b/libs/net/smtp/MMI.SmtpEmail.csproj new file mode 100644 index 0000000000..ef403b7a96 --- /dev/null +++ b/libs/net/smtp/MMI.SmtpEmail.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + win-x64;linux-x64;linux-arm;linux-arm64 + enable + Library + MMI.SmtpEmail + 2.0.1 + 2.0.1 + TNO.Ches + Jeremy Foster + Fosol Solutions Inc. + ../packages + + + + + + + + + + + + diff --git a/libs/net/smtp/Models/AttachmentModel.cs b/libs/net/smtp/Models/AttachmentModel.cs new file mode 100644 index 0000000000..85c5ba6344 --- /dev/null +++ b/libs/net/smtp/Models/AttachmentModel.cs @@ -0,0 +1,30 @@ +namespace MMI.SmtpEmail.Models +{ + /// + /// AttachmentModel class, provides a model that represents an attachment to an email. + /// + public class AttachmentModel + { + #region Properties + /// + /// get/set - The content of the attachment. + /// + public string Content { get; set; } = ""; + + /// + /// get/set - The content type. + /// + public string ContentType { get; set; } = ""; + + /// + /// get/set - The encoding of the attachment. + /// + public string Encoding { get; set; } = ""; + + /// + /// get/set - The file name of the attachment. + /// + public string Filename { get; set; } = ""; + #endregion + } +} diff --git a/libs/net/smtp/Models/MailMergeModel.cs b/libs/net/smtp/Models/MailMergeModel.cs new file mode 100644 index 0000000000..85eee710cc --- /dev/null +++ b/libs/net/smtp/Models/MailMergeModel.cs @@ -0,0 +1,118 @@ +using Fluid; + +namespace MMI.SmtpEmail.Models; + +public class MailMergeModel +{ + #region Properties + /// + /// get/set - Who the email will be sent to. + /// + public string[] To { get; set; } = []; + + /// + /// get/set - Who the email will be sent to as a carbon copy (CC). This is optional and can be used to send a copy of the email to additional recipients while revealing their email addresses to the primary recipients. If specified, the email addresses in this field will receive a copy of the email, and their addresses will be visible to the primary recipients and other CC recipients. + /// + public string[]? CC { get; set; } + + /// + /// get/set - Who the email will be sent to as a blind carbon copy (BCC). This is optional and can be used to send a copy of the email to additional recipients without revealing their email addresses to the primary recipients. If specified, the email addresses in this field will receive a copy of the email, but their addresses will not be visible to the primary recipients or other BCC recipients. + /// + public string[]? Bcc { get; set; } + + /// + /// get/set - A JSON object containing the template variables and their values to be used for generating the email subject and body. The keys in this dictionary should correspond to the variable names used in the email templates, and the values will be substituted into the templates when generating the final email content. This allows for dynamic generation of email content based on a single template and a set of variable values, enabling mail merge functionality where multiple emails can be generated with different content from the same template. + /// + public Dictionary Context { get; set; } = []; + #endregion + + #region Constructors + /// + /// Creates a new instance of a MailMergeModel object, initializes with specified parameters. + /// + public MailMergeModel() { } + + /// + /// Creates a new instance of a MailMergeModel object, initializes with specified parameters. + /// + /// + public MailMergeModel(IEnumerable to) + { + this.To = [.. to]; + } + + /// + /// Creates a new instance of a MailMergeModel object, initializes with specified parameters. + /// + /// + /// + public MailMergeModel(IEnumerable to, Dictionary context) + { + this.To = [.. to]; + this.Context = context; + } + #endregion + + #region Methods + /// + /// Populates the Context dictionary with values from the specified UserModel. This is used to provide user-specific context for generating email content from templates. The UserModel properties are added to the Context dictionary with keys corresponding to the property names, allowing for easy substitution of user-specific values in email templates using Liquid syntax. + /// + /// + public MailMergeModel PopulateContextFromUser(TNO.API.Areas.Services.Models.User.UserModel? user) + { + var context = new Dictionary() { + { "id", user?.Id ?? 0 }, + { "firstName", user?.FirstName ?? "" }, + { "lastName", user?.LastName ?? "" }, + }; + this.Context = context; + return this; + } + + /// + /// Populates the Context dictionary with values from the specified UserModel. This is used to provide user-specific context for generating email content from templates. The UserModel properties are added to the Context dictionary with keys corresponding to the property names, allowing for easy substitution of user-specific values in email templates using Liquid syntax. + /// + /// + public MailMergeModel PopulateContextFromUser(TNO.API.Areas.Services.Models.Notification.UserModel? user) + { + var context = new Dictionary() { + { "id", user?.Id ?? 0 }, + { "firstName", user?.FirstName ?? "" }, + { "lastName", user?.LastName ?? "" }, + }; + this.Context = context; + return this; + } + + /// + /// Merges placeholders in the template string with values from the Context dictionary using Fluid Liquid template syntax. Supports placeholders in the format {{key}} where key is a key in the Context dictionary. Complex Liquid syntax including filters, loops, and conditionals are also supported. If the template cannot be parsed, the original template string is returned. + /// + /// The template string containing Liquid placeholders in the format {{key}}. + /// The template string with placeholders replaced by corresponding context values. + public string Merge(string template) + { + if (string.IsNullOrEmpty(template) || Context == null || Context.Count == 0) + return template ?? string.Empty; + + try + { + var fluidParser = new FluidParser(); + if (!fluidParser.TryParse(template, out var fluidTemplate, out var errors)) + { + // If parsing fails, return the original template + return template; + } + + var templateContext = new TemplateContext(Context); + var result = fluidTemplate.Render(templateContext); + + return result ?? string.Empty; + } + catch + { + // If rendering fails, return the original template + return template ?? string.Empty; + } + } + #endregion +} diff --git a/libs/net/smtp/Models/MailResponseModel.cs b/libs/net/smtp/Models/MailResponseModel.cs new file mode 100644 index 0000000000..5e0ce896ae --- /dev/null +++ b/libs/net/smtp/Models/MailResponseModel.cs @@ -0,0 +1,159 @@ +using System.Net.Mail; +using System.Text.Json; +using TNO.Core.Extensions; + +namespace MMI.SmtpEmail.Models; + +/// +/// MailResponseModel class, provides a model to pass SMTP response details back to the caller. This includes the email addresses the message was sent to, the status code of the SMTP response, any error message from the SMTP response, and any additional details about the error that may be helpful for troubleshooting. This model is used to determine if a message should be retried or not. If the status code indicates a transient error or if the error message indicates a transient error then it should be retried, but if the status code indicates a permanent error or if the error message indicates a permanent error then it should not be retried. Additionally, if the email was sent more than a certain amount of time ago (e.g. 24 hours) then it should not be retried as it may have been updated since the email was sent. +/// +public class MailResponseModel +{ + #region Properties + /// + /// get/set - Who the email will be sent to. + /// + public string[] To { get; set; } = []; + + /// + /// get/set - Who the email will be sent to as a carbon copy (CC). This is optional and can be used to send a copy of the email to additional recipients while revealing their email addresses to the primary recipients. If specified, the email addresses in this field will receive a copy of the email, and their addresses will be visible to the primary recipients and other CC recipients. + /// + public string[]? CC { get; set; } + + /// + /// get/set - Who the email will be sent to as a blind carbon copy (BCC). This is optional and can be used to send a copy of the email to additional recipients without revealing their email addresses to the primary recipients. If specified, the email addresses in this field will receive a copy of the email, but their addresses will not be visible to the primary recipients or other BCC recipients. + /// + public string[]? Bcc { get; set; } + + /// + /// get/set - The status code of the SMTP response. + /// + public SmtpStatusCode StatusCode { get; set; } + + /// + /// get/set - Data that may be helpful for troubleshooting. This could include information such as whether the error was a transient error or a permanent error, or any other relevant details that may help determine whether the message should be retried or not. + /// + public string? Data { get; set; } + + /// + /// get/set - The error message from the SMTP response. This is used to determine if the message should be retried or not. If the error message indicates a transient error then it should be retried, but if it indicates a permanent error then it should not be retried. + /// + public string? ErrorMessage { get; set; } + + /// + /// get/set - Any additional details about the error that may be helpful for troubleshooting. This could include information such as whether the error was a transient error or a permanent error, or any other relevant details that may help determine whether the message should be retried or not. + /// + public string? ErrorDetails { get; set; } + + /// + /// get/set - When the email was sent. This is used to determine if the message should be retried or not. If the email was sent more than a certain amount of time ago (e.g. 24 hours) then it should not be retried as it may have been updated since the email was sent. + /// + public DateTimeOffset SentOn { get; set; } = DateTimeOffset.UtcNow; + #endregion + + #region Constructors + /// + /// Creates a new instance of a MailResponseModel object, initializes with specified parameters. + /// + public MailResponseModel() { } + + /// + /// Creates a new instance of a MailResponseModel object, initializes with specified parameters. + /// + /// + /// + public MailResponseModel(IEnumerable to, SmtpException? exception = null) + { + this.To = [.. to]; + this.StatusCode = exception?.StatusCode ?? SmtpStatusCode.Ok; + this.ErrorMessage = exception?.Message; + this.ErrorDetails = exception?.GetAllMessages(); + } + + /// + /// Creates a new instance of a MailResponseModel object, initializes with specified parameters. + /// + /// + /// + /// + /// + public MailResponseModel(IEnumerable to, IEnumerable? cc, IEnumerable? bcc, SmtpException? exception = null) + { + this.To = [.. to]; + this.CC = cc?.ToArray(); + this.Bcc = bcc?.ToArray(); + this.StatusCode = exception?.StatusCode ?? SmtpStatusCode.Ok; + this.ErrorMessage = exception?.Message; + this.ErrorDetails = exception?.GetAllMessages(); + } + + /// + /// Creates a new instance of a MailResponseModel object, initializes with specified parameters. + /// + /// + public MailResponseModel(MailMergeModel context) + { + this.To = context.To; + this.CC = context.CC?.ToArray(); + this.Bcc = context.Bcc?.ToArray(); + this.StatusCode = SmtpStatusCode.Ok; + } + + /// + /// Creates a new instance of a MailResponseModel object, initializes with specified parameters. + /// + /// + /// + public MailResponseModel(MailMergeModel context, SmtpException exception) + { + this.To = context.To; + this.CC = context.CC?.ToArray(); + this.Bcc = context.Bcc?.ToArray(); + this.StatusCode = exception.StatusCode; + this.ErrorMessage = exception.Message; + this.ErrorDetails = exception.GetAllMessages(); + } + + /// + /// Creates a new instance of a MailResponseModel object, initializes with specified parameters. + /// + /// + /// + public MailResponseModel(MailMergeModel context, Exception exception) + { + this.To = context.To; + this.CC = context.CC?.ToArray(); + this.Bcc = context.Bcc?.ToArray(); + this.StatusCode = SmtpStatusCode.GeneralFailure; + this.ErrorMessage = exception.Message; + this.ErrorDetails = exception.GetAllMessages(); + } + + /// + /// Creates a new instance of a MailResponseModel object, initializes with specified parameters. + /// + /// + /// + public MailResponseModel(MailMessage message, SmtpException? exception = null) + { + this.To = [.. message.To.Select(t => t.Address)]; + this.CC = message.CC?.Select(t => t.Address).ToArray(); + this.Bcc = message.Bcc?.Select(t => t.Address).ToArray(); + this.StatusCode = exception?.StatusCode ?? SmtpStatusCode.Ok; + this.ErrorMessage = exception?.Message; + this.ErrorDetails = exception?.GetAllMessages(); + } + #endregion + + #region Methods + /// + /// Converts this MailResponseModel object to a JsonDocument. This is used to pass the response details back to the caller in a structured format that can be easily parsed and used to determine if a message should be retried or not. + /// + /// + /// + public JsonDocument ToJsonDocument(JsonSerializerOptions? options = null) + { + return JsonDocument.Parse(JsonSerializer.Serialize(this, options)); + } + #endregion +} diff --git a/libs/net/smtp/ServicesExtensions.cs b/libs/net/smtp/ServicesExtensions.cs new file mode 100644 index 0000000000..b330d1134d --- /dev/null +++ b/libs/net/smtp/ServicesExtensions.cs @@ -0,0 +1,89 @@ +using System.Net; +using System.Net.Mail; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace MMI.SmtpEmail; + +/// +/// Extension methods for registering the EmailService and related services in the dependency injection container. Provides methods to register using IConfiguration or an options configuration callback, and to register an injectable SmtpClient configured from SmtpOptions. The AddSmtpEmail methods register the IEmailService and configure SmtpOptions, while AddSmtpClient registers an injectable SmtpClient that can be used by the EmailService when it creates its own instance. If you are injecting your own SmtpClient, you do not need to call AddSmtpClient. +/// +public static class ServicesExtensions +{ + /// + /// Register SmtpOptions, an injectable SmtpClient configured from options, and the EmailService. + /// + /// + /// + /// + public static IServiceCollection AddSmtpEmail(this IServiceCollection services, IConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + services.Configure(configuration.GetSection("Smtp")); + return services.AddTransient(); + } + + /// + /// Register SmtpOptions, an injectable SmtpClient configured from options, and the EmailService. + /// + /// + /// + /// + public static IServiceCollection AddSmtpEmail(this IServiceCollection services, IConfigurationSection configuration) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + services.Configure(configuration); + return services.AddTransient(); + } + + /// + /// Register using an options configuration callback instead of IConfiguration. + /// + /// + /// + /// + public static IServiceCollection AddSmtpEmail(this IServiceCollection services, Action configure) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); + + services.Configure(configure); + return services.AddTransient(); + } + + /// + /// Register an injectable SmtpClient configured from SmtpOptions. This is used by the EmailService when it creates its own SmtpClient instance. If you are injecting your own SmtpClient, you do not need to call this method. + /// + /// + /// + public static IServiceCollection AddSmtpClient(this IServiceCollection services) + { + services.AddTransient(sp => + { + var opts = sp.GetRequiredService>().Value; + var client = new SmtpClient(opts.Host, opts.Port) + { + EnableSsl = opts.EnableSsl, + DeliveryMethod = SmtpDeliveryMethod.Network, + UseDefaultCredentials = opts.UseDefaultCredentials + }; + + if (!string.IsNullOrWhiteSpace(opts.Username)) + { + client.Credentials = new NetworkCredential(opts.Username, opts.Password); + } + + if (opts.Timeout.HasValue) + client.Timeout = (int)opts.Timeout.Value.TotalMilliseconds; + + return client; + }); + + return services; + } +} diff --git a/libs/net/template/Models/Reports/AVOverviewInstanceModel.cs b/libs/net/template/Models/Reports/AVOverviewInstanceModel.cs index c31b715b8c..f479e348b2 100644 --- a/libs/net/template/Models/Reports/AVOverviewInstanceModel.cs +++ b/libs/net/template/Models/Reports/AVOverviewInstanceModel.cs @@ -26,6 +26,23 @@ public class AVOverviewInstanceModel /// public AVOverviewSettingsModel Settings { get; set; } = new(); + /// + /// get/set - The report status. + /// + public Entities.ReportStatus Status { get; set; } + + /// + /// get/set - The compiled subject of the report. + /// Used to recreate the report. + /// + public string Subject { get; set; } = ""; + + /// + /// get/set - The compiled body of the report. + /// Used to recreate the report. + /// + public string Body { get; set; } = ""; + // // get/set - The response. // @@ -47,6 +64,9 @@ public AVOverviewInstanceModel(Entities.AVOverviewInstance entity) this.TemplateType = entity.TemplateType; this.PublishedOn = entity.PublishedOn; this.Sections = entity.Sections.Select(s => new AVOverviewSectionModel(s)).OrderBy(s => s.SortOrder).ToArray(); + this.Status = entity.Status; + this.Subject = entity.Subject; + this.Body = entity.Body; this.Response = entity.Response; } @@ -59,6 +79,9 @@ public AVOverviewInstanceModel(TNO.API.Areas.Editor.Models.AVOverview.AVOverview this.TemplateType = model.TemplateType; this.PublishedOn = model.PublishedOn; this.Sections = model.Sections.Select(s => new AVOverviewSectionModel(s)).OrderBy(s => s.SortOrder).ToArray(); + this.Status = model.Status; + this.Subject = model.Subject; + this.Body = model.Body; this.Response = model.Response; } @@ -71,6 +94,9 @@ public AVOverviewInstanceModel(TNO.API.Areas.Services.Models.AVOverview.AVOvervi this.TemplateType = model.TemplateType; this.PublishedOn = model.PublishedOn; this.Sections = model.Sections.Select(s => new AVOverviewSectionModel(s)).OrderBy(s => s.SortOrder).ToArray(); + this.Status = model.Status; + this.Subject = model.Subject; + this.Body = model.Body; this.Response = model.Response; } #endregion diff --git a/libs/npm/core/src/hooks/api/interfaces/IReportInstanceModel.ts b/libs/npm/core/src/hooks/api/interfaces/IReportInstanceModel.ts index 9e1110ddbd..64a12d0a59 100644 --- a/libs/npm/core/src/hooks/api/interfaces/IReportInstanceModel.ts +++ b/libs/npm/core/src/hooks/api/interfaces/IReportInstanceModel.ts @@ -11,6 +11,7 @@ export interface IReportInstanceModel extends IAuditColumnsModel { status: ReportStatusName; subject: string; body: string; + linkOnlyBody: string; response: any; content: IReportInstanceContentModel[]; } diff --git a/openshift/kustomize/api-services/base/deploy.yaml b/openshift/kustomize/api-services/base/deploy.yaml index 5f4be3d0e0..116c0f35cb 100644 --- a/openshift/kustomize/api-services/base/deploy.yaml +++ b/openshift/kustomize/api-services/base/deploy.yaml @@ -222,43 +222,23 @@ spec: secretKeyRef: name: s3-backup-credentials key: S3_SERVICE_URL - # CHES Configuration - - name: CHES__From + + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: api - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: api - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED livenessProbe: httpGet: diff --git a/openshift/kustomize/api-services/base/deployConfig.yaml b/openshift/kustomize/api-services/base/deployConfig.yaml deleted file mode 100644 index c8759b1158..0000000000 --- a/openshift/kustomize/api-services/base/deployConfig.yaml +++ /dev/null @@ -1,258 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: api-services - namespace: default - annotations: - description: Defines how to deploy api - labels: - name: api-services - part-of: tno - version: 1.0.0 - component: api-services - managed-by: kustomize - created-by: jeremy.foster -spec: - replicas: 1 - test: false - selector: - part-of: tno - component: api-services - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - api-services - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: api:dev - template: - metadata: - name: api-services - labels: - part-of: tno - component: api-services - spec: - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - volumes: - - name: api-storage - persistentVolumeClaim: - claimName: api-storage - - name: ingest-storage - persistentVolumeClaim: - claimName: ingest-storage - containers: - - name: api-services - image: '' - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: api-storage - mountPath: /mnt/data - - name: ingest-storage - mountPath: /mnt/ingest - resources: - requests: - cpu: 100m - memory: 350Mi - env: - - name: ASPNETCORE_URLS - value: http://+:8080 - - name: HubPath - value: /hub - - name: Charts__Url - value: 'http://charts-api:8080' - - - name: Logging__LogLevel__TNO - value: Information - - - name: Storage__UploadPath - value: /mnt/data - - name: Storage__CapturePath - value: /mnt/ingest - - - name: SignalR__EnableKafkaBackPlane - value: 'false' - - - name: Keycloak__Authority - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_AUTHORITY - - name: Keycloak__Audience - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_AUDIENCE - - name: Keycloak__Issuer - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_ISSUER - - name: Keycloak__ClientId - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_ID - - name: Keycloak__ServiceAccount__Authority - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_SERVICEACCOUNT_AUTHORITY - - name: Keycloak__ServiceAccount__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: KAFKA_BOOTSTRAP_SERVERS - valueFrom: - configMapKeyRef: - name: api - key: KAFKA_BOOTSTRAP_SERVERS - - - name: ConnectionStrings__TNO - valueFrom: - configMapKeyRef: - name: api - key: CONNECTION_STRING - - name: DB_POSTGRES_USERNAME - valueFrom: - secretKeyRef: - name: montford - key: USERNAME - - name: DB_POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: montford - key: PASSWORD - - - name: Elastic__Url - valueFrom: - configMapKeyRef: - name: api - key: ELASTIC_URIS - - name: Elastic__Username - valueFrom: - secretKeyRef: - name: elastic - key: USERNAME - - name: Elastic__Password - valueFrom: - secretKeyRef: - name: elastic - key: PASSWORD - - - name: Reporting__SubscriberAppUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_SUBSCRIBER_URL - - name: Reporting__ViewContentUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_VIEW_CONTENT_URL - - name: Reporting__RequestTranscriptUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_REQUEST_TRANSCRIPT_URL - - # S3 Configuration - - name: S3_ACCESS_KEY - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_ACCESS_KEY - - name: S3_SECRET_KEY - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_SECRET_KEY - - name: S3_BUCKET_NAME - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_BUCKET_NAME - - name: S3_SERVICE_URL - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_SERVICE_URL - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: api - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: api - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - - livenessProbe: - httpGet: - path: '/health' - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 30 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: '/health' - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 60 - periodSeconds: 30 - successThreshold: 1 - failureThreshold: 3 diff --git a/openshift/kustomize/api/base/config-map.yaml b/openshift/kustomize/api/base/config-map.yaml index 794437ebf3..0621041462 100644 --- a/openshift/kustomize/api/base/config-map.yaml +++ b/openshift/kustomize/api/base/config-map.yaml @@ -19,5 +19,5 @@ data: KAFKA_BOOTSTRAP_SERVERS: kafka-headless:29092 - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" diff --git a/openshift/kustomize/api/base/deploy.yaml b/openshift/kustomize/api/base/deploy.yaml index d984fe4cee..96c951b540 100644 --- a/openshift/kustomize/api/base/deploy.yaml +++ b/openshift/kustomize/api/base/deploy.yaml @@ -228,43 +228,22 @@ spec: name: s3-backup-credentials key: S3_SERVICE_URL - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: api - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: api - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED livenessProbe: httpGet: diff --git a/openshift/kustomize/api/base/deployConfig.yaml b/openshift/kustomize/api/base/deployConfig.yaml deleted file mode 100644 index 06518c24d7..0000000000 --- a/openshift/kustomize/api/base/deployConfig.yaml +++ /dev/null @@ -1,271 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: api - namespace: default - annotations: - description: Defines how to deploy api - labels: - name: api - part-of: tno - version: 1.0.0 - component: api - managed-by: kustomize - created-by: jeremy.foster -spec: - replicas: 1 - test: false - selector: - part-of: tno - component: api - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - api - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: api:dev - template: - metadata: - name: api - labels: - part-of: tno - component: api - spec: - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - volumes: - - name: api-storage - persistentVolumeClaim: - claimName: api-storage - - name: ingest-storage - persistentVolumeClaim: - claimName: ingest-storage - containers: - - name: api - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: api-storage - mountPath: /mnt/data - - name: ingest-storage - mountPath: /mnt/ingest - resources: - requests: - cpu: 100m - memory: 350Mi - env: - - name: ASPNETCORE_URLS - value: http://+:8080 - - name: HubPath - value: /hub - - name: Charts__Url - value: "http://charts-api:8080" - - - name: Logging__LogLevel__TNO - value: Information - - - name: Storage__UploadPath - value: /mnt/data - - name: Storage__CapturePath - value: /mnt/ingest - - - name: Keycloak__Authority - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_AUTHORITY - - name: Keycloak__Audience - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_AUDIENCE - - name: Keycloak__Issuer - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_ISSUER - - name: Keycloak__ClientId - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_ID - - name: Keycloak__ServiceAccount__Authority - valueFrom: - configMapKeyRef: - name: keycloak - key: KEYCLOAK_SERVICEACCOUNT_AUTHORITY - - name: Keycloak__ServiceAccount__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: KAFKA_BOOTSTRAP_SERVERS - valueFrom: - configMapKeyRef: - name: api - key: KAFKA_BOOTSTRAP_SERVERS - - - name: ConnectionStrings__TNO - valueFrom: - configMapKeyRef: - name: api - key: CONNECTION_STRING - - name: DB_POSTGRES_USERNAME - valueFrom: - secretKeyRef: - name: database - key: USERNAME - - name: DB_POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: database - key: PASSWORD - - - name: Elastic__Url - valueFrom: - configMapKeyRef: - name: api - key: ELASTIC_URIS - - name: Elastic__Username - valueFrom: - secretKeyRef: - name: elastic - key: USERNAME - - name: Elastic__Password - valueFrom: - secretKeyRef: - name: elastic - key: PASSWORD - - name: Elastic__ApiKey - valueFrom: - secretKeyRef: - name: elastic - key: ApiKey - - name: Elastic__ContentIndex - valueFrom: - configMapKeyRef: - name: indexing-service - key: CONTENT_INDEX - - name: Elastic__PublishedIndex - valueFrom: - configMapKeyRef: - name: indexing-service - key: PUBLISHED_INDEX - - - name: Reporting__SubscriberAppUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_SUBSCRIBER_URL - - name: Reporting__ViewContentUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_VIEW_CONTENT_URL - - name: Reporting__RequestTranscriptUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_REQUEST_TRANSCRIPT_URL - - # S3 Configuration - - name: S3_ACCESS_KEY - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_ACCESS_KEY - - name: S3_SECRET_KEY - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_SECRET_KEY - - name: S3_BUCKET_NAME - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_BUCKET_NAME - - name: S3_SERVICE_URL - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_SERVICE_URL - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: api - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: api - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 30 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 60 - periodSeconds: 30 - successThreshold: 1 - failureThreshold: 3 diff --git a/openshift/kustomize/api/base/statefulset.yaml b/openshift/kustomize/api/base/statefulset.yaml index 09840574ba..ecef79189c 100644 --- a/openshift/kustomize/api/base/statefulset.yaml +++ b/openshift/kustomize/api/base/statefulset.yaml @@ -254,43 +254,22 @@ spec: name: s3-backup-credentials key: S3_SERVICE_URL - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: api - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: api - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED livenessProbe: httpGet: diff --git a/openshift/kustomize/app/editor/base/deployConfig.yaml b/openshift/kustomize/app/editor/base/deployConfig.yaml deleted file mode 100644 index 93db36a4c4..0000000000 --- a/openshift/kustomize/app/editor/base/deployConfig.yaml +++ /dev/null @@ -1,99 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: editor - namespace: default - annotations: - description: Defines how to deploy editor - labels: - name: editor-app - part-of: tno - version: 1.0.0 - component: editor - managed-by: kustomize - created-by: jeremy.foster -spec: - replicas: 1 - selector: - name: editor - part-of: tno - component: editor - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: editor - labels: - name: editor - part-of: tno - component: editor - spec: - volumes: - - name: editor-keycloak - configMap: - name: editor-sso - items: - - key: keycloak.json - path: keycloak.json - containers: - - name: editor - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 25m - memory: 30Mi - limits: - cpu: 50m - memory: 100Mi - volumeMounts: - - name: editor-keycloak - mountPath: /usr/share/nginx/html/keycloak.json - subPath: keycloak.json - livenessProbe: - httpGet: - path: "/nginx-status" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 60 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/nginx-status" - port: 8080 - scheme: HTTP - initialDelaySeconds: 10 - timeoutSeconds: 60 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - editor - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: editor:dev diff --git a/openshift/kustomize/app/subscriber/base/deployConfig.yaml b/openshift/kustomize/app/subscriber/base/deployConfig.yaml deleted file mode 100644 index 7a006bb9be..0000000000 --- a/openshift/kustomize/app/subscriber/base/deployConfig.yaml +++ /dev/null @@ -1,104 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: subscriber - namespace: default - annotations: - description: Defines how to deploy subscriber - labels: - name: subscriber-app - part-of: tno - version: 1.0.0 - component: subscriber - managed-by: kustomize - created-by: jeremy.foster -spec: - replicas: 1 - selector: - name: subscriber - part-of: tno - component: subscriber - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: subscriber - labels: - name: subscriber - part-of: tno - component: subscriber - spec: - volumes: - - name: subscriber-keycloak - configMap: - name: subscriber-sso - items: - - key: keycloak.json - path: keycloak.json - - name: subscriber-public-storage - persistentVolumeClaim: - claimName: subscriber-public-storage - containers: - - name: subscriber - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 25m - memory: 30Mi - limits: - cpu: 50m - memory: 100Mi - volumeMounts: - - name: subscriber-keycloak - mountPath: /usr/share/nginx/html/keycloak.json - subPath: keycloak.json - - name: subscriber-public-storage - mountPath: /usr/share/nginx/html/public/videos - livenessProbe: - httpGet: - path: "/nginx-status" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 60 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/nginx-status" - port: 8080 - scheme: HTTP - initialDelaySeconds: 10 - timeoutSeconds: 60 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - subscriber - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: subscriber:dev diff --git a/openshift/kustomize/charts/base/deployConfig.yaml b/openshift/kustomize/charts/base/deployConfig.yaml deleted file mode 100644 index c062eb142c..0000000000 --- a/openshift/kustomize/charts/base/deployConfig.yaml +++ /dev/null @@ -1,91 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: charts-api - namespace: default - annotations: - description: Defines how to deploy charts-api - labels: - name: charts-api - part-of: tno - version: 1.0.0 - component: charts-api - managed-by: kustomize - created-by: jeremy.foster -spec: - replicas: 1 - selector: - name: charts-api - part-of: tno - component: charts-api - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: charts-api - labels: - name: charts-api - part-of: tno - component: charts-api - spec: - containers: - - name: charts-api - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - limits: - cpu: 35m - memory: 100Mi - requests: - cpu: 20m - memory: 75Mi - env: - - name: PORT - value: "8080" - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 60 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 10 - timeoutSeconds: 60 - periodSeconds: 10 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - charts-api - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: charts-api:dev diff --git a/openshift/kustomize/nginx-reverse-proxy/base/deployConfig.yaml b/openshift/kustomize/nginx-reverse-proxy/base/deployConfig.yaml deleted file mode 100644 index e5f5ba2f5b..0000000000 --- a/openshift/kustomize/nginx-reverse-proxy/base/deployConfig.yaml +++ /dev/null @@ -1,175 +0,0 @@ -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: nginx-editor - namespace: default - annotations: - description: Defines how to deploy nginx - labels: - name: nginx-editor - part-of: tno - version: 1.0.0 - component: nginx-editor - managed-by: kustomize - created-by: jeremy.foster -spec: - replicas: 1 - selector: - name: nginx-editor - part-of: tno - component: nginx-editor - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: nginx-editor - labels: - name: nginx-editor - part-of: tno - component: nginx-editor - spec: - containers: - - name: nginx-editor - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 20m - memory: 50Mi - limits: - cpu: 50m - memory: 100Mi - # livenessProbe: - # httpGet: - # path: '/nginx-status' - # port: 8080 - # scheme: HTTP - # initialDelaySeconds: 120 - # timeoutSeconds: 60 - # periodSeconds: 30 - # successThreshold: 1 - # failureThreshold: 3 - # readinessProbe: - # httpGet: - # path: '/nginx-status' - # port: 8080 - # scheme: HTTP - # initialDelaySeconds: 120 - # timeoutSeconds: 60 - # periodSeconds: 30 - # successThreshold: 1 - # failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - nginx-editor - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: nginx-editor:dev ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: nginx-subscriber - namespace: default - annotations: - description: Defines how to deploy nginx - labels: - name: nginx-subscriber - part-of: tno - version: 1.0.0 - component: nginx-subscriber - managed-by: kustomize - created-by: jeremy.foster -spec: - replicas: 1 - selector: - name: nginx-subscriber - part-of: tno - component: nginx-subscriber - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: nginx-subscriber - labels: - name: nginx-subscriber - part-of: tno - component: nginx-subscriber - spec: - containers: - - name: nginx-subscriber - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 20m - memory: 50Mi - limits: - cpu: 50m - memory: 100Mi - # livenessProbe: - # httpGet: - # path: '/nginx-status' - # port: 8080 - # scheme: HTTP - # initialDelaySeconds: 120 - # timeoutSeconds: 60 - # periodSeconds: 30 - # successThreshold: 1 - # failureThreshold: 3 - # readinessProbe: - # httpGet: - # path: '/nginx-status' - # port: 8080 - # scheme: HTTP - # initialDelaySeconds: 120 - # timeoutSeconds: 60 - # periodSeconds: 30 - # successThreshold: 1 - # failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - nginx-subscriber - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: nginx-subscriber:dev diff --git a/openshift/kustomize/nginx/base/deployConfig.yaml b/openshift/kustomize/nginx/base/deployConfig.yaml deleted file mode 100644 index 98efa8fa9f..0000000000 --- a/openshift/kustomize/nginx/base/deployConfig.yaml +++ /dev/null @@ -1,87 +0,0 @@ -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: nginx - namespace: default - annotations: - description: Defines how to deploy nginx - labels: - name: nginx - part-of: tno - version: 1.0.0 - component: nginx - managed-by: kustomize - created-by: jeremy.foster -spec: - replicas: 1 - selector: - name: nginx - part-of: tno - component: nginx - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: nginx - labels: - name: nginx - part-of: tno - component: nginx - spec: - containers: - - name: nginx - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 20m - memory: 50Mi - limits: - cpu: 50m - memory: 100Mi - # livenessProbe: - # httpGet: - # path: '/nginx-status' - # port: 8080 - # scheme: HTTP - # initialDelaySeconds: 120 - # timeoutSeconds: 60 - # periodSeconds: 30 - # successThreshold: 1 - # failureThreshold: 3 - # readinessProbe: - # httpGet: - # path: '/nginx-status' - # port: 8080 - # scheme: HTTP - # initialDelaySeconds: 120 - # timeoutSeconds: 60 - # periodSeconds: 30 - # successThreshold: 1 - # failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - nginx - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: nginx:dev diff --git a/openshift/kustomize/services/auto-clipper/base/config-map.yaml b/openshift/kustomize/services/auto-clipper/base/config-map.yaml index 23cf59e924..91f4ef855a 100644 --- a/openshift/kustomize/services/auto-clipper/base/config-map.yaml +++ b/openshift/kustomize/services/auto-clipper/base/config-map.yaml @@ -19,7 +19,7 @@ data: MAX_FAIL_LIMIT: "5" TOPICS: request-clips VOLUME_PATH: /data - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" LLM_API_URL: "https://mmi-ai-foundry-east-us-2.openai.azure.com/openai/v1/chat/completions" LLM_MODEL_NAME: "gpt-5.1-chat" \ No newline at end of file diff --git a/openshift/kustomize/services/auto-clipper/base/deploy.yaml b/openshift/kustomize/services/auto-clipper/base/deploy.yaml index bee63c144c..2433a6359b 100644 --- a/openshift/kustomize/services/auto-clipper/base/deploy.yaml +++ b/openshift/kustomize/services/auto-clipper/base/deploy.yaml @@ -265,43 +265,22 @@ spec: name: s3-backup-credentials key: S3_SERVICE_URL - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: auto-clipper-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: auto-clipper-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED livenessProbe: httpGet: path: "/health" diff --git a/openshift/kustomize/services/build/base/smtp-retry.yaml b/openshift/kustomize/services/build/base/smtp-retry.yaml new file mode 100644 index 0000000000..52e3bac214 --- /dev/null +++ b/openshift/kustomize/services/build/base/smtp-retry.yaml @@ -0,0 +1,59 @@ +--- +# The final build image. +kind: ImageStream +apiVersion: image.openshift.io/v1 +metadata: + name: smtp-retry-service + annotations: + description: Destination for built images. + created-by: jeremy.foster + labels: + name: smtp-retry-service + part-of: tno + version: 1.0.0 + component: smtp-retry + managed-by: kustomize + +--- +# The build config that will be created will be named for the branch you created it for. +kind: BuildConfig +apiVersion: build.openshift.io/v1 +metadata: + name: smtp-retry-service.dev + annotations: + description: Build image from Dockerfile in git repo. + created-by: jeremy.foster + labels: + name: smtp-retry-service + part-of: tno + version: 1.0.0 + component: smtp-retry + managed-by: kustomize + branch: dev +spec: + completionDeadlineSeconds: 1800 + triggers: + - type: ImageChange + - type: ConfigChange + runPolicy: Serial + source: + git: + uri: https://github.com/bcgov/tno.git + ref: dev + contextDir: ./ + strategy: + type: Docker + dockerStrategy: + imageOptimizationPolicy: SkipLayers + dockerfilePath: services/net/smtp-retry/Dockerfile + output: + to: + kind: ImageStreamTag + name: smtp-retry-service:latest + resources: + requests: + cpu: 20m + memory: 250Mi + limits: + cpu: 500m + memory: 2Gi diff --git a/openshift/kustomize/services/content-historic/base/config-map.yaml b/openshift/kustomize/services/content-historic/base/config-map.yaml index 777cc9dfd7..f749b16763 100644 --- a/openshift/kustomize/services/content-historic/base/config-map.yaml +++ b/openshift/kustomize/services/content-historic/base/config-map.yaml @@ -18,6 +18,6 @@ data: KAFKA_CLIENT_ID: ContentHistoric-TNO MAX_FAIL_LIMIT: "5" CONTENT_TOPICS_OVERRIDE: "TNO-HISTORIC" - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" ALLOW_UPDATE: "true" diff --git a/openshift/kustomize/services/content-historic/base/deploy.yaml b/openshift/kustomize/services/content-historic/base/deploy.yaml index c4e9231f04..544d2fbf16 100644 --- a/openshift/kustomize/services/content-historic/base/deploy.yaml +++ b/openshift/kustomize/services/content-historic/base/deploy.yaml @@ -131,46 +131,25 @@ spec: name: content-historic-service key: ALLOW_UPDATE - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: content-historic-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: content-historic-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -180,7 +159,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/content-historic/base/deployConfig.yaml b/openshift/kustomize/services/content-historic/base/deployConfig.yaml deleted file mode 100644 index 39ed0fd418..0000000000 --- a/openshift/kustomize/services/content-historic/base/deployConfig.yaml +++ /dev/null @@ -1,207 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: content-historic-service - namespace: default - annotations: - description: Defines how to deploy content-historic-service - created-by: jeremy.foster - labels: - name: content-historic-service - part-of: tno - version: 1.0.0 - component: content-historic-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: content-historic-service - part-of: tno - component: content-historic-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: content-historic-service - labels: - name: content-historic-service - part-of: tno - component: content-historic-service - spec: - volumes: - - name: ingest-storage - persistentVolumeClaim: - claimName: ingest-storage - containers: - - name: content-historic-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: ingest-storage - mountPath: /data - resources: - requests: - cpu: 25m - memory: 100Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Staging - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: content-historic-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Producer__GroupId - valueFrom: - configMapKeyRef: - name: content-historic-service - key: KAFKA_CLIENT_ID - - name: Kafka__Producer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Content Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: content-historic-service - key: MAX_FAIL_LIMIT - - name: Service__ContentTopics - valueFrom: - configMapKeyRef: - name: content-historic-service - key: CONTENT_TOPICS_OVERRIDE - - name: Service__AllowUpdate - valueFrom: - configMapKeyRef: - name: content-historic-service - key: ALLOW_UPDATE - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: content-historic-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: content-historic-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - content-historic-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: content-service:dev diff --git a/openshift/kustomize/services/content/base/config-map.yaml b/openshift/kustomize/services/content/base/config-map.yaml index 69fab205af..353d40e49d 100644 --- a/openshift/kustomize/services/content/base/config-map.yaml +++ b/openshift/kustomize/services/content/base/config-map.yaml @@ -18,6 +18,6 @@ data: KAFKA_CLIENT_ID: Content MAX_FAIL_LIMIT: "5" CONTENT_TOPICS_EXCLUSIONS: "TNO-HISTORIC,TNO" - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" ALLOW_UPDATE: "true" diff --git a/openshift/kustomize/services/content/base/deploy.yaml b/openshift/kustomize/services/content/base/deploy.yaml index f43e53a48d..82567f533c 100644 --- a/openshift/kustomize/services/content/base/deploy.yaml +++ b/openshift/kustomize/services/content/base/deploy.yaml @@ -134,49 +134,28 @@ spec: name: content-service key: ALLOW_UPDATE - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: content-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: content-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED - name: Kafka__Consumer__AutoOffsetReset value: Latest livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -186,7 +165,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/content/base/deployConfig.yaml b/openshift/kustomize/services/content/base/deployConfig.yaml deleted file mode 100644 index e254eb73e3..0000000000 --- a/openshift/kustomize/services/content/base/deployConfig.yaml +++ /dev/null @@ -1,210 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: content-service - namespace: default - annotations: - description: Defines how to deploy content-service - created-by: jeremy.foster - labels: - name: content-service - part-of: tno - version: 1.0.0 - component: content-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: content-service - part-of: tno - component: content-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: content-service - labels: - name: content-service - part-of: tno - component: content-service - spec: - volumes: - - name: ingest-storage - persistentVolumeClaim: - claimName: ingest-storage - containers: - - name: content-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: ingest-storage - mountPath: /data - resources: - requests: - cpu: 25m - memory: 150Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Staging - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: content-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Producer__GroupId - valueFrom: - configMapKeyRef: - name: content-service - key: KAFKA_CLIENT_ID - - name: Kafka__Producer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Content Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: content-service - key: MAX_FAIL_LIMIT - - name: Service__ContentTopicsToExclude - valueFrom: - configMapKeyRef: - name: content-service - key: CONTENT_TOPICS_EXCLUSIONS - - name: Service__AllowUpdate - valueFrom: - configMapKeyRef: - name: content-service - key: ALLOW_UPDATE - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: content-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: content-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - - - name: Kafka__Consumer__AutoOffsetReset - value: Latest - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - content-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: content-service:dev diff --git a/openshift/kustomize/services/contentmigration-current/base/config-map.yaml b/openshift/kustomize/services/contentmigration-current/base/config-map.yaml index 355d8e7fb7..04c9343bc3 100644 --- a/openshift/kustomize/services/contentmigration-current/base/config-map.yaml +++ b/openshift/kustomize/services/contentmigration-current/base/config-map.yaml @@ -23,5 +23,5 @@ data: INGEST_TYPES: "TNO-AudioVideo,TNO-Image,TNO-PrintContent,TNO-Story" SUPPORTED_IMPORT_MIGRATION_TYPES: "Current" DEFAULT_USERNAME_FOR_AUDIT: "contentmigrator" - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" diff --git a/openshift/kustomize/services/contentmigration-current/base/deploy.yaml b/openshift/kustomize/services/contentmigration-current/base/deploy.yaml index 0ea1c15667..ed5ce1c130 100644 --- a/openshift/kustomize/services/contentmigration-current/base/deploy.yaml +++ b/openshift/kustomize/services/contentmigration-current/base/deploy.yaml @@ -132,43 +132,22 @@ spec: name: contentmigration-service key: DEFAULT_USERNAME_FOR_AUDIT - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: contentmigration-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: contentmigration-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED # Container TimeZone Configuration - name: TZ @@ -197,4 +176,4 @@ spec: dnsPolicy: ClusterFirst restartPolicy: Always securityContext: {} - terminationGracePeriodSeconds: 30 \ No newline at end of file + terminationGracePeriodSeconds: 30 diff --git a/openshift/kustomize/services/contentmigration-current/base/deployConfig.yaml b/openshift/kustomize/services/contentmigration-current/base/deployConfig.yaml deleted file mode 100644 index 3fedcb4bc7..0000000000 --- a/openshift/kustomize/services/contentmigration-current/base/deployConfig.yaml +++ /dev/null @@ -1,213 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: contentmigration-service - namespace: default - annotations: - description: Defines how to deploy contentmigration-service - created-by: kyle.morris - labels: - name: contentmigration-service - part-of: tno - version: 1.0.0 - component: contentmigration-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: contentmigration-service - part-of: tno - component: contentmigration-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: contentmigration-service - labels: - name: contentmigration-service - part-of: tno - component: contentmigration-service - spec: - volumes: - - name: ingest-storage - persistentVolumeClaim: - claimName: ingest-storage - containers: - - name: contentmigration-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: ingest-storage - mountPath: /data - resources: - limits: - cpu: 50m - memory: 200Mi - requests: - cpu: 20m - memory: 128Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__DataLocation - valueFrom: - configMapKeyRef: - name: services - key: DATA_LOCATION - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - # Content Migration Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: contentmigration-service - key: MAX_FAIL_LIMIT - - name: Service__VolumePath - valueFrom: - configMapKeyRef: - name: contentmigration-service - key: VOLUME_PATH - - name: Service__MediaHostRootUri - valueFrom: - configMapKeyRef: - name: contentmigration-service - key: CONTENT_MIGRATION_MEDIA_HOST_ROOT_URI - - name: Service__IngestTypes - valueFrom: - configMapKeyRef: - name: contentmigration-service - key: INGEST_TYPES - - name: Service__SupportedImportMigrationTypes - valueFrom: - configMapKeyRef: - name: contentmigration-service - key: SUPPORTED_IMPORT_MIGRATION_TYPES - - name: Service__DefaultUserNameForAudit - valueFrom: - configMapKeyRef: - name: contentmigration-service - key: DEFAULT_USERNAME_FOR_AUDIT - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: contentmigration-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: contentmigration-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - - # Container TimeZone Configuration - - name: TZ - value: America/Vancouver - - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - contentmigration-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: contentmigration-service:dev diff --git a/openshift/kustomize/services/contentmigration-historic/base/config-map.yaml b/openshift/kustomize/services/contentmigration-historic/base/config-map.yaml index 35f7c2701f..7d9e0811a7 100644 --- a/openshift/kustomize/services/contentmigration-historic/base/config-map.yaml +++ b/openshift/kustomize/services/contentmigration-historic/base/config-map.yaml @@ -23,7 +23,7 @@ data: SUPPORTED_IMPORT_MIGRATION_TYPES: "Historic,All" DEFAULT_USERNAME_FOR_AUDIT: "contentmigrator" GENERATE_ALERTS_ON_CONTENT_MIGRATION: "true" - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" DATA_LOCATION: Openshift MAX_RECORDS_PER_RETRIEVAL: "250" diff --git a/openshift/kustomize/services/contentmigration-historic/base/deploy.yaml b/openshift/kustomize/services/contentmigration-historic/base/deploy.yaml index 594cc3be60..06b8554920 100644 --- a/openshift/kustomize/services/contentmigration-historic/base/deploy.yaml +++ b/openshift/kustomize/services/contentmigration-historic/base/deploy.yaml @@ -142,43 +142,22 @@ spec: name: contentmigration-historic-service key: MAX_RECORDS_PER_RETRIEVAL - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: contentmigration-historic-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: contentmigration-historic-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED # Container TimeZone Configuration - name: TZ @@ -207,4 +186,4 @@ spec: dnsPolicy: ClusterFirst restartPolicy: Always securityContext: {} - terminationGracePeriodSeconds: 30 \ No newline at end of file + terminationGracePeriodSeconds: 30 diff --git a/openshift/kustomize/services/contentmigration-historic/base/deployConfig.yaml b/openshift/kustomize/services/contentmigration-historic/base/deployConfig.yaml deleted file mode 100644 index fa5af1ace6..0000000000 --- a/openshift/kustomize/services/contentmigration-historic/base/deployConfig.yaml +++ /dev/null @@ -1,223 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: contentmigration-historic-service - namespace: default - annotations: - description: Defines how to deploy contentmigration-historic-service - created-by: jeremy.foster - labels: - name: contentmigration-historic-service - part-of: tno - version: 1.0.0 - component: contentmigration-historic-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: contentmigration-historic-service - part-of: tno - component: contentmigration-historic-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: contentmigration-historic-service - labels: - name: contentmigration-historic-service - part-of: tno - component: contentmigration-historic-service - spec: - volumes: - - name: ingest-storage - persistentVolumeClaim: - claimName: ingest-storage - containers: - - name: contentmigration-historic-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: ingest-storage - mountPath: /data - resources: - limits: - cpu: 25m - memory: 200Mi - requests: - cpu: 10m - memory: 128Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - # Content Migration Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: MAX_FAIL_LIMIT - - name: Service__VolumePath - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: VOLUME_PATH - - name: Service__MediaHostRootUri - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: CONTENT_MIGRATION_MEDIA_HOST_ROOT_URI - - name: Service__IngestTypes - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: INGEST_TYPES - - name: Service__SupportedImportMigrationTypes - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: SUPPORTED_IMPORT_MIGRATION_TYPES - - name: Service__DefaultUserNameForAudit - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: DEFAULT_USERNAME_FOR_AUDIT - - name: Service__GenerateAlertsOnContentMigration - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: GENERATE_ALERTS_ON_CONTENT_MIGRATION - - name: Service__DataLocation - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: DATA_LOCATION - - name: Service__MaxRecordsPerRetrieval - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: MAX_RECORDS_PER_RETRIEVAL - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: contentmigration-historic-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - - # Container TimeZone Configuration - - name: TZ - value: America/Vancouver - - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - contentmigration-historic-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: contentmigration-service:dev diff --git a/openshift/kustomize/services/event-handler/base/config-map.yaml b/openshift/kustomize/services/event-handler/base/config-map.yaml index b77fecaa3d..0c64472e63 100644 --- a/openshift/kustomize/services/event-handler/base/config-map.yaml +++ b/openshift/kustomize/services/event-handler/base/config-map.yaml @@ -17,5 +17,5 @@ data: KAFKA_CLIENT_ID: Event-Scheduler MAX_FAIL_LIMIT: "5" TOPICS: event-schedule - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" diff --git a/openshift/kustomize/services/event-handler/base/deploy.yaml b/openshift/kustomize/services/event-handler/base/deploy.yaml index b4421c434d..eddb1ccbd9 100644 --- a/openshift/kustomize/services/event-handler/base/deploy.yaml +++ b/openshift/kustomize/services/event-handler/base/deploy.yaml @@ -122,47 +122,26 @@ spec: name: event-handler-service key: TOPICS - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: event-handler-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: event-handler-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -172,7 +151,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/event-handler/base/deployConfig.yaml b/openshift/kustomize/services/event-handler/base/deployConfig.yaml deleted file mode 100644 index 8100c18cea..0000000000 --- a/openshift/kustomize/services/event-handler/base/deployConfig.yaml +++ /dev/null @@ -1,199 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: event-handler-service - namespace: default - annotations: - description: Defines how to deploy event-handler-service - created-by: jeremy.foster - labels: - name: event-handler-service - part-of: tno - version: 1.0.0 - component: event-handler-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: event-handler-service - part-of: tno - component: event-handler-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: event-handler-service - labels: - name: event-handler-service - part-of: tno - component: event-handler-service - spec: - containers: - - name: event-handler-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 20m - memory: 50Mi - limits: - cpu: 100m - memory: 100Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: event-handler-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: event-handler-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: event-handler-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: event-handler-service - key: TOPICS - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: event-handler-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: event-handler-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - event-handler-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: event-handler-service:dev diff --git a/openshift/kustomize/services/extract-quotes/base/config-map.yaml b/openshift/kustomize/services/extract-quotes/base/config-map.yaml index 2965a40519..7d4de2ddd8 100644 --- a/openshift/kustomize/services/extract-quotes/base/config-map.yaml +++ b/openshift/kustomize/services/extract-quotes/base/config-map.yaml @@ -17,8 +17,8 @@ data: KAFKA_CLIENT_ID: ExtractQuotes MAX_FAIL_LIMIT: "5" TOPICS: index - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" CORENLP_URL: http://corenlp:9000 EXTRACT_QUOTES_ON_INDEX: "false" EXTRACT_QUOTES_ON_PUBLISH: "true" diff --git a/openshift/kustomize/services/extract-quotes/base/deploy.yaml b/openshift/kustomize/services/extract-quotes/base/deploy.yaml index 4b1b74ace1..5d7bd84bad 100644 --- a/openshift/kustomize/services/extract-quotes/base/deploy.yaml +++ b/openshift/kustomize/services/extract-quotes/base/deploy.yaml @@ -194,47 +194,26 @@ spec: name: llm-api-keys key: FALLBACK_API_KEYS - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: extract-quotes-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: extract-quotes-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + key: SMTP_EMAIL_AUTHORIZED livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -244,7 +223,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/extract-quotes/base/deployConfig.yaml b/openshift/kustomize/services/extract-quotes/base/deployConfig.yaml deleted file mode 100644 index 2be722dd0f..0000000000 --- a/openshift/kustomize/services/extract-quotes/base/deployConfig.yaml +++ /dev/null @@ -1,271 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: extract-quotes-service - namespace: default - annotations: - description: Defines how to deploy extract-quotes-service - created-by: jeremy.foster - labels: - name: extract-quotes-service - part-of: tno - version: 1.0.0 - component: extract-quotes-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: extract-quotes-service - part-of: tno - component: extract-quotes-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: extract-quotes-service - labels: - name: extract-quotes-service - part-of: tno - component: extract-quotes-service - spec: - containers: - - name: extract-quotes-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 20m - memory: 80Mi - limits: - cpu: 100m - memory: 150Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: TOPICS - - name: Service__CoreNLPApiUrl - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: CORENLP_URL - - name: Service__ExtractQuotesOnIndex - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: EXTRACT_QUOTES_ON_INDEX - - name: Service__ExtractQuotesOnPublish - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: EXTRACT_QUOTES_ON_PUBLISH - - name: Service__IgnoreContentPublishedBeforeOffset - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET - - # LLM Configuration - - name: Service__UseLLM - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: USE_LLM - - name: Service__PrimaryModelName - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: PRIMARY_MODEL_NAME - - name: Service__PrimaryApiUrl - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: PRIMARY_API_URL - - name: Service__FallbackModelName - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: FALLBACK_MODEL_NAME - - name: Service__FallbackApiUrl - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: FALLBACK_API_URL - - name: Service__MaxRequestsPerMinute - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: MAX_REQUESTS_PER_MINUTE - - name: Service__RetryLimit - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: RETRY_LIMIT - - name: Service__RetryDelayMS - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: RETRY_DELAY_MS - - name: Service__PrimaryApiKeys - valueFrom: - secretKeyRef: - name: llm-api-keys - key: PRIMARY_API_KEYS - - name: Service__FallbackApiKeys - valueFrom: - secretKeyRef: - name: llm-api-keys - key: FALLBACK_API_KEYS - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: extract-quotes-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - extract-quotes-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: extract-quotes-service:dev diff --git a/openshift/kustomize/services/ffmpeg/base/config-map.yaml b/openshift/kustomize/services/ffmpeg/base/config-map.yaml index 4c643a02ec..ddb66b4807 100644 --- a/openshift/kustomize/services/ffmpeg/base/config-map.yaml +++ b/openshift/kustomize/services/ffmpeg/base/config-map.yaml @@ -19,5 +19,5 @@ data: MAX_FAIL_LIMIT: "5" TOPICS: index VOLUME_PATH: /data - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" diff --git a/openshift/kustomize/services/ffmpeg/base/deploy.yaml b/openshift/kustomize/services/ffmpeg/base/deploy.yaml index 4c82982e98..90d9088ebf 100644 --- a/openshift/kustomize/services/ffmpeg/base/deploy.yaml +++ b/openshift/kustomize/services/ffmpeg/base/deploy.yaml @@ -134,46 +134,26 @@ spec: name: ffmpeg-service key: VOLUME_PATH - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: ffmpeg-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: ffmpeg-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -183,7 +163,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/ffmpeg/base/deployConfig.yaml b/openshift/kustomize/services/ffmpeg/base/deployConfig.yaml deleted file mode 100644 index 860daeaca9..0000000000 --- a/openshift/kustomize/services/ffmpeg/base/deployConfig.yaml +++ /dev/null @@ -1,210 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: ffmpeg-service - namespace: default - annotations: - description: Defines how to deploy ffmpeg-service - created-by: jeremy.foster - labels: - name: ffmpeg-service - part-of: tno - version: 1.0.0 - component: ffmpeg-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: ffmpeg-service - part-of: tno - component: ffmpeg-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: ffmpeg-service - labels: - name: ffmpeg-service - part-of: tno - component: ffmpeg-service - spec: - volumes: - - name: api-storage - persistentVolumeClaim: - claimName: api-storage - containers: - - name: ffmpeg-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: api-storage - mountPath: /data - resources: - requests: - cpu: 20m - memory: 100Mi - limits: - cpu: 75m - memory: 300Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: ffmpeg-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: ffmpeg-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: ffmpeg-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: ffmpeg-service - key: TOPICS - - name: Service__VolumePath - valueFrom: - configMapKeyRef: - name: ffmpeg-service - key: VOLUME_PATH - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: ffmpeg-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: ffmpeg-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - ffmpeg-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: ffmpeg-service:dev diff --git a/openshift/kustomize/services/filemonitor/base/config-map.yaml b/openshift/kustomize/services/filemonitor/base/config-map.yaml index a1231e1abf..0e3dc83389 100644 --- a/openshift/kustomize/services/filemonitor/base/config-map.yaml +++ b/openshift/kustomize/services/filemonitor/base/config-map.yaml @@ -19,5 +19,5 @@ data: VOLUME_PATH: /data KEY_PATH: ../keys TOPICS: "" - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" diff --git a/openshift/kustomize/services/filemonitor/base/deploy.yaml b/openshift/kustomize/services/filemonitor/base/deploy.yaml index 194ac93ce7..d64f1b9b64 100644 --- a/openshift/kustomize/services/filemonitor/base/deploy.yaml +++ b/openshift/kustomize/services/filemonitor/base/deploy.yaml @@ -134,46 +134,26 @@ spec: name: ssh-key key: id_rsa - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: filemonitor-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: filemonitor-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -183,7 +163,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/folder-collection/base/config-map.yaml b/openshift/kustomize/services/folder-collection/base/config-map.yaml index 69acaeb895..6ac371faba 100644 --- a/openshift/kustomize/services/folder-collection/base/config-map.yaml +++ b/openshift/kustomize/services/folder-collection/base/config-map.yaml @@ -18,6 +18,6 @@ data: KAFKA_CLIENT_ID: FolderCollection MAX_FAIL_LIMIT: "5" TOPICS: folder - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET: "7" diff --git a/openshift/kustomize/services/folder-collection/base/deploy.yaml b/openshift/kustomize/services/folder-collection/base/deploy.yaml index 8cf9e47f5b..edbdfd80b5 100644 --- a/openshift/kustomize/services/folder-collection/base/deploy.yaml +++ b/openshift/kustomize/services/folder-collection/base/deploy.yaml @@ -37,7 +37,7 @@ spec: spec: containers: - name: folder-collection-service - image: '' + image: "" imagePullPolicy: Always ports: - containerPort: 8080 @@ -159,46 +159,26 @@ spec: name: folder-collection-service key: IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: folder-collection-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: folder-collection-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -208,7 +188,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/folder-collection/base/deployConfig.yaml b/openshift/kustomize/services/folder-collection/base/deployConfig.yaml deleted file mode 100644 index 20c7b48221..0000000000 --- a/openshift/kustomize/services/folder-collection/base/deployConfig.yaml +++ /dev/null @@ -1,235 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: folder-collection-service - namespace: default - annotations: - description: Defines how to deploy folder-collection-service - created-by: jeremy.foster - labels: - name: folder-collection-service - part-of: tno - version: 1.0.0 - component: folder-collection-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: folder-collection-service - part-of: tno - component: folder-collection-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: folder-collection-service - labels: - name: folder-collection-service - part-of: tno - component: folder-collection-service - spec: - containers: - - name: folder-collection-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - limits: - cpu: 50m - memory: 150Mi - requests: - cpu: 25m - memory: 80Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: folder-collection-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: folder-collection-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Elasticsearch Configuration - - name: Elastic__Url - valueFrom: - configMapKeyRef: - name: api - key: ELASTIC_URIS - - name: Elastic__ContentIndex - valueFrom: - configMapKeyRef: - name: indexing-service - key: CONTENT_INDEX - - name: Elastic__PublishedIndex - valueFrom: - configMapKeyRef: - name: indexing-service - key: PUBLISHED_INDEX - - name: Elastic__Username - valueFrom: - secretKeyRef: - name: elastic - key: USERNAME - - name: Elastic__Password - valueFrom: - secretKeyRef: - name: elastic - key: PASSWORD - - name: Elastic__ApiKey - valueFrom: - secretKeyRef: - name: elastic - key: ApiKey - - # Indexing Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: folder-collection-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: folder-collection-service - key: TOPICS - - name: Service__IgnoreContentPublishedBeforeOffset - valueFrom: - configMapKeyRef: - name: folder-collection-service - key: IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: folder-collection-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: folder-collection-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - folder-collection-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: folder-collection-service:dev diff --git a/openshift/kustomize/services/image/base/config-map.yaml b/openshift/kustomize/services/image/base/config-map.yaml index 3b0dea8a3c..e1dd028a24 100644 --- a/openshift/kustomize/services/image/base/config-map.yaml +++ b/openshift/kustomize/services/image/base/config-map.yaml @@ -19,5 +19,5 @@ data: VOLUME_PATH: /data KEY_PATH: ../keys TOPICS: "" - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" diff --git a/openshift/kustomize/services/image/base/deploy.yaml b/openshift/kustomize/services/image/base/deploy.yaml index 1fbca44fac..bba6f9d761 100644 --- a/openshift/kustomize/services/image/base/deploy.yaml +++ b/openshift/kustomize/services/image/base/deploy.yaml @@ -134,46 +134,26 @@ spec: name: ssh-key key: id_rsa - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: image-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: image-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -183,7 +163,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/image/base/deployConfig.yaml b/openshift/kustomize/services/image/base/deployConfig.yaml deleted file mode 100644 index 5530cce931..0000000000 --- a/openshift/kustomize/services/image/base/deployConfig.yaml +++ /dev/null @@ -1,210 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: image-service - namespace: default - annotations: - description: Defines how to deploy image-service - created-by: jeremy.foster, alessia.yanchen - labels: - name: image-service - part-of: tno - version: 1.0.0 - component: image-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: image-service - part-of: tno - component: image-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: image-service - labels: - name: image-service - part-of: tno - component: image-service - spec: - volumes: - - name: ingest-storage - persistentVolumeClaim: - claimName: ingest-storage - - name: ssh-key - secret: - secretName: ssh-key # name of the Secret - optional: false - defaultMode: 420 - containers: - - name: image-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: ingest-storage - mountPath: /data - - name: ssh-key - mountPath: /keys - readOnly: true - resources: - limits: - cpu: 35m - memory: 120Mi - requests: - cpu: 15m - memory: 75Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__DataLocation - valueFrom: - configMapKeyRef: - name: services - key: DATA_LOCATION - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Service__VolumePath - valueFrom: - configMapKeyRef: - name: image-service - key: VOLUME_PATH - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: image-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: image-service - key: TOPICS - - name: Service__PrivateKeysPath - valueFrom: - configMapKeyRef: - name: image-service - key: KEY_PATH - - name: Service__PrivateKeyFileName - valueFrom: - secretKeyRef: - name: ssh-key - key: id_rsa - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: image-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: image-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - image-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: image-service:dev diff --git a/openshift/kustomize/services/indexing/base/config-map.yaml b/openshift/kustomize/services/indexing/base/config-map.yaml index 6738bafd66..324aaa57fc 100644 --- a/openshift/kustomize/services/indexing/base/config-map.yaml +++ b/openshift/kustomize/services/indexing/base/config-map.yaml @@ -22,6 +22,6 @@ data: CONTENT_INDEX: unpublished_content PUBLISHED_INDEX: content NOTIFICATION_TOPIC: notify - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" INDEX_ONLY: "false" diff --git a/openshift/kustomize/services/indexing/base/deploy.yaml b/openshift/kustomize/services/indexing/base/deploy.yaml index 924298d95d..9c01278aab 100644 --- a/openshift/kustomize/services/indexing/base/deploy.yaml +++ b/openshift/kustomize/services/indexing/base/deploy.yaml @@ -37,7 +37,7 @@ spec: spec: containers: - name: indexing-service - image: '' + image: "" imagePullPolicy: Always ports: - containerPort: 8080 @@ -174,46 +174,26 @@ spec: name: indexing-service key: INDEX_ONLY - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: indexing-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: indexing-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -223,7 +203,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/indexing/base/deployConfig.yaml b/openshift/kustomize/services/indexing/base/deployConfig.yaml deleted file mode 100644 index 89d9508895..0000000000 --- a/openshift/kustomize/services/indexing/base/deployConfig.yaml +++ /dev/null @@ -1,237 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: indexing-service - namespace: default - annotations: - description: Defines how to deploy indexing-service - created-by: jeremy.foster - labels: - name: indexing-service - part-of: tno - version: 1.0.0 - component: indexing-service - managed-by: kustomize -spec: - replicas: 3 - selector: - name: indexing-service - part-of: tno - component: indexing-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: indexing-service - labels: - name: indexing-service - part-of: tno - component: indexing-service - spec: - containers: - - name: indexing-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 25m - memory: 80Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: indexing-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: indexing-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Elasticsearch Configuration - - name: Elastic__Url - valueFrom: - configMapKeyRef: - name: indexing-service - key: ELASTICSEARCH_URI - - name: Elastic__Username - valueFrom: - secretKeyRef: - name: elastic - key: USERNAME - - name: Elastic__Password - valueFrom: - secretKeyRef: - name: elastic - key: PASSWORD - - name: Elastic__ApiKey - valueFrom: - secretKeyRef: - name: elastic - key: ApiKey - - name: Elastic__ContentIndex - valueFrom: - configMapKeyRef: - name: indexing-service - key: CONTENT_INDEX - - name: Elastic__PublishedIndex - valueFrom: - configMapKeyRef: - name: indexing-service - key: PUBLISHED_INDEX - - # Indexing Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: indexing-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: indexing-service - key: TOPICS - - name: Service__NotificationTopic - valueFrom: - configMapKeyRef: - name: indexing-service - key: NOTIFICATION_TOPIC - - name: Service__IndexOnly - valueFrom: - configMapKeyRef: - name: indexing-service - key: INDEX_ONLY - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: indexing-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: indexing-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - indexing-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: indexing-service:dev diff --git a/openshift/kustomize/services/nlp/base/config-map.yaml b/openshift/kustomize/services/nlp/base/config-map.yaml index 1afbe638d2..81c16aab7f 100644 --- a/openshift/kustomize/services/nlp/base/config-map.yaml +++ b/openshift/kustomize/services/nlp/base/config-map.yaml @@ -19,6 +19,6 @@ data: MAX_FAIL_LIMIT: "5" TOPICS: nlp INDEXING_TOPIC: index - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET: "7" diff --git a/openshift/kustomize/services/nlp/base/deploy.yaml b/openshift/kustomize/services/nlp/base/deploy.yaml index 40afec2650..27eb668f6f 100644 --- a/openshift/kustomize/services/nlp/base/deploy.yaml +++ b/openshift/kustomize/services/nlp/base/deploy.yaml @@ -132,46 +132,26 @@ spec: name: nlp-service key: IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: nlp-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: nlp-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -181,7 +161,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/nlp/base/deployConfig.yaml b/openshift/kustomize/services/nlp/base/deployConfig.yaml deleted file mode 100644 index 452a86c67d..0000000000 --- a/openshift/kustomize/services/nlp/base/deployConfig.yaml +++ /dev/null @@ -1,208 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: nlp-service - namespace: default - annotations: - description: Defines how to deploy nlp-service - created-by: jeremy.foster - labels: - name: nlp-service - part-of: tno - version: 1.0.0 - component: nlp-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: nlp-service - part-of: tno - component: nlp-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: nlp-service - labels: - name: nlp-service - part-of: tno - component: nlp-service - spec: - containers: - - name: nlp-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - limits: - cpu: 75m - memory: 250Mi - requests: - cpu: 20m - memory: 80Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: nlp-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Producer__ClientId - valueFrom: - configMapKeyRef: - name: nlp-service - key: KAFKA_CLIENT_ID - - name: Kafka__Producer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Indexing Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: nlp-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: nlp-service - key: TOPICS - - name: Service__IndexingTopic - valueFrom: - configMapKeyRef: - name: nlp-service - key: INDEXING_TOPIC - - name: Service__IgnoreContentPublishedBeforeOffset - valueFrom: - configMapKeyRef: - name: nlp-service - key: IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: nlp-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: nlp-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - nlp-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: nlp-service:dev diff --git a/openshift/kustomize/services/notification/base/config-map.yaml b/openshift/kustomize/services/notification/base/config-map.yaml index 937f2f67cd..917a6d656c 100644 --- a/openshift/kustomize/services/notification/base/config-map.yaml +++ b/openshift/kustomize/services/notification/base/config-map.yaml @@ -17,7 +17,7 @@ data: KAFKA_CLIENT_ID: Notification MAX_FAIL_LIMIT: "5" TOPICS: notify - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET: "7" ALWAYS_BCC: "ITSupportMMI@gov.bc.ca" diff --git a/openshift/kustomize/services/notification/base/deploy.yaml b/openshift/kustomize/services/notification/base/deploy.yaml index 7adfb0c593..a018cae64f 100644 --- a/openshift/kustomize/services/notification/base/deploy.yaml +++ b/openshift/kustomize/services/notification/base/deploy.yaml @@ -140,7 +140,7 @@ spec: name: notification-service key: TOPICS - name: Service__UploadPath - value: '/mnt/data' + value: "/mnt/data" - name: Service__IgnoreContentPublishedBeforeOffset valueFrom: configMapKeyRef: @@ -201,51 +201,31 @@ spec: name: elastic key: ApiKey - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: notification-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: notification-service - key: CHES_EMAIL_AUTHORIZED - - name: CHES__AlwaysBcc + key: SMTP_EMAIL_AUTHORIZED + - name: Smtp__AlwaysBcc valueFrom: configMapKeyRef: name: notification-service key: ALWAYS_BCC - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -255,7 +235,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/notification/base/deployConfig.yaml b/openshift/kustomize/services/notification/base/deployConfig.yaml deleted file mode 100644 index c3f71e633f..0000000000 --- a/openshift/kustomize/services/notification/base/deployConfig.yaml +++ /dev/null @@ -1,279 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: notification-service - namespace: default - annotations: - description: Defines how to deploy notification-service - created-by: jeremy.foster - labels: - name: notification-service - part-of: tno - version: 1.0.0 - component: notification-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: notification-service - part-of: tno - component: notification-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: notification-service - labels: - name: notification-service - part-of: tno - component: notification-service - spec: - volumes: - - name: api-storage - persistentVolumeClaim: - claimName: api-storage - containers: - - name: notification-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: api-storage - mountPath: /mnt/data - resources: - requests: - cpu: 20m - memory: 80Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: notification-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: notification-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Producer__ClientId - valueFrom: - configMapKeyRef: - name: notification-service - key: KAFKA_CLIENT_ID - - name: Kafka__Producer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Notification Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: notification-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: notification-service - key: TOPICS - - name: Service__UploadPath - value: "/mnt/data" - - name: Service__IgnoreContentPublishedBeforeOffset - valueFrom: - configMapKeyRef: - name: notification-service - key: IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET - - # Shared Reporting Configuration - - name: Reporting__SubscriberAppUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_SUBSCRIBER_URL - - name: Reporting__ViewContentUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_VIEW_CONTENT_URL - - name: Reporting__RequestTranscriptUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_REQUEST_TRANSCRIPT_URL - - name: Reporting__AddToReportUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_ADD_TO_REPORT_URL - - # Elasticsearch Configuration - - name: Elastic__Url - valueFrom: - configMapKeyRef: - name: api - key: ELASTIC_URIS - - name: Elastic__ContentIndex - valueFrom: - configMapKeyRef: - name: indexing-service - key: CONTENT_INDEX - - name: Elastic__PublishedIndex - valueFrom: - configMapKeyRef: - name: indexing-service - key: PUBLISHED_INDEX - - name: Elastic__Username - valueFrom: - secretKeyRef: - name: elastic - key: USERNAME - - name: Elastic__Password - valueFrom: - secretKeyRef: - name: elastic - key: PASSWORD - - name: Elastic__ApiKey - valueFrom: - secretKeyRef: - name: elastic - key: ApiKey - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: notification-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: notification-service - key: CHES_EMAIL_AUTHORIZED - - name: CHES__AlwaysBcc - valueFrom: - configMapKeyRef: - name: notification-service - key: ALWAYS_BCC - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - notification-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: notification-service:dev diff --git a/openshift/kustomize/services/reporting/base/config-map.yaml b/openshift/kustomize/services/reporting/base/config-map.yaml index aedf5cdded..449689b12e 100644 --- a/openshift/kustomize/services/reporting/base/config-map.yaml +++ b/openshift/kustomize/services/reporting/base/config-map.yaml @@ -17,8 +17,8 @@ data: KAFKA_CLIENT_ID: Reporting MAX_FAIL_LIMIT: "5" TOPICS: reporting - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" CHARTS_URL: http://charts-api:8080 RESEND_ON_FAILURE: "true" SEND_TO_ALL_BEFORE_FAILING: "false" diff --git a/openshift/kustomize/services/reporting/base/deploy.yaml b/openshift/kustomize/services/reporting/base/deploy.yaml index 8466c0b8c8..40b8c47eed 100644 --- a/openshift/kustomize/services/reporting/base/deploy.yaml +++ b/openshift/kustomize/services/reporting/base/deploy.yaml @@ -200,51 +200,30 @@ spec: configMapKeyRef: name: reporting-service key: TOPICS - - name: Service__DefaultFrom - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - name: Service__UseMailMerge value: "false" - # CHES Configuration - - name: CHES__EmailEnabled + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: reporting-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: reporting-service - key: CHES_EMAIL_AUTHORIZED - - name: CHES__AlwaysBcc + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: reporting-service - key: ALWAYS_BCC - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri + key: SMTP_EMAIL_AUTHORIZED + - name: Smtp__AlwaysBcc valueFrom: configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + name: reporting-service + key: ALWAYS_BCC - name: Charts__Url valueFrom: diff --git a/openshift/kustomize/services/reporting/base/deployConfig.yaml b/openshift/kustomize/services/reporting/base/deployConfig.yaml deleted file mode 100644 index 41ffb8e238..0000000000 --- a/openshift/kustomize/services/reporting/base/deployConfig.yaml +++ /dev/null @@ -1,259 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: reporting-service - namespace: default - annotations: - description: Defines how to deploy reporting-service - created-by: jeremy.foster - labels: - name: reporting-service - part-of: tno - version: 1.0.0 - component: reporting-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: reporting-service - part-of: tno - component: reporting-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: reporting-service - labels: - name: reporting-service - part-of: tno - component: reporting-service - spec: - volumes: - - name: api-storage - persistentVolumeClaim: - claimName: api-storage - containers: - - name: reporting-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: api-storage - mountPath: /mnt/data - resources: - requests: - cpu: 20m - memory: 80Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - name: Service__ImageVolumePath - value: /mnt/data - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: reporting-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: reporting-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Producer__ClientId - valueFrom: - configMapKeyRef: - name: reporting-service - key: KAFKA_CLIENT_ID - - name: Kafka__Producer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Shared Reporting Configuration - - name: Reporting__SubscriberAppUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_SUBSCRIBER_URL - - name: Reporting__ViewContentUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_VIEW_CONTENT_URL - - name: Reporting__RequestTranscriptUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_REQUEST_TRANSCRIPT_URL - - name: Reporting__AddToReportUrl - valueFrom: - configMapKeyRef: - name: reporting-shared - key: REPORTING_ADD_TO_REPORT_URL - - # Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: reporting-service - key: MAX_FAIL_LIMIT - - name: Service__ResendOnFailure - valueFrom: - configMapKeyRef: - name: reporting-service - key: RESEND_ON_FAILURE - - name: Service__SendToAllSubscribersBeforeFailing - valueFrom: - configMapKeyRef: - name: reporting-service - key: SEND_TO_ALL_BEFORE_FAILING - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: reporting-service - key: TOPICS - - name: Service__DefaultFrom - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - # CHES Configuration - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: reporting-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: reporting-service - key: CHES_EMAIL_AUTHORIZED - - name: CHES__AlwaysBcc - valueFrom: - configMapKeyRef: - name: reporting-service - key: ALWAYS_BCC - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - - - name: Charts__Url - valueFrom: - configMapKeyRef: - name: reporting-service - key: CHARTS_URL - - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - reporting-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: reporting-service:dev diff --git a/openshift/kustomize/services/scheduler/base/config-map.yaml b/openshift/kustomize/services/scheduler/base/config-map.yaml index 3c9b4f4b86..9181d05017 100644 --- a/openshift/kustomize/services/scheduler/base/config-map.yaml +++ b/openshift/kustomize/services/scheduler/base/config-map.yaml @@ -19,5 +19,5 @@ data: EVENT_TYPES: | - Notification - Report - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" diff --git a/openshift/kustomize/services/scheduler/base/deploy.yaml b/openshift/kustomize/services/scheduler/base/deploy.yaml index 36f31636ec..b5515765f3 100644 --- a/openshift/kustomize/services/scheduler/base/deploy.yaml +++ b/openshift/kustomize/services/scheduler/base/deploy.yaml @@ -133,46 +133,26 @@ spec: name: scheduler-service key: EVENT_TYPES - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: scheduler-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: scheduler-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -182,7 +162,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/scheduler/base/deployConfig.yaml b/openshift/kustomize/services/scheduler/base/deployConfig.yaml deleted file mode 100644 index 97768e77b7..0000000000 --- a/openshift/kustomize/services/scheduler/base/deployConfig.yaml +++ /dev/null @@ -1,209 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: scheduler-service - namespace: default - annotations: - description: Defines how to deploy scheduler-service - created-by: jeremy.foster - labels: - name: scheduler-service - part-of: tno - version: 1.0.0 - component: scheduler-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: scheduler-service - part-of: tno - component: scheduler-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: scheduler-service - labels: - name: scheduler-service - part-of: tno - component: scheduler-service - spec: - containers: - - name: scheduler-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 20m - memory: 80Mi - limits: - cpu: 100m - memory: 150Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: scheduler-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: scheduler-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Producer__ClientId - valueFrom: - configMapKeyRef: - name: scheduler-service - key: KAFKA_CLIENT_ID - - name: Kafka__Producer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Notification Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: scheduler-service - key: MAX_FAIL_LIMIT - - name: Service__EventTypes - valueFrom: - configMapKeyRef: - name: scheduler-service - key: EVENT_TYPES - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: scheduler-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: scheduler-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - scheduler-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: scheduler-service:dev diff --git a/openshift/kustomize/services/smtp-retry/base/config-map.yaml b/openshift/kustomize/services/smtp-retry/base/config-map.yaml new file mode 100644 index 0000000000..e6893a5de2 --- /dev/null +++ b/openshift/kustomize/services/smtp-retry/base/config-map.yaml @@ -0,0 +1,21 @@ +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: smtp-retry-service + namespace: default + annotations: + description: SMTP retry service configuration settings + created-by: jeremy.foster + labels: + name: smtp-retry-service + part-of: tno + version: 1.0.0 + component: smtp-retry-service + managed-by: kustomize +data: + MAX_FAIL_LIMIT: "5" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" + RETRY_TIME_LIMIT: "5" + RETRY_TIME_SCOPE: "60" diff --git a/openshift/kustomize/services/ches-retry/base/deployConfig.yaml b/openshift/kustomize/services/smtp-retry/base/deploy.yaml similarity index 61% rename from openshift/kustomize/services/ches-retry/base/deployConfig.yaml rename to openshift/kustomize/services/smtp-retry/base/deploy.yaml index b1a8e7a017..20ec439e33 100644 --- a/openshift/kustomize/services/ches-retry/base/deployConfig.yaml +++ b/openshift/kustomize/services/smtp-retry/base/deploy.yaml @@ -1,44 +1,43 @@ --- # How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 +kind: Deployment +apiVersion: apps/v1 metadata: - name: ches-retry-service + name: smtp-retry-service namespace: default annotations: - description: Defines how to deploy ches-retry-service + description: Defines how to deploy smtp-retry-service created-by: jeremy.foster + image.openshift.io/triggers: '[{"from": {"kind": "ImageStreamTag", "name": "smtp-retry-service:dev", "namespace": "9b301c-tools"}, "fieldPath": "spec.template.spec.containers[?(@.name==\"smtp-retry-service\")].image"}]' labels: - name: ches-retry-service + name: smtp-retry-service part-of: tno version: 1.0.0 - component: ches-retry-service + component: smtp-retry-service managed-by: kustomize spec: replicas: 1 selector: - name: ches-retry-service - part-of: tno - component: ches-retry-service + matchLabels: + name: smtp-retry-service + part-of: tno + component: smtp-retry-service strategy: - rollingParams: - intervalSeconds: 1 + type: RollingUpdate + rollingUpdate: maxSurge: 25% maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling template: metadata: - name: ches-retry-service + name: smtp-retry-service labels: - name: ches-retry-service + name: smtp-retry-service part-of: tno - component: ches-retry-service + component: smtp-retry-service spec: containers: - - name: ches-retry-service - image: "" + - name: smtp-retry-service + image: image-registry.apps.silver.devops.gov.bc.ca/9b301c-tools/smtp-retry-service:latest imagePullPolicy: Always ports: - containerPort: 8080 @@ -74,12 +73,12 @@ spec: - name: Service__RetryTimeLimit valueFrom: configMapKeyRef: - name: ches-retry-service + name: smtp-retry-service key: RETRY_TIME_LIMIT - name: Service__RetryTimeScope valueFrom: configMapKeyRef: - name: ches-retry-service + name: smtp-retry-service key: RETRY_TIME_SCOPE # Authentication Configuration @@ -103,46 +102,25 @@ spec: - name: Service__MaxFailLimit valueFrom: configMapKeyRef: - name: ches-retry-service + name: smtp-retry-service key: MAX_FAIL_LIMIT - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: ches-retry-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches-retry-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri + name: smtp-retry-service + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD + name: smtp-retry-service + key: SMTP_EMAIL_AUTHORIZED livenessProbe: httpGet: @@ -168,15 +146,3 @@ spec: restartPolicy: Always securityContext: {} terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - ches-retry-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: ches-retry-service:dev diff --git a/openshift/kustomize/services/smtp-retry/base/kustomization.yaml b/openshift/kustomize/services/smtp-retry/base/kustomization.yaml new file mode 100644 index 0000000000..8075511b29 --- /dev/null +++ b/openshift/kustomize/services/smtp-retry/base/kustomization.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - config-map.yaml + - deploy.yaml + - service.yaml + +generatorOptions: + disableNameSuffixHash: true diff --git a/openshift/kustomize/services/smtp-retry/base/service.yaml b/openshift/kustomize/services/smtp-retry/base/service.yaml new file mode 100644 index 0000000000..1a2c7d2864 --- /dev/null +++ b/openshift/kustomize/services/smtp-retry/base/service.yaml @@ -0,0 +1,27 @@ +--- +# Open up ports to communicate with the app. +kind: Service +apiVersion: v1 +metadata: + name: ches-retry-service + namespace: default + annotations: + description: Exposes and load balances the application pods. + created-by: jeremy.foster + labels: + name: ches-retry-service + part-of: tno + version: 1.0.0 + component: ches-retry-service + managed-by: kustomize +spec: + ports: + - name: 8080-tcp + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + part-of: tno + component: ches-retry-service + sessionAffinity: None + type: ClusterIP diff --git a/openshift/kustomize/services/smtp-retry/overlays/dev/kustomization.yaml b/openshift/kustomize/services/smtp-retry/overlays/dev/kustomization.yaml new file mode 100644 index 0000000000..300d228d03 --- /dev/null +++ b/openshift/kustomize/services/smtp-retry/overlays/dev/kustomization.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: 9b301c-dev + +resources: + - ../../base + +generatorOptions: + disableNameSuffixHash: true + +patsmtp: + - target: + kind: Deployment + name: smtp-retry-service + patch: |- + - op: replace + path: /spec/replicas + value: 1 + - op: replace + path: /spec/template/spec/containers/0/resources/requests/cpu + value: 20m + - op: replace + path: /spec/template/spec/containers/0/resources/requests/memory + value: 50Mi + - op: replace + path: /spec/template/spec/containers/0/resources/limits/cpu + value: 100m + - op: replace + path: /spec/template/spec/containers/0/resources/limits/memory + value: 100Mi + - op: replace + path: /spec/template/spec/containers/0/image + value: image-registry.openshift-image-registry.svc:5000/9b301c-tools/smtp-retry-service:dev diff --git a/openshift/kustomize/services/smtp-retry/overlays/prod/kustomization.yaml b/openshift/kustomize/services/smtp-retry/overlays/prod/kustomization.yaml new file mode 100644 index 0000000000..8a24d9cdcf --- /dev/null +++ b/openshift/kustomize/services/smtp-retry/overlays/prod/kustomization.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: 9b301c-prod + +resources: + - ../../base + +patsmtp: + - target: + kind: Deployment + name: smtp-retry-service + patch: |- + - op: replace + path: /spec/replicas + value: 1 + - op: replace + path: /metadata/annotations/image.openshift.io~1triggers + value: '[{"from": {"kind": "ImageStreamTag", "name": "smtp-retry-service:prod", "namespace": "9b301c-tools"}, "fieldPath": "spec.template.spec.containers[?(@.name==\"smtp-retry-service\")].image"}]' + - op: replace + path: /spec/template/spec/containers/0/resources/requests/cpu + value: 20m + - op: replace + path: /spec/template/spec/containers/0/resources/requests/memory + value: 100Mi + - op: replace + path: /spec/template/spec/containers/0/resources/limits/cpu + value: 50m + - op: replace + path: /spec/template/spec/containers/0/resources/limits/memory + value: 150Mi diff --git a/openshift/kustomize/services/smtp-retry/overlays/test/kustomization.yaml b/openshift/kustomize/services/smtp-retry/overlays/test/kustomization.yaml new file mode 100644 index 0000000000..640ea5fc5b --- /dev/null +++ b/openshift/kustomize/services/smtp-retry/overlays/test/kustomization.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: 9b301c-test + +resources: + - ../../base + +patsmtp: + - target: + kind: Deployment + name: smtp-retry-service + patch: |- + - op: replace + path: /spec/replicas + value: 1 + - op: replace + path: /spec/template/spec/containers/0/resources/requests/cpu + value: 20m + - op: replace + path: /spec/template/spec/containers/0/resources/requests/memory + value: 50Mi + - op: replace + path: /spec/template/spec/containers/0/resources/limits/cpu + value: 100m + - op: replace + path: /spec/template/spec/containers/0/resources/limits/memory + value: 100Mi diff --git a/openshift/kustomize/services/syndication/base/config-map.yaml b/openshift/kustomize/services/syndication/base/config-map.yaml index b1841758e9..12833f1a5a 100644 --- a/openshift/kustomize/services/syndication/base/config-map.yaml +++ b/openshift/kustomize/services/syndication/base/config-map.yaml @@ -16,6 +16,6 @@ metadata: managed-by: kustomize data: MAX_FAIL_LIMIT: "5" - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" SEND_EMAIL_ON_FAILURE: "true" diff --git a/openshift/kustomize/services/syndication/base/deploy.yaml b/openshift/kustomize/services/syndication/base/deploy.yaml index d14b66a303..5378fb9415 100644 --- a/openshift/kustomize/services/syndication/base/deploy.yaml +++ b/openshift/kustomize/services/syndication/base/deploy.yaml @@ -109,46 +109,26 @@ spec: name: syndication-service key: SEND_EMAIL_ON_FAILURE - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: syndication-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: syndication-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 @@ -158,7 +138,7 @@ spec: failureThreshold: 3 readinessProbe: httpGet: - path: '/health' + path: "/health" port: 8080 scheme: HTTP initialDelaySeconds: 30 diff --git a/openshift/kustomize/services/syndication/base/deployConfig.yaml b/openshift/kustomize/services/syndication/base/deployConfig.yaml deleted file mode 100644 index 04292f047a..0000000000 --- a/openshift/kustomize/services/syndication/base/deployConfig.yaml +++ /dev/null @@ -1,181 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: syndication-service - namespace: default - annotations: - description: Defines how to deploy syndication-service - created-by: jeremy.foster - labels: - name: syndication-service - part-of: tno - version: 1.0.0 - component: syndication-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: syndication-service - part-of: tno - component: syndication-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - syndication-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: syndication-service:dev - template: - metadata: - name: syndication-service - labels: - name: syndication-service - part-of: tno - component: syndication-service - spec: - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - containers: - - name: syndication-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - resources: - requests: - cpu: 20m - memory: 80Mi - limits: - cpu: 40m - memory: 128Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__DataLocation - valueFrom: - configMapKeyRef: - name: services - key: DATA_LOCATION - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - # Syndication Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: syndication-service - key: MAX_FAIL_LIMIT - - name: Service__SendEmailOnFailure - valueFrom: - configMapKeyRef: - name: syndication-service - key: SEND_EMAIL_ON_FAILURE - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: syndication-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: syndication-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 diff --git a/openshift/kustomize/services/transcription/base/config-map.yaml b/openshift/kustomize/services/transcription/base/config-map.yaml index 187abda103..35189697f5 100644 --- a/openshift/kustomize/services/transcription/base/config-map.yaml +++ b/openshift/kustomize/services/transcription/base/config-map.yaml @@ -20,6 +20,6 @@ data: TOPICS: transcribe VOLUME_PATH: /data NOTIFICATION_TOPIC: "" - CHES_EMAIL_ENABLED: "true" - CHES_EMAIL_AUTHORIZED: "true" + SMTP_EMAIL_ENABLED: "true" + SMTP_EMAIL_AUTHORIZED: "true" IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET: "7" diff --git a/openshift/kustomize/services/transcription/base/deploy.yaml b/openshift/kustomize/services/transcription/base/deploy.yaml index f37206d6d8..8573c1c4ca 100644 --- a/openshift/kustomize/services/transcription/base/deploy.yaml +++ b/openshift/kustomize/services/transcription/base/deploy.yaml @@ -199,43 +199,23 @@ spec: name: s3-backup-credentials key: S3_SERVICE_URL - # CHES Configuration - - name: CHES__From + # SMTP Configuration + - name: Smtp__From valueFrom: configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled + name: smtp + key: SMTP_FROM + - name: Smtp__EmailEnabled valueFrom: configMapKeyRef: name: transcription-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized + key: SMTP_EMAIL_ENABLED + - name: Smtp__EmailAuthorized valueFrom: configMapKeyRef: name: transcription-service - key: CHES_EMAIL_AUTHORIZED + key: SMTP_EMAIL_AUTHORIZED - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD livenessProbe: httpGet: path: "/health" diff --git a/openshift/kustomize/services/transcription/base/deployConfig.yaml b/openshift/kustomize/services/transcription/base/deployConfig.yaml deleted file mode 100644 index c61b42bc2a..0000000000 --- a/openshift/kustomize/services/transcription/base/deployConfig.yaml +++ /dev/null @@ -1,272 +0,0 @@ ---- -# How the app will be deployed to the pod. -kind: DeploymentConfig -apiVersion: apps.openshift.io/v1 -metadata: - name: transcription-service - namespace: default - annotations: - description: Defines how to deploy transcription-service - created-by: jeremy.foster - labels: - name: transcription-service - part-of: tno - version: 1.0.0 - component: transcription-service - managed-by: kustomize -spec: - replicas: 1 - selector: - name: transcription-service - part-of: tno - component: transcription-service - strategy: - rollingParams: - intervalSeconds: 1 - maxSurge: 25% - maxUnavailable: 25% - timeoutSeconds: 600 - updatePeriodSeconds: 1 - type: Rolling - template: - metadata: - name: transcription-service - labels: - name: transcription-service - part-of: tno - component: transcription-service - spec: - volumes: - - name: api-storage - persistentVolumeClaim: - claimName: api-storage - containers: - - name: transcription-service - image: "" - imagePullPolicy: Always - ports: - - containerPort: 8080 - protocol: TCP - volumeMounts: - - name: api-storage - mountPath: /data - resources: - requests: - cpu: 20m - memory: 80Mi - env: - # .NET Configuration - - name: ASPNETCORE_ENVIRONMENT - value: Production - - name: ASPNETCORE_URLS - value: http://+:8080 - - - name: Logging__LogLevel__TNO - value: Information - - # Common Service Configuration - - name: Service__ApiUrl - valueFrom: - configMapKeyRef: - name: services - key: API_HOST_URL - - name: Service__EmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_FAILURE_TO - - name: Service__NoticeEmailTo - valueFrom: - configMapKeyRef: - name: services - key: EMAIL_NOTICE_TO - - # Authentication Configuration - - name: Auth__Keycloak__Authority - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUTHORITY - - name: Auth__Keycloak__Audience - valueFrom: - configMapKeyRef: - name: services - key: KEYCLOAK_AUDIENCE - - name: Auth__Keycloak__Secret - valueFrom: - secretKeyRef: - name: keycloak - key: KEYCLOAK_CLIENT_SECRET - - - name: Kafka__Admin__ClientId - valueFrom: - configMapKeyRef: - name: transcription-service - key: KAFKA_CLIENT_ID - - name: Kafka__Admin__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Consumer__GroupId - valueFrom: - configMapKeyRef: - name: transcription-service - key: KAFKA_CLIENT_ID - - name: Kafka__Consumer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - - name: Kafka__Producer__ClientId - valueFrom: - configMapKeyRef: - name: transcription-service - key: KAFKA_CLIENT_ID - - name: Kafka__Producer__BootstrapServers - valueFrom: - configMapKeyRef: - name: services - key: KAFKA_BOOTSTRAP_SERVERS - - # Azure Cognitive Services Configuration - - name: Service__AzureRegion - valueFrom: - secretKeyRef: - name: azure-cognitive-services - key: AZURE_REGION - - name: Service__AzureCognitiveServicesKey - valueFrom: - secretKeyRef: - name: azure-cognitive-services - key: AZURE_COGNITIVE_SERVICES_KEY - - # Service Configuration - - name: Service__MaxFailLimit - valueFrom: - configMapKeyRef: - name: transcription-service - key: MAX_FAIL_LIMIT - - name: Service__Topics - valueFrom: - configMapKeyRef: - name: transcription-service - key: TOPICS - - name: Service__VolumePath - valueFrom: - configMapKeyRef: - name: transcription-service - key: VOLUME_PATH - - name: Service__NotificationTopic - valueFrom: - configMapKeyRef: - name: transcription-service - key: NOTIFICATION_TOPIC - - name: Service__IgnoreContentPublishedBeforeOffset - valueFrom: - configMapKeyRef: - name: transcription-service - key: IGNORE_CONTENT_PUBLISHED_BEFORE_OFFSET - - name: Service__OldTnoContentTagName - valueFrom: - configMapKeyRef: - name: transcription-service - key: OLD_TNO_CONTENT_TAG_NAME - - # S3 Configuration - - name: S3_ACCESS_KEY - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_ACCESS_KEY - - name: S3_SECRET_KEY - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_SECRET_KEY - - name: S3_BUCKET_NAME - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_BUCKET_NAME - - name: S3_SERVICE_URL - valueFrom: - secretKeyRef: - name: s3-backup-credentials - key: S3_SERVICE_URL - - # CHES Configuration - - name: CHES__From - valueFrom: - configMapKeyRef: - name: ches - key: CHES_FROM - - name: CHES__EmailEnabled - valueFrom: - configMapKeyRef: - name: transcription-service - key: CHES_EMAIL_ENABLED - - name: CHES__EmailAuthorized - valueFrom: - configMapKeyRef: - name: transcription-service - key: CHES_EMAIL_AUTHORIZED - - - name: CHES__AuthUrl - valueFrom: - configMapKeyRef: - name: ches - key: CHES_AUTH_URL - - name: CHES__HostUri - valueFrom: - configMapKeyRef: - name: ches - key: CHES_HOST_URI - - name: CHES__Username - valueFrom: - secretKeyRef: - name: ches - key: USERNAME - - name: CHES__Password - valueFrom: - secretKeyRef: - name: ches - key: PASSWORD - livenessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: "/health" - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 30 - periodSeconds: 20 - successThreshold: 1 - failureThreshold: 3 - dnsPolicy: ClusterFirst - restartPolicy: Always - securityContext: {} - terminationGracePeriodSeconds: 30 - test: false - triggers: - - type: ConfigChange - - type: ImageChange - imageChangeParams: - automatic: true - containerNames: - - transcription-service - from: - kind: ImageStreamTag - namespace: 9b301c-tools - name: transcription-service:dev diff --git a/openshift/kustomize/shared_resources/base/ches-config-map.yaml b/openshift/kustomize/shared_resources/base/ches-config-map.yaml deleted file mode 100644 index 319b87f9a6..0000000000 --- a/openshift/kustomize/shared_resources/base/ches-config-map.yaml +++ /dev/null @@ -1,18 +0,0 @@ -kind: ConfigMap -apiVersion: v1 -metadata: - name: ches - namespace: default - annotations: - description: CHES configuration settings - created-by: jeremy.foster - labels: - name: ches - part-of: tno - version: 1.0.0 - component: ches - managed-by: kustomize -data: - CHES_AUTH_URL: https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token - CHES_HOST_URI: https://ches-dev.api.gov.bc.ca/api/v1 - CHES_FROM: Media Monitoring Insights diff --git a/openshift/kustomize/shared_resources/base/kustomization.yaml b/openshift/kustomize/shared_resources/base/kustomization.yaml index f5caf2239a..ceb5a272ea 100644 --- a/openshift/kustomize/shared_resources/base/kustomization.yaml +++ b/openshift/kustomize/shared_resources/base/kustomization.yaml @@ -5,7 +5,7 @@ kind: Kustomization resources: - keycloak-config-map.yaml - services-config-map.yaml - - ches-config-map.yaml + - smtp-config-map.yaml - reporting-shared-config-map.yaml - reporting-azure-ai-config-map.yaml - pvc.yaml diff --git a/openshift/kustomize/shared_resources/base/smtp-config-map.yaml b/openshift/kustomize/shared_resources/base/smtp-config-map.yaml new file mode 100644 index 0000000000..b01fa36521 --- /dev/null +++ b/openshift/kustomize/shared_resources/base/smtp-config-map.yaml @@ -0,0 +1,18 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: smtp + namespace: default + annotations: + description: SMTP configuration settings + created-by: jeremy.foster + labels: + name: smtp + part-of: tno + version: 1.0.0 + component: smtp + managed-by: kustomize +data: + SMTP_HOST: apps.smtp.gov.bc.ca + SMTP_PORT: 25 + SMTP_FROM: Media Monitoring Insights diff --git a/services/net/auto-clipper/AutoClipperManager.cs b/services/net/auto-clipper/AutoClipperManager.cs index 07de0f0e59..fd3061c734 100644 --- a/services/net/auto-clipper/AutoClipperManager.cs +++ b/services/net/auto-clipper/AutoClipperManager.cs @@ -4,9 +4,8 @@ using Confluent.Kafka; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Content; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.Core.Storage; @@ -64,12 +63,12 @@ public AutoClipperManager( ClipProcessingPipeline processingPipeline, IStationConfigurationService stationConfigurationService, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger logger, IS3StorageService s3StorageService) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { this.Listener = listener; this.Listener.IsLongRunningJob = true; @@ -350,7 +349,7 @@ private async Task DownloadS3File(string? s3Path) /// /// /// - private static async Task CleanUpFilesAsync(IEnumerable generatedClipFiles, bool isSyncedToS3, string downloadedFile) + private static void CleanUpFiles(IEnumerable generatedClipFiles, bool isSyncedToS3, string downloadedFile) { CleanupLocalFiles(generatedClipFiles); CleanupTemporaryFiles(isSyncedToS3, downloadedFile); @@ -407,7 +406,7 @@ private async Task ProcessClipRequestAsync(ClipRequestModel request, ContentMode this.Logger.LogWarning("Work order has been cancelled. Content ID: {id}", requestContentId); else this.Logger.LogWarning("Request ignored because it does not have a work order. Content ID: {id}", requestContentId); - await CleanUpFilesAsync(generatedClipFiles, isSyncedToS3, downloadedFile); + CleanUpFiles(generatedClipFiles, isSyncedToS3, downloadedFile); return; } @@ -430,7 +429,7 @@ private async Task ProcessClipRequestAsync(ClipRequestModel request, ContentMode { var exception = new EmptyTranscriptException(requestContentId); await UpdateWorkOrderAsync(request, WorkOrderStatus.Failed, exception); - await CleanUpFilesAsync(generatedClipFiles, isSyncedToS3, downloadedFile); + CleanUpFiles(generatedClipFiles, isSyncedToS3, downloadedFile); return; } @@ -439,7 +438,7 @@ private async Task ProcessClipRequestAsync(ClipRequestModel request, ContentMode if (workOrder?.Status == WorkOrderStatus.Cancelled) { this.Logger.LogWarning("Work order has been cancelled during processing. Content ID: {id}", requestContentId); - await CleanUpFilesAsync(generatedClipFiles, isSyncedToS3, downloadedFile); + CleanUpFiles(generatedClipFiles, isSyncedToS3, downloadedFile); return; } @@ -449,7 +448,7 @@ private async Task ProcessClipRequestAsync(ClipRequestModel request, ContentMode { var exception = new ContentNotFoundException(requestContentId); await UpdateWorkOrderAsync(request, WorkOrderStatus.Failed, exception); - await CleanUpFilesAsync(generatedClipFiles, isSyncedToS3, downloadedFile); + CleanUpFiles(generatedClipFiles, isSyncedToS3, downloadedFile); return; } @@ -506,7 +505,7 @@ private async Task ProcessClipRequestAsync(ClipRequestModel request, ContentMode } await UpdateWorkOrderAsync(request, WorkOrderStatus.Completed); - await CleanUpFilesAsync(generatedClipFiles, isSyncedToS3, downloadedFile); + CleanUpFiles(generatedClipFiles, isSyncedToS3, downloadedFile); } private static ClipDefinition? NormalizeClipDefinition(ClipDefinition definition, IReadOnlyList segments) diff --git a/services/net/auto-clipper/appsettings.Development.json b/services/net/auto-clipper/appsettings.Development.json index 7ea658fedf..b3b30e8521 100644 --- a/services/net/auto-clipper/appsettings.Development.json +++ b/services/net/auto-clipper/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "host.docker.internal:40102", diff --git a/services/net/auto-clipper/appsettings.Staging.json b/services/net/auto-clipper/appsettings.Staging.json index a9f74118fc..040ac2068e 100644 --- a/services/net/auto-clipper/appsettings.Staging.json +++ b/services/net/auto-clipper/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092", diff --git a/services/net/auto-clipper/appsettings.json b/services/net/auto-clipper/appsettings.json index 1f0a93ff7f..7e40eee19c 100644 --- a/services/net/auto-clipper/appsettings.json +++ b/services/net/auto-clipper/appsettings.json @@ -68,12 +68,10 @@ "LlmPromptCharacterLimit": 0, "StationConfigPath": "Config/Stations" }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/ches-retry/ChesRetryManager.cs b/services/net/ches-retry/ChesRetryManager.cs index be25712d36..9fcc50e7d7 100644 --- a/services/net/ches-retry/ChesRetryManager.cs +++ b/services/net/ches-retry/ChesRetryManager.cs @@ -2,8 +2,8 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Entities; using TNO.Services.ChesRetry.Config; @@ -22,6 +22,10 @@ public class ChesRetryManager : ServiceManager #endregion #region Properties + /// + /// get - The CHES service used to make requests to CHES. + /// + protected IChesService Ches { get; } #endregion #region Constructors @@ -32,8 +36,9 @@ public class ChesRetryManager : ServiceManager /// /// /// + /// + /// /// - /// /// /// /// @@ -42,13 +47,15 @@ public class ChesRetryManager : ServiceManager public ChesRetryManager( IApiService api, ClaimsPrincipal user, + IEmailService emailService, + IOptions smtpOptions, IChesService chesService, - IOptions chesOptions, IOptions serializationOptions, IOptions notificationOptions, ILogger logger) - : base(api, chesService, chesOptions, notificationOptions, logger) + : base(api, emailService, smtpOptions, notificationOptions, logger) { + this.Ches = chesService; _user = user; _serializationOptions = serializationOptions.Value; } diff --git a/services/net/ches-retry/ChesRetryService.cs b/services/net/ches-retry/ChesRetryService.cs index 99d620a80d..6a86b566cc 100644 --- a/services/net/ches-retry/ChesRetryService.cs +++ b/services/net/ches-retry/ChesRetryService.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using TNO.Ches; using TNO.Services.ChesRetry.Config; using TNO.Services.Runners; @@ -36,6 +37,7 @@ protected override IServiceCollection ConfigureServices(IServiceCollection servi base.ConfigureServices(services); services .Configure(this.Configuration.GetSection("Service")) + .AddChesSingletonService(this.Configuration.GetSection("CHES")) .AddSingleton(); // TODO: Figure out how to validate without resulting in aggregating the config values. diff --git a/services/net/ches-retry/Dockerfile b/services/net/ches-retry/Dockerfile index 4f8f298b11..cc998cce98 100644 --- a/services/net/ches-retry/Dockerfile +++ b/services/net/ches-retry/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -18,7 +18,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/ches-retry RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . @@ -30,4 +30,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.ChesRetry.dll +ENTRYPOINT ["dotnet", "TNO.Services.ChesRetry.dll"] diff --git a/services/net/ches-retry/TNO.Services.ChesRetry.csproj b/services/net/ches-retry/TNO.Services.ChesRetry.csproj index 8094cc43cc..ccdea582af 100644 --- a/services/net/ches-retry/TNO.Services.ChesRetry.csproj +++ b/services/net/ches-retry/TNO.Services.ChesRetry.csproj @@ -1,5 +1,4 @@  - Exe net9.0 @@ -13,6 +12,17 @@ + + + + + + + + + + + @@ -20,5 +30,4 @@ PreserveNewest - diff --git a/services/net/ches-retry/appsettings.json b/services/net/ches-retry/appsettings.json index 62fcf24fca..0d1aeca03c 100644 --- a/services/net/ches-retry/appsettings.json +++ b/services/net/ches-retry/appsettings.json @@ -43,6 +43,11 @@ "EmailEnabled": true, "EmailAuthorized": false }, + "Smtp": { + "From": "Media Monitoring Insights ", + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 + }, "Auth": { "Keycloak": { "Authority": "https://loginproxy.gov.bc.ca/auth", diff --git a/services/net/content/ContentManager.cs b/services/net/content/ContentManager.cs index c55e1098eb..19b701e6cb 100644 --- a/services/net/content/ContentManager.cs +++ b/services/net/content/ContentManager.cs @@ -4,12 +4,11 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using Renci.SshNet; using Renci.SshNet.Common; using TNO.API.Areas.Services.Models.Content; using TNO.API.Areas.Services.Models.DataLocation; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.Entities; @@ -71,8 +70,8 @@ public class ContentManager : ServiceManager /// /// /// - /// - /// + /// + /// /// /// /// @@ -80,12 +79,12 @@ public ContentManager( IKafkaAdmin kafkaAdmin, IKafkaListener kafkaListener, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger logger, IMemoryCache memoryCache) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { this.KafkaAdmin = kafkaAdmin; this.Listener = kafkaListener; diff --git a/services/net/content/appsettings.Development.json b/services/net/content/appsettings.Development.json index cd9ccc3bdf..bdc2bfc690 100644 --- a/services/net/content/appsettings.Development.json +++ b/services/net/content/appsettings.Development.json @@ -9,13 +9,6 @@ "Service": { "MaxFailLimit": 5 }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://dev.loginproxy.gov.bc.ca/auth", diff --git a/services/net/content/appsettings.Staging.json b/services/net/content/appsettings.Staging.json index 92a02e5c33..e5429f351e 100644 --- a/services/net/content/appsettings.Staging.json +++ b/services/net/content/appsettings.Staging.json @@ -9,13 +9,6 @@ "Service": { "MaxFailLimit": 5 }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://test.loginproxy.gov.bc.ca/auth", diff --git a/services/net/content/appsettings.json b/services/net/content/appsettings.json index 5ab00d3f91..20bda49c72 100644 --- a/services/net/content/appsettings.json +++ b/services/net/content/appsettings.json @@ -45,6 +45,11 @@ "EmailEnabled": true, "EmailAuthorized": false }, + "Smtp": { + "From": "Media Monitoring Insights ", + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 + }, "Auth": { "Keycloak": { "Authority": "https://loginproxy.gov.bc.ca/auth", diff --git a/services/net/contentmigration/appsettings.Development.json b/services/net/contentmigration/appsettings.Development.json index b49baaceb0..094e25c209 100644 --- a/services/net/contentmigration/appsettings.Development.json +++ b/services/net/contentmigration/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://dev.loginproxy.gov.bc.ca/auth", diff --git a/services/net/contentmigration/appsettings.Staging.json b/services/net/contentmigration/appsettings.Staging.json index ee2d979675..3d777a1524 100644 --- a/services/net/contentmigration/appsettings.Staging.json +++ b/services/net/contentmigration/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://test.loginproxy.gov.bc.ca/auth", diff --git a/services/net/contentmigration/appsettings.json b/services/net/contentmigration/appsettings.json index 0ae9037101..f72b392f14 100644 --- a/services/net/contentmigration/appsettings.json +++ b/services/net/contentmigration/appsettings.json @@ -149,12 +149,10 @@ }, "SendEmailOnFailure": true }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/event-handler/EventHandlerManager.cs b/services/net/event-handler/EventHandlerManager.cs index fd6fa612fc..aac2b17fbe 100644 --- a/services/net/event-handler/EventHandlerManager.cs +++ b/services/net/event-handler/EventHandlerManager.cs @@ -2,8 +2,7 @@ using Confluent.Kafka; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; +using MMI.SmtpEmail; using TNO.Core.Exceptions; using TNO.Kafka; using TNO.Kafka.Models; @@ -38,20 +37,20 @@ public class EventHandlerManager : ServiceManager /// /// /// - /// - /// + /// + /// /// /// /// public EventHandlerManager( IKafkaListener listener, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions eventHandlerOptions, IOptions serializationOptions, ILogger logger) - : base(api, chesService, chesOptions, eventHandlerOptions, logger) + : base(api, emailService, smtpOptions, eventHandlerOptions, logger) { _serializationOptions = serializationOptions.Value; this.Listener = listener; diff --git a/services/net/event-handler/appsettings.Development.json b/services/net/event-handler/appsettings.Development.json index d42d19d0b8..035758ae78 100644 --- a/services/net/event-handler/appsettings.Development.json +++ b/services/net/event-handler/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/event-handler/appsettings.Staging.json b/services/net/event-handler/appsettings.Staging.json index 303ff8fea0..f442a087f5 100644 --- a/services/net/event-handler/appsettings.Staging.json +++ b/services/net/event-handler/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/event-handler/appsettings.json b/services/net/event-handler/appsettings.json index d904da800e..9f90d07637 100644 --- a/services/net/event-handler/appsettings.json +++ b/services/net/event-handler/appsettings.json @@ -38,12 +38,10 @@ "Topics": "event-schedule", "SendEmailOnFailure": true }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/extract-quotes/ExtractQuotesManager.cs b/services/net/extract-quotes/ExtractQuotesManager.cs index 4034437a2d..33361d1256 100644 --- a/services/net/extract-quotes/ExtractQuotesManager.cs +++ b/services/net/extract-quotes/ExtractQuotesManager.cs @@ -1,16 +1,13 @@ -using System.Collections.Concurrent; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks.Dataflow; using Confluent.Kafka; using HtmlAgilityPack; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Content; using TNO.API.Areas.Services.Models.Minister; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Kafka; using TNO.Kafka.Models; @@ -70,8 +67,8 @@ public partial class ExtractQuotesManager : ServiceManager /// /// /// - /// - /// + /// + /// /// /// /// @@ -79,12 +76,12 @@ public ExtractQuotesManager( IKafkaListener listener, IApiService api, ICoreNLPService coreNLPService, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions extractQuotesOptions, IMemoryCache memoryCache, ILogger logger) - : base(api, chesService, chesOptions, extractQuotesOptions, logger) + : base(api, emailService, smtpOptions, extractQuotesOptions, logger) { Listener = listener; Listener.IsLongRunningJob = true; diff --git a/services/net/extract-quotes/appsettings.Development.json b/services/net/extract-quotes/appsettings.Development.json index d42d19d0b8..035758ae78 100644 --- a/services/net/extract-quotes/appsettings.Development.json +++ b/services/net/extract-quotes/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/extract-quotes/appsettings.Staging.json b/services/net/extract-quotes/appsettings.Staging.json index 303ff8fea0..f442a087f5 100644 --- a/services/net/extract-quotes/appsettings.Staging.json +++ b/services/net/extract-quotes/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/extract-quotes/appsettings.json b/services/net/extract-quotes/appsettings.json index 6b4974610a..8c58792ffa 100644 --- a/services/net/extract-quotes/appsettings.json +++ b/services/net/extract-quotes/appsettings.json @@ -47,12 +47,10 @@ "FallbackApiUrl": "PLACEHOLDER_FALLBACK_URL", "MaxRequestsPerMinute": 10 }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/ffmpeg/FFmpegManager.cs b/services/net/ffmpeg/FFmpegManager.cs index ae36792c42..4b1655d041 100644 --- a/services/net/ffmpeg/FFmpegManager.cs +++ b/services/net/ffmpeg/FFmpegManager.cs @@ -2,9 +2,8 @@ using FTTLib; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Content; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.Entities; @@ -41,16 +40,18 @@ public class FFmpegManager : ServiceManager /// /// /// + /// + /// /// /// public FFmpegManager( IKafkaListener listener, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger logger) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { this.Listener = listener; this.Listener.IsLongRunningJob = true; diff --git a/services/net/ffmpeg/appsettings.Development.json b/services/net/ffmpeg/appsettings.Development.json index d42d19d0b8..035758ae78 100644 --- a/services/net/ffmpeg/appsettings.Development.json +++ b/services/net/ffmpeg/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/ffmpeg/appsettings.Staging.json b/services/net/ffmpeg/appsettings.Staging.json index 303ff8fea0..f442a087f5 100644 --- a/services/net/ffmpeg/appsettings.Staging.json +++ b/services/net/ffmpeg/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/ffmpeg/appsettings.json b/services/net/ffmpeg/appsettings.json index 5b388d0064..6f94f752c5 100644 --- a/services/net/ffmpeg/appsettings.json +++ b/services/net/ffmpeg/appsettings.json @@ -56,12 +56,10 @@ } ] }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/filemonitor/Dockerfile b/services/net/filemonitor/Dockerfile index fbc331044b..28b22abe7f 100644 --- a/services/net/filemonitor/Dockerfile +++ b/services/net/filemonitor/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -21,7 +21,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/filemonitor RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy # [Optional] Uncomment this section to install additional OS packages. RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ @@ -40,4 +40,4 @@ RUN chmod -R 0777 /data # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.FileMonitor.dll +ENTRYPOINT ["dotnet", "TNO.Services.FileMonitor.dll"] diff --git a/services/net/filemonitor/FileMonitorIngestActionManager.cs b/services/net/filemonitor/FileMonitorIngestActionManager.cs index aa57a1771d..93f3c01005 100644 --- a/services/net/filemonitor/FileMonitorIngestActionManager.cs +++ b/services/net/filemonitor/FileMonitorIngestActionManager.cs @@ -1,8 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Ingest; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Models.Extensions; using TNO.Services.Actions.Managers; using TNO.Services.FileMonitor.Config; @@ -20,20 +19,18 @@ public class FileMonitorIngestActionManager : IngestActionManager /// /// - /// - /// + /// /// /// /// public FileMonitorIngestActionManager( IngestModel ingest, IApiService api, - IChesService ches, - IOptions chesOptions, + IEmailService emailService, IIngestAction action, IOptions options, ILogger logger) - : base(ingest, api, ches, chesOptions, action, options, logger) + : base(ingest, api, emailService, action, options, logger) { } #endregion diff --git a/services/net/filemonitor/FilemonitorManager.cs b/services/net/filemonitor/FilemonitorManager.cs index b9f51937c2..f412a11bc0 100644 --- a/services/net/filemonitor/FilemonitorManager.cs +++ b/services/net/filemonitor/FilemonitorManager.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Services.Managers; +using MMI.SmtpEmail; using TNO.Services.FileMonitor.Config; -using TNO.Ches; -using TNO.Ches.Configuration; +using TNO.Services.Managers; namespace TNO.Services.FileMonitor; @@ -18,20 +17,20 @@ public class FileMonitorManager : IngestManager /// /// - /// - /// + /// + /// /// /// /// public FileMonitorManager( IServiceProvider serviceProvider, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IngestManagerFactory factory, IOptions options, ILogger logger) - : base(serviceProvider, api, chesService, chesOptions, factory, options, logger) + : base(serviceProvider, api, emailService, smtpOptions, factory, options, logger) { } #endregion diff --git a/services/net/filemonitor/appsettings.Development.json b/services/net/filemonitor/appsettings.Development.json index 49cd6ce827..7d8bdd3147 100644 --- a/services/net/filemonitor/appsettings.Development.json +++ b/services/net/filemonitor/appsettings.Development.json @@ -11,13 +11,6 @@ "ApiUrl": "http://host.docker.internal:40010/api", "FailedStoryBody": "PUBLISHER SENT INVALID FILE FORMAT" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://dev.loginproxy.gov.bc.ca/auth", diff --git a/services/net/filemonitor/appsettings.Staging.json b/services/net/filemonitor/appsettings.Staging.json index ee2d979675..3d777a1524 100644 --- a/services/net/filemonitor/appsettings.Staging.json +++ b/services/net/filemonitor/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://test.loginproxy.gov.bc.ca/auth", diff --git a/services/net/filemonitor/appsettings.json b/services/net/filemonitor/appsettings.json index 15bed8e8be..fda4a1d3b3 100644 --- a/services/net/filemonitor/appsettings.json +++ b/services/net/filemonitor/appsettings.json @@ -38,12 +38,10 @@ "PrivateKeysPath": "keys", "SendEmailOnFailure": true }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/fileupload/Dockerfile b/services/net/fileupload/Dockerfile index cc365fb97e..5d7e3abd1a 100644 --- a/services/net/fileupload/Dockerfile +++ b/services/net/fileupload/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -17,7 +17,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/fileupload RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . diff --git a/services/net/fileupload/FileUploadManager.cs b/services/net/fileupload/FileUploadManager.cs index 126823b68c..f08a24cb8b 100644 --- a/services/net/fileupload/FileUploadManager.cs +++ b/services/net/fileupload/FileUploadManager.cs @@ -1,30 +1,32 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; -using TNO.Core.Exceptions; -using TNO.Services.Managers; -using TNO.API.Areas.Services.Models.Content; -using System.Net.Http.Json; +using MMI.SmtpEmail; using TNO.Services.FileUpload.Config; +using TNO.Services.Managers; + namespace TNO.Services.FileUpload; -using System.Net.Http; -using System.Net.Http.Json; -using Microsoft.AspNetCore.Mvc; -using TNO.Core.Http; + public class FileUploadManager : ServiceManager { #region Variables #endregion #region Constructors + /// + /// Creates a new instance of a FileUploadManager object, initializes with specified parameters. + /// + /// + /// + /// + /// + /// public FileUploadManager( IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger logger) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { } #endregion @@ -71,4 +73,4 @@ public override async Task RunAsync() } } #endregion -} \ No newline at end of file +} diff --git a/services/net/fileupload/FileUploadService .cs b/services/net/fileupload/FileUploadService .cs index 803a8c0455..f313db8f2c 100644 --- a/services/net/fileupload/FileUploadService .cs +++ b/services/net/fileupload/FileUploadService .cs @@ -1,12 +1,5 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; -using TNO.Services; -using TNO.Services.Managers; -using TNO.Services.FileUpload.Config; using Microsoft.Extensions.DependencyInjection; - +using TNO.Services.FileUpload.Config; using TNO.Services.Runners; namespace TNO.Services.FileUpload; @@ -36,4 +29,4 @@ protected override IServiceCollection ConfigureServices(IServiceCollection servi } #endregion -} \ No newline at end of file +} diff --git a/services/net/fileupload/appsettings.Development.json b/services/net/fileupload/appsettings.Development.json index b49baaceb0..094e25c209 100644 --- a/services/net/fileupload/appsettings.Development.json +++ b/services/net/fileupload/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://dev.loginproxy.gov.bc.ca/auth", diff --git a/services/net/fileupload/appsettings.Staging.json b/services/net/fileupload/appsettings.Staging.json index ee2d979675..3d777a1524 100644 --- a/services/net/fileupload/appsettings.Staging.json +++ b/services/net/fileupload/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://test.loginproxy.gov.bc.ca/auth", diff --git a/services/net/fileupload/appsettings.json b/services/net/fileupload/appsettings.json index 90f05c5e02..8db4f73526 100644 --- a/services/net/fileupload/appsettings.json +++ b/services/net/fileupload/appsettings.json @@ -40,12 +40,10 @@ "DaysBeforeStart": 32, "DaysBeforeEnd": 30 }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/folder-collection/Dockerfile b/services/net/folder-collection/Dockerfile index 6b76f98761..760cf13d61 100644 --- a/services/net/folder-collection/Dockerfile +++ b/services/net/folder-collection/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -20,7 +20,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/folder-collection RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . @@ -32,4 +32,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.FolderCollection.dll +ENTRYPOINT ["dotnet", "TNO.Services.FolderCollection.dll"] diff --git a/services/net/folder-collection/FolderCollectionManager.cs b/services/net/folder-collection/FolderCollectionManager.cs index 4f3041c403..c7a3c92f59 100644 --- a/services/net/folder-collection/FolderCollectionManager.cs +++ b/services/net/folder-collection/FolderCollectionManager.cs @@ -3,8 +3,7 @@ using Confluent.Kafka; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; +using MMI.SmtpEmail; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.Elastic; @@ -58,8 +57,8 @@ public class FolderCollectionManager : ServiceManager /// /// /// - /// - /// + /// + /// /// /// public FolderCollectionManager( @@ -68,11 +67,11 @@ public FolderCollectionManager( IKafkaAdmin kafkaAdmin, IKafkaListener consumer, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger logger) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { this.Client = elasticClient; this.ElasticOptions = elasticOptions.Value; diff --git a/services/net/folder-collection/appsettings.Development.json b/services/net/folder-collection/appsettings.Development.json index 8f0a111030..8ffbceee5c 100644 --- a/services/net/folder-collection/appsettings.Development.json +++ b/services/net/folder-collection/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Admin": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/folder-collection/appsettings.Staging.json b/services/net/folder-collection/appsettings.Staging.json index ab5f29b9ed..ca171bb50a 100644 --- a/services/net/folder-collection/appsettings.Staging.json +++ b/services/net/folder-collection/appsettings.Staging.json @@ -9,13 +9,6 @@ "Service": { "MaxFailLimit": 5 }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Admin": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/folder-collection/appsettings.json b/services/net/folder-collection/appsettings.json index 32c75c91a7..30c34004a9 100644 --- a/services/net/folder-collection/appsettings.json +++ b/services/net/folder-collection/appsettings.json @@ -43,12 +43,10 @@ "ContentIndex": "unpublished_content", "PublishedIndex": "content" }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/image/Dockerfile b/services/net/image/Dockerfile index f26b153e29..4af9aac21e 100644 --- a/services/net/image/Dockerfile +++ b/services/net/image/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -21,7 +21,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/image RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends procps curl ffmpeg libc6-dev libgdiplus @@ -43,4 +43,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Run container by default as user with id 1001 (default) (Test) USER 1001 -ENTRYPOINT dotnet TNO.Services.Image.dll +ENTRYPOINT ["dotnet", "TNO.Services.Image.dll"] diff --git a/services/net/image/ImageIngestActionManager.cs b/services/net/image/ImageIngestActionManager.cs index 3e167946ec..37d55f79b1 100644 --- a/services/net/image/ImageIngestActionManager.cs +++ b/services/net/image/ImageIngestActionManager.cs @@ -1,8 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Ingest; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Services.Actions.Managers; using TNO.Services.Image.Config; @@ -30,12 +29,11 @@ public class ImageIngestActionManager : IngestActionManager public ImageIngestActionManager( IngestModel dataSource, IApiService api, - IChesService ches, - IOptions chesOptions, + IEmailService emailService, IIngestAction action, IOptions options, ILogger logger) - : base(dataSource, api, ches, chesOptions, action, options, logger) + : base(dataSource, api, emailService, action, options, logger) { } #endregion diff --git a/services/net/image/ImageManager.cs b/services/net/image/ImageManager.cs index 0b0d88b4f0..b08972777f 100644 --- a/services/net/image/ImageManager.cs +++ b/services/net/image/ImageManager.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Services.Managers; +using MMI.SmtpEmail; using TNO.Services.Image.Config; -using TNO.Ches; -using TNO.Ches.Configuration; +using TNO.Services.Managers; namespace TNO.Services.Image; @@ -18,20 +17,20 @@ public class ImageManager : IngestManager /// /// - /// - /// + /// + /// /// /// /// public ImageManager( IServiceProvider serviceProvider, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IngestManagerFactory factory, IOptions options, ILogger logger) - : base(serviceProvider, api, chesService, chesOptions, factory, options, logger) + : base(serviceProvider, api, emailService, smtpOptions, factory, options, logger) { } #endregion diff --git a/services/net/image/TNO.Services.Image.csproj b/services/net/image/TNO.Services.Image.csproj index 0e9d95d612..04ab3616a8 100644 --- a/services/net/image/TNO.Services.Image.csproj +++ b/services/net/image/TNO.Services.Image.csproj @@ -11,10 +11,13 @@ - + + + + PreserveNewest diff --git a/services/net/image/appsettings.Development.json b/services/net/image/appsettings.Development.json index b49baaceb0..094e25c209 100644 --- a/services/net/image/appsettings.Development.json +++ b/services/net/image/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://dev.loginproxy.gov.bc.ca/auth", diff --git a/services/net/image/appsettings.Staging.json b/services/net/image/appsettings.Staging.json index ee2d979675..3d777a1524 100644 --- a/services/net/image/appsettings.Staging.json +++ b/services/net/image/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Auth": { "Keycloak": { "Authority": "https://test.loginproxy.gov.bc.ca/auth", diff --git a/services/net/image/appsettings.json b/services/net/image/appsettings.json index 362b52ae3e..d9b03af3e4 100644 --- a/services/net/image/appsettings.json +++ b/services/net/image/appsettings.json @@ -41,12 +41,10 @@ "PrivateKeysPath": "keys", "SendEmailOnFailure": true }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/indexing/Dockerfile b/services/net/indexing/Dockerfile index 4f9cb62208..58c54c7d6b 100644 --- a/services/net/indexing/Dockerfile +++ b/services/net/indexing/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -20,7 +20,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/indexing RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . @@ -32,4 +32,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.Indexing.dll +ENTRYPOINT ["dotnet", "TNO.Services.Indexing.dll"] diff --git a/services/net/indexing/IndexingManager.cs b/services/net/indexing/IndexingManager.cs index 78558a8c61..176172629c 100644 --- a/services/net/indexing/IndexingManager.cs +++ b/services/net/indexing/IndexingManager.cs @@ -4,9 +4,8 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Content; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Elastic; using TNO.Entities; @@ -63,9 +62,9 @@ public class IndexingManager : ServiceManager /// /// /// - /// + /// + /// /// - /// /// /// /// @@ -73,13 +72,13 @@ public IndexingManager( IKafkaAdmin kafkaAdmin, IKafkaListener listener, IApiService api, - IChesService chesService, + IEmailService emailService, + IOptions smtpOptions, IOptions elasticOptions, - IOptions chesOptions, IOptions options, ILogger logger, IMemoryCache memoryCache) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { this.ElasticOptions = elasticOptions.Value; this.KafkaAdmin = kafkaAdmin; diff --git a/services/net/indexing/TNO.Services.Indexing.csproj b/services/net/indexing/TNO.Services.Indexing.csproj index e151df8147..4b3f44c84d 100644 --- a/services/net/indexing/TNO.Services.Indexing.csproj +++ b/services/net/indexing/TNO.Services.Indexing.csproj @@ -22,6 +22,7 @@ + diff --git a/services/net/indexing/appsettings.Development.json b/services/net/indexing/appsettings.Development.json index 11c77e7415..938cd808e2 100644 --- a/services/net/indexing/appsettings.Development.json +++ b/services/net/indexing/appsettings.Development.json @@ -13,13 +13,6 @@ "Elastic": { "Url": "http://host.docker.internal:40003" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Admin": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/indexing/appsettings.Staging.json b/services/net/indexing/appsettings.Staging.json index ab5f29b9ed..ca171bb50a 100644 --- a/services/net/indexing/appsettings.Staging.json +++ b/services/net/indexing/appsettings.Staging.json @@ -9,13 +9,6 @@ "Service": { "MaxFailLimit": 5 }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Admin": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/indexing/appsettings.json b/services/net/indexing/appsettings.json index 197cf63441..9c018fbdba 100644 --- a/services/net/indexing/appsettings.json +++ b/services/net/indexing/appsettings.json @@ -45,12 +45,10 @@ "ContentIndex": "unpublished_content", "PublishedIndex": "content" }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/nlp/NuGet.Config b/services/net/nlp/Docker.NuGet.Config similarity index 100% rename from services/net/nlp/NuGet.Config rename to services/net/nlp/Docker.NuGet.Config diff --git a/services/net/nlp/Dockerfile b/services/net/nlp/Dockerfile index 9de25b46ff..6ff42670a4 100644 --- a/services/net/nlp/Dockerfile +++ b/services/net/nlp/Dockerfile @@ -13,6 +13,7 @@ WORKDIR /src COPY ../../../libs/net/packages /root/.nuget/packages COPY services/net/nlp services/net/nlp +RUN mv services/net/nlp/Docker.NuGet.Config services/net/nlp/NuGet.Config COPY libs/net libs/net RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u "$1"; shift; done } && \ diff --git a/services/net/nlp/NLPManager.cs b/services/net/nlp/NLPManager.cs index b35710acbc..d2546c795f 100644 --- a/services/net/nlp/NLPManager.cs +++ b/services/net/nlp/NLPManager.cs @@ -2,9 +2,8 @@ using Confluent.Kafka; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Content; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Entities; using TNO.Kafka; @@ -49,19 +48,19 @@ public class NlpManager : ServiceManager /// /// /// - /// - /// + /// + /// /// /// public NlpManager( IKafkaListener consumer, IKafkaMessenger producer, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger logger) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { this.Producer = producer; this.Listener = consumer; diff --git a/services/net/nlp/TNO.Services.NLP.csproj b/services/net/nlp/TNO.Services.NLP.csproj index 9bb1915fae..959769b422 100644 --- a/services/net/nlp/TNO.Services.NLP.csproj +++ b/services/net/nlp/TNO.Services.NLP.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable TNO.Services.NLP @@ -11,9 +11,7 @@ - - PreserveNewest - + @@ -27,7 +25,13 @@ - + + + + + + PreserveNewest + diff --git a/services/net/nlp/appsettings.Development.json b/services/net/nlp/appsettings.Development.json index c1af09d4af..5e9c8b15ef 100644 --- a/services/net/nlp/appsettings.Development.json +++ b/services/net/nlp/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/nlp/appsettings.Staging.json b/services/net/nlp/appsettings.Staging.json index f06c9d6d9b..b643b9c12d 100644 --- a/services/net/nlp/appsettings.Staging.json +++ b/services/net/nlp/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/nlp/appsettings.json b/services/net/nlp/appsettings.json index 2b7f3e8766..4f70417ccb 100644 --- a/services/net/nlp/appsettings.json +++ b/services/net/nlp/appsettings.json @@ -39,12 +39,10 @@ "IndexingTopic": "index", "SendEmailOnFailure": true }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/notification/Dockerfile b/services/net/notification/Dockerfile index 9ea96ec28e..a3704e94f4 100644 --- a/services/net/notification/Dockerfile +++ b/services/net/notification/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -18,7 +18,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/notification RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . @@ -30,4 +30,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.Notification.dll +ENTRYPOINT ["dotnet", "TNO.Services.Notification.dll"] diff --git a/services/net/notification/NotificationManager.cs b/services/net/notification/NotificationManager.cs index b477648a42..073272fd0b 100644 --- a/services/net/notification/NotificationManager.cs +++ b/services/net/notification/NotificationManager.cs @@ -1,11 +1,11 @@ +using System.Net.Mail; using System.Security.Claims; using System.Text.Json; using Confluent.Kafka; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; -using TNO.Ches.Models; +using MMI.SmtpEmail; +using MMI.SmtpEmail.Models; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.Entities; @@ -63,8 +63,8 @@ public class NotificationManager : ServiceManager /// /// /// - /// - /// + /// + /// /// /// /// @@ -75,14 +75,14 @@ public NotificationManager( IApiService api, ClaimsPrincipal user, INotificationEngine notificationEngine, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions serializationOptions, IOptions notificationOptions, IOptions reportingOptions, INotificationValidator notificationValidator, ILogger logger) - : base(api, chesService, chesOptions, notificationOptions, logger) + : base(api, emailService, smtpOptions, notificationOptions, logger) { _user = user; this.NotificationEngine = notificationEngine; @@ -429,7 +429,7 @@ private async Task SendNotificationAsync( API.Areas.Services.Models.Notification.NotificationModel notification, API.Areas.Services.Models.Content.ContentModel content) { - await HandleChesEmailOverrideAsync(request); + await HandleEmailOverrideAsync(request); var subscribers = await GetNotificationSubscribersAsync(notification, content); @@ -437,10 +437,10 @@ private async Task SendNotificationAsync( var subject = string.Empty; if (!String.IsNullOrWhiteSpace(request.To)) { - var contexts = new List(); + var contexts = new List(); // When a notification request has specified 'To' it means only send it to the emails in that property. var requestTo = request.To.Split(",").Select(v => v.Trim()); - contexts.Add(new EmailContextModel(requestTo, new Dictionary(), DateTime.Now)); + contexts.Add(new MailMergeModel(requestTo, new Dictionary())); // There are no subscribers, or a notification has been sent for this content to all the subscribers. if (!contexts.Any()) { @@ -461,7 +461,7 @@ private async Task SendNotificationAsync( { foreach (var subscriber in subscribers) { - var contexts = new List(); + var contexts = new List(); contexts.AddRange(this.NotificationValidator.GetSubscriberEmails(new List() { subscriber })); // There are no subscribers, or a notification has been sent for this content to all the subscribers. @@ -477,76 +477,52 @@ private async Task SendNotificationAsync( } } - private async void SendOutNotificationEmailsAsync(IEnumerable contexts, string subject, string body, + private async void SendOutNotificationEmailsAsync(IEnumerable contexts, string subject, string body, IEnumerable subscriber, NotificationRequestModel request, API.Areas.Services.Models.Notification.NotificationModel notification, API.Areas.Services.Models.Content.ContentModel content) { - var merge = new EmailMergeModel(this.ChesOptions.From, contexts, subject, body) - { - // TODO: Extract values from notification settings. - Encoding = EmailEncodings.Utf8, - BodyType = EmailBodyTypes.Html, - Priority = EmailPriorities.Normal, - }; // Add the subscribers to the notification validator so that they don't receive more than one email for a specific content item. this.NotificationValidator.AddUsers(subscriber); var allEmails = String.Join(", ", contexts.Select(c => String.Join(", ", c.To))); - try - { - var response = await this.Ches.SendEmailAsync(merge); - this.Logger.LogInformation($"Notification sent to CHES. Notification: {notification.Id}, Content ID: {content.Id}, Subscriber: {subscriber.FirstOrDefault()?.DisplayName}, Emails: {allEmails}"); + var (success, response) = await this.EmailService.TrySendAsync(contexts, subject, body, null, true); - if (!request.IsPreview) - { - // Save the notification instance. - var instance = new NotificationInstance(notification.Id, content.Id) - { - Status = NotificationStatus.Accepted, - SentOn = DateTime.UtcNow, - OwnerId = request.RequestorId ?? notification.OwnerId, - Response = JsonDocument.Parse(JsonSerializer.Serialize(response, _serializationOptions)), - Subject = subject, - Body = body, - }; - await this.Api.AddNotificationInstanceAsync(new API.Areas.Services.Models.NotificationInstance.NotificationInstanceModel(instance, _serializationOptions)); - } + if (success) + { + this.Logger.LogInformation("Notification sent. Notification: {notificationId}, Content ID: {contentId}, Subscriber: {subscriberDisplayName}, Emails: {allEmails}", notification.Id, content.Id, subscriber.FirstOrDefault()?.DisplayName, allEmails); } - catch (ChesException ex) + + if (!request.IsPreview) { - this.Logger.LogError(ex, "Failed to send email. Notification: {notificationId}, Content ID: {contentId}, Emails: {emails}", notification.Id, content.Id, allEmails); - if (!request.IsPreview) + var instance = new NotificationInstance(notification.Id, content.Id) { - var instance = new NotificationInstance(notification.Id, content.Id) - { - Status = NotificationStatus.Failed, - OwnerId = request.RequestorId ?? notification.OwnerId, - Response = JsonDocument.Parse(JsonSerializer.Serialize(ex.Data["error"], _serializationOptions)), - Subject = subject, - Body = body, - }; - await this.Api.AddNotificationInstanceAsync(new API.Areas.Services.Models.NotificationInstance.NotificationInstanceModel(instance, _serializationOptions)); - } + Status = success ? NotificationStatus.Completed : NotificationStatus.Failed, + OwnerId = request.RequestorId ?? notification.OwnerId, + Response = JsonDocument.Parse(JsonSerializer.Serialize(response, _serializationOptions)), + Subject = subject, + Body = body, + }; + await this.Api.AddNotificationInstanceAsync(new API.Areas.Services.Models.NotificationInstance.NotificationInstanceModel(instance)); } } /// - /// If CHES has been configured to send emails to the user we need to provide an appropriate user. + /// If SMTP has been configured to send emails to the user we need to provide an appropriate user. /// /// /// - private async Task HandleChesEmailOverrideAsync(NotificationRequestModel request) + private async Task HandleEmailOverrideAsync(NotificationRequestModel request) { // The requestor becomes the current user. - var email = this.ChesOptions.OverrideTo ?? ""; + var email = this.SmtpOptions.OverrideTo ?? ""; if (request.RequestorId.HasValue) { var user = await this.Api.GetUserAsync(request.RequestorId.Value); if (user != null) email = user.GetEmail(); } - var identity = _user.Identity as ClaimsIdentity ?? throw new ConfigurationException("CHES requires an active ClaimsPrincipal"); + var identity = _user.Identity as ClaimsIdentity ?? throw new ConfigurationException("SMTP override requires an active ClaimsPrincipal"); identity.RemoveClaim(_user.FindFirst(ClaimTypes.Email)); identity.AddClaim(new Claim(ClaimTypes.Email, email)); } diff --git a/services/net/notification/Validation/INotificationValidator.cs b/services/net/notification/Validation/INotificationValidator.cs index 541905e7c0..52e8d6277b 100644 --- a/services/net/notification/Validation/INotificationValidator.cs +++ b/services/net/notification/Validation/INotificationValidator.cs @@ -1,4 +1,4 @@ -using TNO.Ches.Models; +using MMI.SmtpEmail.Models; namespace TNO.Services.Notification.Validation; @@ -66,6 +66,6 @@ public interface INotificationValidator /// /// /// - IEnumerable GetSubscriberEmails(IEnumerable users); + IEnumerable GetSubscriberEmails(IEnumerable users); #endregion } diff --git a/services/net/notification/Validation/NotificationValidator.cs b/services/net/notification/Validation/NotificationValidator.cs index e23fe9499d..fe21c899b1 100644 --- a/services/net/notification/Validation/NotificationValidator.cs +++ b/services/net/notification/Validation/NotificationValidator.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches.Models; +using MMI.SmtpEmail.Models; using TNO.Core.Extensions; using TNO.Elastic; using TNO.Entities; @@ -309,24 +309,15 @@ public bool ValidateSentToUser(int userId) /// /// /// - public IEnumerable GetSubscriberEmails(IEnumerable users) + public IEnumerable GetSubscriberEmails(IEnumerable users) { - var now = DateTime.Now; - var emails = new List(); - if (this.Notification == null) return Array.Empty(); + var emails = new List(); + if (this.Notification == null) return []; // Remove any subscribers who have already received a notification for the current process execution. return users.Where(u => !this.SentToUsers.Contains(u.Id)).Select(user => { - var context = new Dictionary() { - { "id", user.Id }, - { "firstName", user.FirstName ?? "" }, - { "lastName", user.LastName ?? "" }, - }; - return new EmailContextModel(new[] { user.GetEmail() }, context, now) - { - Tag = $"{this.Notification.Name}", - }; + return new MailMergeModel([user.GetEmail()]).PopulateContextFromUser(user); }); } diff --git a/services/net/notification/appsettings.Development.json b/services/net/notification/appsettings.Development.json index ed7ee68b03..a9fde2ef48 100644 --- a/services/net/notification/appsettings.Development.json +++ b/services/net/notification/appsettings.Development.json @@ -16,13 +16,6 @@ "RequestTranscriptUrl": "https://dev.mmi.gov.bc.ca/api/subscriber/work/orders/transcribe/", "AddToReportUrl": "https://dev.mmi.gov.bc.ca" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/notification/appsettings.Staging.json b/services/net/notification/appsettings.Staging.json index ffc8548aad..88c622dbc5 100644 --- a/services/net/notification/appsettings.Staging.json +++ b/services/net/notification/appsettings.Staging.json @@ -16,13 +16,6 @@ "RequestTranscriptUrl": "https://test.mmi.gov.bc.ca/api/subscriber/work/orders/transcribe/", "AddToReportUrl": "https://test.mmi.gov.bc.ca" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/notification/appsettings.json b/services/net/notification/appsettings.json index 8be7ce9607..a2a5bd2899 100644 --- a/services/net/notification/appsettings.json +++ b/services/net/notification/appsettings.json @@ -49,12 +49,10 @@ "ContentIndex": "unpublished_content", "PublishedIndex": "content" }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/reporting/Config/ReportingOptions.cs b/services/net/reporting/Config/ReportingOptions.cs index dcd47f6950..463f706913 100644 --- a/services/net/reporting/Config/ReportingOptions.cs +++ b/services/net/reporting/Config/ReportingOptions.cs @@ -39,11 +39,6 @@ public class ReportingOptions : ServiceOptions /// public int RetryConcurrencyFailureLimit { get; set; } = 10; - /// - /// get/set - The default email address to use when sending reports. - /// - public string DefaultFrom { get; set; } = ""; - /// /// get/set - The path to where images are saved. /// diff --git a/services/net/reporting/Dockerfile b/services/net/reporting/Dockerfile index 347c64e598..4864728edd 100644 --- a/services/net/reporting/Dockerfile +++ b/services/net/reporting/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -19,7 +19,7 @@ WORKDIR /src/services/net/reporting RUN dotnet restore RUN dotnet publish "TNO.Services.Reporting.csproj" -c "$BUILD_CONFIGURATION" -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . @@ -31,4 +31,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.Reporting.dll +ENTRYPOINT ["dotnet", "TNO.Services.Reporting.dll"] diff --git a/services/net/reporting/Models/UserEmail.cs b/services/net/reporting/Models/UserEmail.cs index 0f5aaef1f1..4ef71acb0a 100644 --- a/services/net/reporting/Models/UserEmail.cs +++ b/services/net/reporting/Models/UserEmail.cs @@ -1,4 +1,6 @@ -namespace TNO.Services.Reporting.Models; +using MMI.SmtpEmail.Models; + +namespace TNO.Services.Reporting.Models; /// /// UserEmail class, provides a model to identify users to send emails to. @@ -19,12 +21,27 @@ public class UserEmail /// /// get/set - An array of CC to include with this email. /// - public UserEmail[] CC { get; set; } = Array.Empty(); + public UserEmail[] CC { get; set; } = []; /// /// get/set - An array of BCC to include with this email. /// - public UserEmail[] BCC { get; set; } = Array.Empty(); + public UserEmail[] BCC { get; set; } = []; + + /// + /// get/set - Display name of user. + /// + public string DisplayName { get; set; } = ""; + + /// + /// get/set - First name of user. + /// + public string FirstName { get; set; } = ""; + + /// + /// get/set - Last name of user. + /// + public string LastName { get; set; } = ""; #endregion #region Constructors @@ -39,8 +56,63 @@ public UserEmail(int userId, string to, UserEmail[]? cc = null, UserEmail[]? bcc { this.UserId = userId; this.To = to; - this.CC = cc ?? Array.Empty(); - this.BCC = bcc ?? Array.Empty(); + this.CC = cc ?? []; + this.BCC = bcc ?? []; + } + + /// + /// Creates a new instance of a UserEmail based on the specified user and email address to send to. + /// + /// + /// + /// + /// + public UserEmail(TNO.API.Areas.Services.Models.User.UserModel user, string to, UserEmail[]? cc = null, UserEmail[]? bcc = null) + { + this.UserId = user.Id; + this.DisplayName = user.DisplayName; + this.FirstName = user.FirstName; + this.LastName = user.LastName; + this.To = to; + this.CC = cc ?? []; + this.BCC = bcc ?? []; + } + + /// + /// Creates a new instance of a UserEmail based on the specified user and email address to send to. + /// + /// + /// + /// + /// + public UserEmail(API.Areas.Services.Models.Report.UserReportModel user, string to, UserEmail[]? cc = null, UserEmail[]? bcc = null) + { + this.UserId = user.UserId; + this.DisplayName = user.User?.DisplayName ?? ""; + this.FirstName = user.User?.FirstName ?? ""; + this.LastName = user.User?.LastName ?? ""; + this.To = to; + this.CC = cc ?? []; + this.BCC = bcc ?? []; + } + + + /// + /// Creates a new instance of a UserEmail based on the specified user and email address to send to. + /// + /// + /// + /// + /// + public UserEmail(API.Areas.Services.Models.AVOverview.UserModel user, string to, UserEmail[]? cc = null, UserEmail[]? bcc = null) + { + this.UserId = user.Id; + this.DisplayName = user.DisplayName; + this.FirstName = user.FirstName; + this.LastName = user.LastName; + this.To = to; + this.CC = cc ?? []; + this.BCC = bcc ?? []; } #endregion @@ -53,5 +125,14 @@ public bool Equals(UserEmail? other) public override bool Equals(object? obj) => Equals(obj as UserEmail); public override int GetHashCode() => this.To.GetHashCode(); + + /// + /// Converts this UserEmail to a MailMergeModel, populating the context with the user's information. This allows for the generation of personalized email content using templates that can access user-specific data such as first name, last name, and user ID through the context. The resulting MailMergeModel can then be used to generate email content with dynamic values based on the user's information. + /// + /// + public MailMergeModel ToMailMergeModel() + { + return new MailMergeModel([this.To]).PopulateContextFromUser(new TNO.API.Areas.Services.Models.User.UserModel() { Id = this.UserId, FirstName = this.FirstName, LastName = this.LastName }); + } #endregion } diff --git a/services/net/reporting/ReportingManager.cs b/services/net/reporting/ReportingManager.cs index 6cc730da17..0e4975bb75 100644 --- a/services/net/reporting/ReportingManager.cs +++ b/services/net/reporting/ReportingManager.cs @@ -1,11 +1,11 @@ +using System.Net.Mail; using System.Security.Claims; using System.Text.Json; using Confluent.Kafka; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; -using TNO.Ches.Models; +using MMI.SmtpEmail; +using MMI.SmtpEmail.Models; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.Elastic.Models; @@ -18,7 +18,6 @@ using TNO.TemplateEngine; using TNO.TemplateEngine.Converters; using TNO.TemplateEngine.Models; -using TNO.TemplateEngine.Models.Reports; namespace TNO.Services.Reporting; @@ -30,13 +29,13 @@ public class ReportingManager : ServiceManager #region Variables private CancellationTokenSource? _cancelToken; private Task? _consumer; - private readonly TaskStatus[] _notRunning = new TaskStatus[] { TaskStatus.Canceled, TaskStatus.Faulted, TaskStatus.RanToCompletion }; + private readonly TaskStatus[] _notRunning = [TaskStatus.Canceled, TaskStatus.Faulted, TaskStatus.RanToCompletion]; private int _retries = 0; private readonly JsonSerializerOptions _serializationOptions; private readonly ClaimsPrincipal _user; - static readonly ReportDistributionFormat[] LinkOnlyFormats = new[] { ReportDistributionFormat.LinkOnly, ReportDistributionFormat.ReceiveBoth }; - static readonly ReportDistributionFormat[] FullTextFormats = new[] { ReportDistributionFormat.FullText, ReportDistributionFormat.ReceiveBoth }; - static readonly ReportStatus[] _successfulEmailStatuses = new[] { ReportStatus.Accepted, ReportStatus.Completed }; + static readonly ReportDistributionFormat[] LinkOnlyFormats = [ReportDistributionFormat.LinkOnly, ReportDistributionFormat.ReceiveBoth]; + static readonly ReportDistributionFormat[] FullTextFormats = [ReportDistributionFormat.FullText, ReportDistributionFormat.ReceiveBoth]; + static readonly ReportStatus[] _successfulEmailStatuses = [ReportStatus.Accepted, ReportStatus.Completed]; #endregion #region Properties @@ -59,8 +58,8 @@ public class ReportingManager : ServiceManager /// /// /// - /// - /// + /// + /// /// /// /// @@ -69,12 +68,12 @@ public ReportingManager( IApiService api, ClaimsPrincipal user, IReportEngine reportEngine, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions serializationOptions, IOptions reportOptions, ILogger logger) - : base(api, chesService, chesOptions, reportOptions, logger) + : base(api, emailService, smtpOptions, reportOptions, logger) { _user = user; this.ReportEngine = reportEngine; @@ -234,8 +233,7 @@ private async Task AddRequestToQueue(Message mes if (this.Options.ResendOnFailure && failureCount < this.Options.ResendAttemptLimit) { message.Value.Resend = false; - var reportingException = ex as ReportingException; - if (reportingException != null) + if (ex is ReportingException reportingException) { message.Value.ReportInstanceId = reportingException.InstanceId; message.Value.Data = JsonDocument.Parse(JsonSerializer.Serialize(new { reportingException.Error, FailureCount = failureCount + 1 }, _serializationOptions)); @@ -344,6 +342,7 @@ private async Task ProcessReportAsync(ConsumeResult { await GenerateAndSendReportAsync(request, instance); + // If a report failed to send we need to try again. if (instance.Status == ReportStatus.Failed) await AddRequestToQueue(result.Message); } @@ -357,6 +356,7 @@ private async Task ProcessReportAsync(ConsumeResult { var instance = await GenerateAndSendReportAsync(request, report); + // If a report failed to send we need to try again. if (instance != null && instance.Status == ReportStatus.Failed) await AddRequestToQueue(result.Message); } @@ -395,16 +395,16 @@ private async Task ProcessReportAsync(ConsumeResult /// /// /// - private async Task> GetLinkedReportAsync(int reportId, int? ownerId) + private async Task> GetLinkedReportAsync(int reportId, int? ownerId) { var instance = await this.Api.GetCurrentReportInstanceAsync(reportId, ownerId); - if (instance == null) return new(); + if (instance == null) return []; var sections = instance.Report?.Sections.ToDictionary(section => section.Name, section => { - var content = instance.Content.Where(c => c.SectionName == section.Name && c.Content != null).Select(c => new ContentModel(c.Content!, c.SortOrder, c.SectionName, section.Settings.Label)); - return new ReportSectionModel(section, content); - }) ?? new(); + var content = instance.Content.Where(c => c.SectionName == section.Name && c.Content != null).Select(c => new TNO.TemplateEngine.Models.ContentModel(c.Content!, c.SortOrder, c.SectionName, section.Settings.Label)); + return new TNO.TemplateEngine.Models.Reports.ReportSectionModel(section, content); + }) ?? []; return sections; } @@ -417,10 +417,10 @@ private async Task> GetLinkedReportAsync( /// /// /// - private async Task<(API.Areas.Services.Models.ReportInstance.ReportInstanceModel?, Dictionary)> PopulateReportInstance(ReportRequestModel request, API.Areas.Services.Models.Report.ReportModel report) + private async Task<(API.Areas.Services.Models.ReportInstance.ReportInstanceModel?, Dictionary)> PopulateReportInstance(ReportRequestModel request, API.Areas.Services.Models.Report.ReportModel report) { - var sections = report.Sections.OrderBy(s => s.SortOrder).Select(s => new ReportSectionModel(s)); - var sectionContent = new Dictionary(); + var sections = report.Sections.OrderBy(s => s.SortOrder).Select(s => new TNO.TemplateEngine.Models.Reports.ReportSectionModel(s)); + var sectionContent = new Dictionary(); API.Areas.Services.Models.Report.ReportInstanceModel? reportInstanceModel = null; try @@ -435,7 +435,7 @@ private async Task> GetLinkedReportAsync( try { // If we don't clear the prior report we must append new content to it. - var currentSectionContent = new Dictionary(); + var currentSectionContent = new Dictionary(); if (reportInstanceModel != null && !reportInstanceModel.SentOn.HasValue && !report.Settings.Content.ClearOnStartNewReport) { // Extract the section content from the current instance. @@ -461,7 +461,7 @@ private async Task> GetLinkedReportAsync( IEnumerable distinctContent; if (report.Settings.Content.CopyPriorInstance) distinctContent = ( - currentSectionContent.TryGetValue(section.Name, out ReportSectionModel? currentSection) + currentSectionContent.TryGetValue(section.Name, out TNO.TemplateEngine.Models.Reports.ReportSectionModel? currentSection) ? currentSection?.Content.ToArray().AppendRange(newContent).DistinctBy(c => c.Id) : newContent ) ?? Array.Empty(); @@ -501,7 +501,7 @@ private async Task> GetLinkedReportAsync( // filter out Content with no valid ContentId var reportInstanceContents = sectionContent.SelectMany(s => s.Value.Content.Where(c => c.Id > 0).Select(c => new ReportInstanceContent(0, c.Id, s.Key, c.SortOrder)).ToArray()).ToArray(); var reportInstanceContentsBad = sectionContent.SelectMany(s => s.Value.Content.Where(c => c.Id == 0).Select(c => new ReportInstanceContent(0, c.Id, s.Key, c.SortOrder)).ToArray()).ToArray(); - if (reportInstanceContentsBad.Any()) + if (reportInstanceContentsBad.Length != 0) { this.Logger.LogWarning("Report [{name}] {generateInstance} has malformed content. It will be generated, but may not match expectations.", report.Name, request.GenerateInstance); foreach (var section in sectionContent) @@ -615,7 +615,7 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar var resending = IsResend(request, instance); var retry = IsRetry(request, instance); var report = instance.Report ?? throw new ArgumentException("Report instance must include the report model."); - var sections = report.Sections.OrderBy(s => s.SortOrder).Select(s => new ReportSectionModel(s)); + var sections = report.Sections.OrderBy(s => s.SortOrder).Select(s => new TNO.TemplateEngine.Models.Reports.ReportSectionModel(s)); var searchResults = !resending && !retry ? await this.Api.GetContentForReportInstanceIdAsync(instance.Id) : Array.Empty(); var sectionContent = sections.ToDictionary(s => s.Name, section => @@ -648,7 +648,7 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar private async Task CheckForSubscribersAsync(API.Areas.Services.Models.Report.ReportModel report) { var subscribers = report.Subscribers.Where(s => s.IsSubscribed && s.User != null && s.User.IsVacationMode() != true).ToArray(); - if (subscribers.Any()) + if (subscribers.Length != 0) { return true; } @@ -682,7 +682,7 @@ private async Task SendReportAsync( ReportRequestModel request, API.Areas.Services.Models.Report.ReportModel report, API.Areas.Services.Models.ReportInstance.ReportInstanceModel? instance, - Dictionary sectionContent) + Dictionary sectionContent) { try { @@ -738,7 +738,7 @@ private async Task SendReportAsync( ReportRequestModel request, API.Areas.Services.Models.Report.ReportModel report, API.Areas.Services.Models.ReportInstance.ReportInstanceModel? instance, - Dictionary sectionContent) + Dictionary sectionContent) { try { @@ -788,7 +788,7 @@ private async Task SendEmailsAsync( } // TODO: This implementation can result in one set of emails being successful and the second failing. - var responseModel = new ReportEmailResponseModel(); + var responseModel = new API.Areas.Services.Models.Report.ReportEmailResponseModel(); try { var userReportInstances = new List(); @@ -804,12 +804,19 @@ private async Task SendEmailsAsync( { instance.Subject = subject; instance.Body = fullBody; + instance.LinkOnlyBody = linkBody; if (instance.PublishedOn == null) instance.PublishedOn = DateTime.UtcNow; if (request.SendToSubscribers) instance.SentOn = DateTime.UtcNow; // We track when it was sent, even if it failed. } - if (!report.Settings.DoNotSendEmail && String.IsNullOrEmpty(request.To) && (linkEmails[EmailSentTo.To].Any() || linkEmails[EmailSentTo.CC].Any() || linkEmails[EmailSentTo.BCC].Any())) + if (!report.Settings.DoNotSendEmail + && String.IsNullOrEmpty(request.To) + && ( + linkEmails[EmailSentTo.To].Count != 0 + || linkEmails[EmailSentTo.CC].Count != 0 + || linkEmails[EmailSentTo.BCC].Count != 0 + )) { // Send the email. var (linkOnlyStatus, responseLinkOnly) = await SendEmailAsync( @@ -830,7 +837,12 @@ private async Task SendEmailsAsync( } } - if (!report.Settings.DoNotSendEmail && (fullEmails[EmailSentTo.To].Any() || fullEmails[EmailSentTo.CC].Any() || fullEmails[EmailSentTo.BCC].Any() || !String.IsNullOrEmpty(request.To))) + if (!report.Settings.DoNotSendEmail + && (fullEmails[EmailSentTo.To].Count != 0 + || fullEmails[EmailSentTo.CC].Count != 0 + || fullEmails[EmailSentTo.BCC].Count != 0 + || !String.IsNullOrEmpty(request.To) + )) { // Send the email. var (fullTextStatus, responseFullText) = await SendEmailAsync( @@ -852,8 +864,13 @@ private async Task SendEmailsAsync( } // If the report wasn't sent out due to having no subscribers, update the status based on the current status. - if (instance != null && !linkEmails[EmailSentTo.To].Any() && !linkEmails[EmailSentTo.CC].Any() && !linkEmails[EmailSentTo.BCC].Any() && - !fullEmails[EmailSentTo.To].Any() && !fullEmails[EmailSentTo.CC].Any() && !fullEmails[EmailSentTo.BCC].Any()) + if (instance != null + && linkEmails[EmailSentTo.To].Count == 0 + && linkEmails[EmailSentTo.CC].Count == 0 + && linkEmails[EmailSentTo.BCC].Count == 0 + && fullEmails[EmailSentTo.To].Count == 0 + && fullEmails[EmailSentTo.CC].Count == 0 + && fullEmails[EmailSentTo.BCC].Count == 0) { // A report without subscribers is cancelled (it should never have gotten this far). // All other statuses are considered complete, as there is no more work to do. @@ -867,21 +884,6 @@ private async Task SendEmailsAsync( instance.Status = ReportStatus.Cancelled; } } - catch (ChesException ex) - { - this.Logger.LogError(ex, "Failed to email report. ReportId:{reportId}, InstanceId:{instanceId}", report.Id, instance?.Id); - if (responseModel.LinkOnlyFormatResponse != null) - responseModel.LinkOnlyFormatResponse = JsonDocument.Parse(JsonSerializer.Serialize(ex.Data["error"], _serializationOptions)); - else - responseModel.FullTextFormatResponse = JsonDocument.Parse(JsonSerializer.Serialize(ex.Data["error"], _serializationOptions)); - - if (instance != null) - { - instance.Status = ReportStatus.Failed; - instance.Response = JsonDocument.Parse(JsonSerializer.Serialize(responseModel, _serializationOptions)); - } - throw new ReportingException(report.Id, instance?.Id, ReportingErrors.FailedToEmail, $"Failed to email report. ReportId:{report.Id}, InstanceId:{instance?.Id}", ex); - } catch (Exception ex) { this.Logger.LogError(ex, "Failed to email report. ReportId:{reportId}, InstanceId:{instanceId}", report.Id, instance?.Id); @@ -917,6 +919,7 @@ private async Task UpdateReportInstanceAsync(API.Areas.Services.Models.ReportIns { latestInstanceModel.Subject = instance.Subject; latestInstanceModel.Body = instance.Body; + latestInstanceModel.LinkOnlyBody = instance.LinkOnlyBody; latestInstanceModel.SentOn = instance.SentOn; latestInstanceModel.Status = instance.Status; latestInstanceModel.Response = instance.Response; @@ -945,53 +948,54 @@ private async Task UpdateReportInstanceAsync(API.Areas.Services.Models.ReportIns /// /// /// - private Func, ReportStatus, JsonDocument, Task> UpdateUserReportInstances( + private Func UpdateUserReportInstances( API.Areas.Services.Models.Report.ReportModel report, API.Areas.Services.Models.ReportInstance.ReportInstanceModel? instance, List userReportInstances, bool fullBodyReport) { - return async (IEnumerable users, ReportStatus status, JsonDocument response) => + return async (userId, status, response) => { - if (instance?.Id > 0) + if (instance?.Id > 0 && userId.HasValue && userId.Value > 0) { var updateUserReportInstances = new List(); - foreach (var user in users) + var userReportInstance = userReportInstances.FirstOrDefault(uri => uri.UserId == userId.Value); + + var responseJson = response.ToJsonDocument(_serializationOptions); + + if (userReportInstance == null) { - var userReportInstance = userReportInstances.FirstOrDefault(uri => uri.UserId == user.UserId); - if (userReportInstance == null) + userReportInstance = new API.Areas.Services.Models.ReportInstance.UserReportInstanceModel(userId.Value, instance.Id) { - userReportInstance = new API.Areas.Services.Models.ReportInstance.UserReportInstanceModel(user.UserId, instance.Id) - { - LinkStatus = !fullBodyReport ? status : ReportStatus.Pending, - LinkSentOn = !fullBodyReport ? DateTime.UtcNow : null, - LinkResponse = !fullBodyReport ? response : JsonDocument.Parse("{}"), - TextStatus = fullBodyReport ? status : ReportStatus.Pending, - TextSentOn = fullBodyReport ? DateTime.UtcNow : null, - TextResponse = fullBodyReport ? response : JsonDocument.Parse("{}"), - }; - userReportInstances.Add(userReportInstance); + LinkStatus = !fullBodyReport ? status : ReportStatus.Pending, + LinkSentOn = !fullBodyReport ? DateTime.UtcNow : null, + LinkResponse = !fullBodyReport ? responseJson : JsonDocument.Parse("{}"), + TextStatus = fullBodyReport ? status : ReportStatus.Pending, + TextSentOn = fullBodyReport ? DateTime.UtcNow : null, + TextResponse = fullBodyReport ? responseJson : JsonDocument.Parse("{}"), + }; + userReportInstances.Add(userReportInstance); + } + else + { + if (!fullBodyReport) + { + userReportInstance.LinkStatus = status; + userReportInstance.LinkSentOn = DateTime.UtcNow; + userReportInstance.LinkResponse = responseJson; } else { - if (!fullBodyReport) - { - userReportInstance.LinkStatus = status; - userReportInstance.LinkSentOn = DateTime.UtcNow; - userReportInstance.LinkResponse = response; - } - else - { - userReportInstance.TextStatus = status; - userReportInstance.TextSentOn = DateTime.UtcNow; - userReportInstance.TextResponse = response; - } + userReportInstance.TextStatus = status; + userReportInstance.TextSentOn = DateTime.UtcNow; + userReportInstance.TextResponse = responseJson; } - updateUserReportInstances.Add(userReportInstance); } - var toEmails = String.Join(",", users.Select(u => u.To)); - var ccEmails = String.Join(",", users.SelectMany(u => u.CC.Select(v => v.To))); - var bccEmails = String.Join(",", users.SelectMany(u => u.BCC.Select(v => v.To))); + updateUserReportInstances.Add(userReportInstance); + + var toEmails = String.Join(",", response.To); + var ccEmails = String.Join(",", response.CC ?? []); + var bccEmails = String.Join(",", response.Bcc ?? []); this.Logger.LogDebug("Saving email responses. reportId:{reportId}, InstanceId:{instanceId}, Status:{status}, To:{to}, CC:{cc}, BCC:{bcc}", report.Id, instance.Id, status, toEmails, ccEmails, bccEmails); await this.Api.AddOrUpdateUserReportInstancesAsync(updateUserReportInstances); } @@ -1018,6 +1022,9 @@ private async Task UpdateReportInstanceAsync(API.Areas.Services.Models.AVOvervie var latestInstanceModel = await this.Api.GetAVOverviewInstanceAsync(instance.Id) ?? throw new InvalidOperationException("Report instance failed to be returned by API"); if (latestInstanceModel.Version != instance.Version) { + latestInstanceModel.Subject = instance.Subject; + latestInstanceModel.Body = instance.Body; + latestInstanceModel.Status = instance.Status; latestInstanceModel.Response = instance.Response; latestInstanceModel.PublishedOn = instance.PublishedOn; instance = await this.Api.UpdateAVOverviewInstanceAsync(latestInstanceModel) ?? throw new InvalidOperationException("Report instance failed to be returned by API"); @@ -1073,8 +1080,8 @@ private async Task UpdateReportInstanceAsync(API.Areas.Services.Models.AVOvervie _successfulEmailStatuses.Contains(uri.TextStatus) && (instance == null || uri.InstanceId == instance.Id)))).ToArray(); - var linkEmails = await GetEmailAddressesAsync(request, linkOnlyFormatSubscribers, userReportInstances); - var fullEmails = await GetEmailAddressesAsync(request, fullTextFormatSubscribers, userReportInstances); + var linkEmails = await GetEmailAddressesAsync(request, linkOnlyFormatSubscribers, userReportInstances, false); + var fullEmails = await GetEmailAddressesAsync(request, fullTextFormatSubscribers, userReportInstances, true); return (linkEmails, fullEmails); } @@ -1086,12 +1093,17 @@ private async Task UpdateReportInstanceAsync(API.Areas.Services.Models.AVOvervie /// /// /// + /// /// private async Task>> GetEmailAddressesAsync( ReportRequestModel request, API.Areas.Services.Models.Report.UserReportModel[] subscribers, - IEnumerable userReportInstances) + IEnumerable userReportInstances, + bool fullTextReport + ) { + // If this is a retry, we want to target users who had failures. Otherwise, we target users who haven't received the email. + var failureCount = request.Data.GetElementValue(".failureCount") ?? 0; var emails = new Dictionary>() { { EmailSentTo.To, new List() }, @@ -1103,11 +1115,18 @@ private async Task>> GetEmailAddressesAs { if (user.User == null) throw new InvalidOperationException("Report subscriber is missing user information"); - // Only include users who have not received an email yet. - var (a, b, c) = await GetEmailAddressesAsync(user.UserId, user.User.GetEmail(), user.User.AccountType, user.SendTo); - emails[EmailSentTo.To].AddRange(a.Where(s => request.Resend || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.LinkStatus)))); - emails[EmailSentTo.CC].AddRange(b.Where(s => request.Resend || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.LinkStatus)))); - emails[EmailSentTo.BCC].AddRange(c.Where(s => request.Resend || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.LinkStatus)))); + // Only include users who have not received an email yet, unless it is a resend without a failure. + // We have to assume if this is retry and there are failures that all users who already received the email should not be included. + var (a, b, c) = await GetEmailAddressesAsync(user, user.User.GetEmail(), user.User.AccountType, user.SendTo); + emails[EmailSentTo.To].AddRange(a.Where(s => (request.Resend && failureCount == 0) + || !userReportInstances.Any(uri => uri.UserId == s.UserId + && _successfulEmailStatuses.Contains(fullTextReport ? uri.TextStatus : uri.LinkStatus)))); + emails[EmailSentTo.CC].AddRange(b.Where(s => (request.Resend && failureCount == 0) + || !userReportInstances.Any(uri => uri.UserId == s.UserId + && _successfulEmailStatuses.Contains(fullTextReport ? uri.TextStatus : uri.LinkStatus)))); + emails[EmailSentTo.BCC].AddRange(c.Where(s => (request.Resend && failureCount == 0) + || !userReportInstances.Any(uri => uri.UserId == s.UserId + && _successfulEmailStatuses.Contains(fullTextReport ? uri.TextStatus : uri.LinkStatus)))); } return emails; @@ -1116,13 +1135,77 @@ private async Task>> GetEmailAddressesAs /// /// Return a user email, or return all user email addresses in a distribution list. /// - /// + /// + /// + /// + /// + /// + /// + private async Task<(IEnumerable to, IEnumerable cc, IEnumerable bcc)> GetEmailAddressesAsync( + API.Areas.Services.Models.Report.UserReportModel user, + string email, + UserAccountType accountType, + EmailSentTo sendTo) + { + var to = new List(); + var cc = new List(); + var bcc = new List(); + // If the user is a distribution list, fetch the rest of the users. + if (accountType == UserAccountType.Distribution) + { + var users = await this.Api.GetDistributionListAsync(user.UserId); + var filteredUsers = users.Where(u => !u.IsVacationMode() && u.GetEmail().IsValidEmail()).ToList(); + var emails = filteredUsers.Select(u => new UserEmail(u, u.GetEmail())); + switch (sendTo) + { + case EmailSentTo.To: + to.AddRange(emails); + break; + case EmailSentTo.CC: + cc.AddRange(emails); + break; + case EmailSentTo.BCC: + bcc.AddRange(emails); + break; + } + } + else if (email.IsValidEmail()) + { + switch (sendTo) + { + case EmailSentTo.To: + to.Add(new UserEmail(user, email)); + break; + case EmailSentTo.CC: + cc.Add(new UserEmail(user, email)); + break; + case EmailSentTo.BCC: + bcc.Add(new UserEmail(user, email)); + break; + } + } + else + { + this.Logger.LogError("Email address is invalid {email}", email); + } + + return (to.Distinct(), cc.Distinct(), bcc.Distinct()); + } + + /// + /// Return a user email, or return all user email addresses in a distribution list. + /// + /// /// /// /// /// /// - private async Task<(IEnumerable to, IEnumerable cc, IEnumerable bcc)> GetEmailAddressesAsync(int userId, string email, UserAccountType accountType, EmailSentTo sendTo) + private async Task<(IEnumerable to, IEnumerable cc, IEnumerable bcc)> GetEmailAddressesAsync( + API.Areas.Services.Models.AVOverview.UserModel user, + string email, + UserAccountType accountType, + EmailSentTo sendTo) { var to = new List(); var cc = new List(); @@ -1130,9 +1213,9 @@ private async Task>> GetEmailAddressesAs // If the user is a distribution list, fetch the rest of the users. if (accountType == UserAccountType.Distribution) { - var users = await this.Api.GetDistributionListAsync(userId); + var users = await this.Api.GetDistributionListAsync(user.Id); var filteredUsers = users.Where(u => !u.IsVacationMode() && u.GetEmail().IsValidEmail()).ToList(); - var emails = filteredUsers.Select(u => new UserEmail(u.Id, u.GetEmail())); + var emails = filteredUsers.Select(u => new UserEmail(u, u.GetEmail())); switch (sendTo) { case EmailSentTo.To: @@ -1151,13 +1234,13 @@ private async Task>> GetEmailAddressesAs switch (sendTo) { case EmailSentTo.To: - to.Add(new UserEmail(userId, email)); + to.Add(new UserEmail(user, email)); break; case EmailSentTo.CC: - cc.Add(new UserEmail(userId, email)); + cc.Add(new UserEmail(user, email)); break; case EmailSentTo.BCC: - bcc.Add(new UserEmail(userId, email)); + bcc.Add(new UserEmail(user, email)); break; } } @@ -1172,7 +1255,7 @@ private async Task>> GetEmailAddressesAs /// /// Send out an email for the specified report. /// Generate a report instance for this email. - /// Send an email merge to CHES. + /// Send an email merge to SMTP. /// This will send out a separate email to each context provided. /// /// @@ -1181,7 +1264,7 @@ private async Task>> GetEmailAddressesAs /// private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Areas.Services.Models.AVOverview.AVOverviewInstanceModel instance) { - var model = new AVOverviewInstanceModel(instance); + var model = new TNO.TemplateEngine.Models.Reports.AVOverviewInstanceModel(instance); var template = instance.Template ?? throw new InvalidOperationException($"Report template was not included in model."); // No need to send an email if there are no subscribers. @@ -1194,6 +1277,7 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar } var subscribers = instance.Subscribers.Where(s => s.IsSubscribed).ToArray(); + var failureCount = request.Data.GetElementValue(".failureCount") ?? 0; var to = new List(); var cc = new List(); @@ -1201,29 +1285,34 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar foreach (var user in subscribers) { // Determine which users have already received the email. - var (a, b, c) = await GetEmailAddressesAsync(user.Id, user.GetEmail(), user.AccountType, user.SendTo); - to.AddRange(a.Where(s => request.Resend || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.Status)))); - cc.AddRange(b.Where(s => request.Resend || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.Status)))); - bcc.AddRange(c.Where(s => request.Resend || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.Status)))); + var (a, b, c) = await GetEmailAddressesAsync(user, user.GetEmail(), user.AccountType, user.SendTo); + to.AddRange(a.Where(s => (request.Resend && failureCount == 0) || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.Status)))); + cc.AddRange(b.Where(s => (request.Resend && failureCount == 0) || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.Status)))); + bcc.AddRange(c.Where(s => (request.Resend && failureCount == 0) || !userReportInstances.Any(uri => uri.UserId == s.UserId && _successfulEmailStatuses.Contains(uri.Status)))); } - if (to.Any() || !String.IsNullOrEmpty(request.To)) + if (to.Count != 0 || !String.IsNullOrEmpty(request.To)) { var subject = await this.ReportEngine.GenerateReportSubjectAsync(template, model, false); var body = await this.ReportEngine.GenerateReportBodyAsync(template, model, false); + instance.Subject = subject; + instance.Body = body; // Send the email. - var (status, response) = await SendEmailAsync(request, to, cc, bcc, subject, body, $"{instance.TemplateType}-{instance.Id}", async (users, status, response) => - { - if (instance.Id > 0) + var (status, responses) = await SendEmailAsync(request, to, cc, bcc, subject, body, $"{instance.TemplateType}-{instance.Id}", + async (userId, status, response) => { - var userReportInstances = users.Select((user) => new API.Areas.Services.Models.AVOverview.UserAVOverviewInstanceModel(user.UserId, instance.Id, DateTime.UtcNow, status, response)); - await this.Api.AddOrUpdateUserAVOverviewInstancesAsync(userReportInstances); - } - }); + if (instance.Id > 0 && userId.HasValue && userId > 0) + { + var details = JsonDocument.Parse(JsonSerializer.Serialize(response, _serializationOptions)); + var userReportInstances = new API.Areas.Services.Models.AVOverview.UserAVOverviewInstanceModel(userId.Value, instance.Id, DateTime.UtcNow, status, details); + await this.Api.AddOrUpdateUserAVOverviewInstancesAsync([userReportInstances]); + } + }); // Update the report instance with the email response. - instance.Response = JsonDocument.Parse(JsonSerializer.Serialize(response, _serializationOptions)); + instance.Status = status; + instance.Response = JsonDocument.Parse(JsonSerializer.Serialize(responses, _serializationOptions)); } instance.IsPublished = true; @@ -1232,7 +1321,7 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar } /// - /// Send an email to CHES. + /// Send an email to SMTP. /// Also update user report instances to identify who has received an email. /// Handles sending email as a mail merge, or individually. /// @@ -1244,7 +1333,7 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar /// /// /// - private async Task<(ReportStatus status, EmailResponseModel[] response)> SendEmailAsync( + private async Task<(ReportStatus status, MailResponseModel[] responses)> SendEmailAsync( ReportRequestModel request, IEnumerable to, IEnumerable cc, @@ -1252,19 +1341,16 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar string subject, string body, string tag, - Func, ReportStatus, JsonDocument, Task> updateCallbackAsync) + Func updateCallbackAsync) { - await HandleChesEmailOverrideAsync(request.RequestorId); + await HandleEmailOverrideAsync(request.RequestorId); - var contexts = new List<(UserEmail User, EmailContextModel Context)>(); + var contexts = new List(); if (!String.IsNullOrWhiteSpace(request.To) && request.To.IsValidEmail()) { // Add a context for the requested list of users. var toAddresses = request.To.Split(",").Select(v => v.Trim()).ToArray(); - contexts.Add((new UserEmail(0, request.To), new EmailContextModel(toAddresses, new Dictionary(), DateTime.Now) - { - Tag = tag, - })); + contexts.Add(new MailMergeModel(toAddresses)); } else { @@ -1275,11 +1361,8 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar // Do not send multiple emails to the requestor if there is also a CC/BCC list. var toAddresses = String.IsNullOrWhiteSpace(requestorEmail) ? to.Distinct() : to.Distinct().Where(v => v.To != requestorEmail); var toContexts = toAddresses - .Where(v => v.To.IsValidEmail()) - .Select(v => (v, new EmailContextModel(new[] { v.To }, new Dictionary(), DateTime.Now) - { - Tag = tag, - })).ToArray(); + .Where(ue => ue.To.IsValidEmail()) + .Select(ue => ue.ToMailMergeModel()).ToArray(); contexts.AddRange(toContexts); // If there are BCC and CC options then they must be sent with an email that is sent to the requestor. @@ -1287,162 +1370,137 @@ private async Task GenerateAndSendReportAsync(ReportRequestModel request, API.Ar { var cc1 = cc.Distinct().Where(a => a.To.IsValidEmail()).ToArray(); var bcc1 = bcc.Distinct().Where(a => a.To.IsValidEmail()).ToArray(); - contexts.Add((new UserEmail(request.RequestorId!.Value, requestorEmail, cc1, bcc1), new EmailContextModel(new[] { requestorEmail }, new Dictionary(), DateTime.Now) + contexts.Add(new MailMergeModel([requestorEmail]) { - Cc = cc1.Select(v => v.To), - Bcc = bcc1.Select(v => v.To), - Tag = tag, - })); + CC = [.. cc1.Select(v => v.To)], + Bcc = [.. bcc1.Select(v => v.To)], + }.PopulateContextFromUser(requestor)); } } - if (!contexts.Any()) return (ReportStatus.Cancelled, Array.Empty()); + if (contexts.Count == 0) return (ReportStatus.Cancelled, []); if (this.Options.UseMailMerge) { - var merge = new EmailMergeModel(request.From ?? this.Options.DefaultFrom, contexts.Select(c => c.Context), subject, body) - { - // TODO: Extract values from report settings. - Encoding = EmailEncodings.Utf8, - BodyType = EmailBodyTypes.Html, - Priority = EmailPriorities.Normal, - }; - - var failureCount = 0; - while (true) + var (success, responses) = await this.EmailService.TrySendAsync(contexts, subject, body); + + if (success) + this.Logger.LogInformation("Report sent. ReportId:{report}, InstanceId:{instance}", request.ReportId, request.ReportInstanceId); + else + this.Logger.LogError("Report sent with failures. ReportId:{report}, InstanceId:{instance}", request.ReportId, request.ReportInstanceId); + + if (request.ReportInstanceId.HasValue) { - try + for (var i = 0; i < contexts.Count; i++) { - var response = await this.Ches.SendEmailAsync(merge); - this.Logger.LogInformation("Report sent to CHES. ReportId:{report}, InstanceId:{instance}", request.ReportId, request.ReportInstanceId); - - if (request.ReportInstanceId.HasValue) + var context = contexts[i]; + if (responses.Length > i) { - var document = JsonDocument.Parse(JsonSerializer.Serialize(response, _serializationOptions)); - await updateCallbackAsync(contexts.Select(c => c.User), ReportStatus.Accepted, document); + // Save the status of each email sent. + var response = responses[i]; + var userId = context.Context.TryGetValue("id", out var userIdObj) && int.TryParse(userIdObj?.ToString(), out var userIdInt) ? userIdInt : 0; + await updateCallbackAsync(userId, success ? ReportStatus.Completed : ReportStatus.Failed, response); } - - return (ReportStatus.Accepted, new[] { response }); - } - catch (ChesException ex) - { - // Retry X times before giving up. - failureCount++; - if (failureCount >= this.Options.RetryLimit) - throw; - else - this.Logger.LogError(ex, "Failed to send report to CHES. ReportId:{report}, InstanceId:{instance}", request.ReportId, request.ReportInstanceId); - - // Wait before trying again. - await Task.Delay(this.Options.DefaultDelayMS); } } + + return (success ? ReportStatus.Completed : ReportStatus.Failed, responses); } else { - var responses = new List(); + var responses = new List(); var failureCount = 0; // Keep track of each failed subscriber email. - foreach (var (user, context) in contexts) + foreach (var context in contexts) { - failureCount++; var retryCount = 0; - var allUsers = new[] { user }.Concat(user.CC.Concat(user.BCC)).Distinct(); - var allEmails = String.Join(", ", allUsers.Select(u => u.To)); while (true) { try { - var email = new EmailModel(request.From ?? this.Options.DefaultFrom, context.To.ToArray(), subject, body) - { - Cc = context.Cc.ToArray(), - Bcc = context.Bcc.ToArray(), - // TODO: Extract values from report settings. - Encoding = EmailEncodings.Utf8, - BodyType = EmailBodyTypes.Html, - Priority = EmailPriorities.Normal, - }; - - var response = await this.Ches.SendEmailAsync(email); + var email = this.EmailService.CreateMailMessage(subject, body, [.. context.To], context.CC?.ToArray(), context.Bcc?.ToArray(), null, true); + var response = await this.EmailService.SendAsync(email); responses.Add(response); + var userId = context.Context.TryGetValue("id", out var userIdObj) && int.TryParse(userIdObj?.ToString(), out var userIdInt) ? userIdInt : 0; - failureCount--; // Success, remove from failure count. - if (user.UserId != 0) - { - // Save the status of each email sent. - var document = JsonDocument.Parse(JsonSerializer.Serialize(response, _serializationOptions)); - await updateCallbackAsync(allUsers, ReportStatus.Accepted, document); - } + // Success, reset failure count. + failureCount = 0; + + // Save the status of each email sent. + await updateCallbackAsync(userId, ReportStatus.Completed, response); break; } - catch (ChesException ex) + catch (SmtpException ex) { - if (user.UserId != 0) - { - // Save the status of each email sent. - var document = JsonDocument.Parse(JsonSerializer.Serialize(ex.Data["error"], _serializationOptions)); - await updateCallbackAsync(allUsers, ReportStatus.Failed, document); - } + this.Logger.LogError(ex, "Failed to send report. ReportId:{report}, InstanceId:{instance}, Emails:{emails}", request.ReportId, request.ReportInstanceId, String.Join(',', context.To)); retryCount++; if (retryCount >= this.Options.RetryLimit) { - // Escape from retry loop. - if (!this.Options.SendToAllSubscribersBeforeFailing) - throw; + // Only increment failure count after we've exhausted all retries for this email. This allows us to attempt to send to all subscribers even if some are failing, and only fail at the end if there were any failures. + failureCount++; + var response = new MailResponseModel(context, ex); + responses.Add(response); + var userId = context.Context.TryGetValue("id", out var userIdObj) && int.TryParse(userIdObj?.ToString(), out var userIdInt) ? userIdInt : 0; + + // Save the status of each email sent. + await updateCallbackAsync(userId, ReportStatus.Failed, response); break; } - else - this.Logger.LogError(ex, "Failed to send report to CHES. ReportId:{report}, InstanceId:{instance}, Emails:{emails}", request.ReportId, request.ReportInstanceId, allEmails); // Wait before trying again. await Task.Delay(this.Options.RetryDelayMS); } catch (Exception ex) { - if (user.UserId != 0) - { - // Save the status of each email sent. - var document = JsonDocument.Parse(JsonSerializer.Serialize(new { Error = ex.GetAllMessages() }, _serializationOptions)); - await updateCallbackAsync(allUsers, ReportStatus.Failed, document); - } - + this.Logger.LogError(ex, "Failed to send report. ReportId:{report}, InstanceId:{instance}, Emails:{emails}", request.ReportId, request.ReportInstanceId, String.Join(',', context.To)); retryCount++; if (retryCount >= this.Options.RetryLimit) { - // Escape from retry loop. - if (!this.Options.SendToAllSubscribersBeforeFailing) - throw; + // Only increment failure count after we've exhausted all retries for this email. This allows us to attempt to send to all subscribers even if some are failing, and only fail at the end if there were any failures. + failureCount++; + var response = new MailResponseModel(context, ex); + responses.Add(response); + var userId = context.Context.TryGetValue("id", out var userIdObj) && int.TryParse(userIdObj?.ToString(), out var userIdInt) ? userIdInt : 0; + + // Save the status of each email sent. + await updateCallbackAsync(userId, ReportStatus.Failed, response); break; } - else - this.Logger.LogError(ex, "Failed to send report to CHES. ReportId:{report}, InstanceId:{instance}, Emails:{emails}", request.ReportId, request.ReportInstanceId, allEmails); // Wait before trying again. await Task.Delay(this.Options.RetryDelayMS); } } + + // Exit loop, stop sending emails if there have been failures and we're not configured to attempt to send to all subscribers before failing. + if (failureCount > 0 && !this.Options.SendToAllSubscribersBeforeFailing) + break; } - this.Logger.LogInformation("Report sent to CHES. ReportId:{report}, InstanceId:{instance}", request.ReportId, request.ReportInstanceId); - return (failureCount > 0 ? ReportStatus.Failed : ReportStatus.Accepted, responses.ToArray()); + if (FailureCount > 0) + this.Logger.LogWarning("Report sent with failures. ReportId:{report}, InstanceId:{instance}, Failures:{failures}", request.ReportId, request.ReportInstanceId, failureCount); + else + this.Logger.LogInformation("Report sent. ReportId:{report}, InstanceId:{instance}", request.ReportId, request.ReportInstanceId); + + return (failureCount > 0 ? ReportStatus.Failed : ReportStatus.Completed, responses.ToArray()); } } /// - /// If CHES has been configured to send emails to the user we need to provide an appropriate user. + /// If SMTP has been configured to send emails to the user we need to provide an appropriate user. /// /// /// - private async Task HandleChesEmailOverrideAsync(int? requestorId) + private async Task HandleEmailOverrideAsync(int? requestorId) { // The requestor becomes the current user. - var email = this.ChesOptions.OverrideTo ?? ""; + var email = this.SmtpOptions.OverrideTo ?? ""; if (requestorId.HasValue) { var user = await this.Api.GetUserAsync(requestorId.Value); if (user != null) email = user.GetEmail(); } - var identity = _user.Identity as ClaimsIdentity ?? throw new ConfigurationException("CHES requires an active ClaimsPrincipal"); + var identity = _user.Identity as ClaimsIdentity ?? throw new ConfigurationException("Requires an active ClaimsPrincipal"); identity.RemoveClaim(_user.FindFirst(ClaimTypes.Email)); identity.AddClaim(new Claim(ClaimTypes.Email, email)); } diff --git a/services/net/reporting/appsettings.Development.json b/services/net/reporting/appsettings.Development.json index 552ae65c9e..59767c14af 100644 --- a/services/net/reporting/appsettings.Development.json +++ b/services/net/reporting/appsettings.Development.json @@ -8,20 +8,13 @@ } }, "Service": { - "MaxFailLimit": 5, - "DefaultFrom": "Media Monitoring Insights " + "MaxFailLimit": 5 }, "Reporting": { "SubscriberAppUrl": "https://dev.mmi.gov.bc.ca", "ViewContentUrl": "https://dev.mmi.gov.bc.ca/view/", "RequestTranscriptUrl": "https://dev.mmi.gov.bc.ca/api/subscriber/work/orders/transcribe/" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/reporting/appsettings.Staging.json b/services/net/reporting/appsettings.Staging.json index e12f89efaf..3c78cbddcf 100644 --- a/services/net/reporting/appsettings.Staging.json +++ b/services/net/reporting/appsettings.Staging.json @@ -8,20 +8,13 @@ }, "Service": { "MaxFailLimit": 5, - "ApiUrl": "http://api:8080", - "DefaultFrom": "Media Monitoring Insights " + "ApiUrl": "http://api:8080" }, "Reporting": { "SubscriberAppUrl": "https://test.mmi.gov.bc.ca", "ViewContentUrl": "https://test.mmi.gov.bc.ca/view/", "RequestTranscriptUrl": "https://test.mmi.gov.bc.ca/api/subscriber/work/orders/transcribe/" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/reporting/appsettings.json b/services/net/reporting/appsettings.json index ad491ca413..95cf00b503 100644 --- a/services/net/reporting/appsettings.json +++ b/services/net/reporting/appsettings.json @@ -38,7 +38,6 @@ "TimeZone": "Pacific Standard Time", "Topics": "reporting", "SendEmailOnFailure": true, - "DefaultFrom": "Media Monitoring Insights ", "ImageVolumePath": "/data" }, "Reporting": { @@ -46,17 +45,17 @@ "ViewContentUrl": "https://mmi.gov.bc.ca/view/", "RequestTranscriptUrl": "https://mmi.gov.bc.ca/api/subscriber/work/orders/transcribe/" }, - "Charts": { - "Url": "http://charts-api:8080", - "Base64Path": "/base64", - "ImagePath": "/graph" - }, "CHES": { "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", "HostUri": "https://ches.api.gov.bc.ca/api/v1", "EmailEnabled": true, "EmailAuthorized": false }, + "Smtp": { + "From": "Media Monitoring Insights ", + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 + }, "Auth": { "Keycloak": { "Authority": "https://loginproxy.gov.bc.ca/auth", diff --git a/services/net/scheduler/Dockerfile b/services/net/scheduler/Dockerfile index 4b5c88b946..59e7317487 100644 --- a/services/net/scheduler/Dockerfile +++ b/services/net/scheduler/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -20,7 +20,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/scheduler RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . @@ -32,4 +32,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.Scheduler.dll +ENTRYPOINT ["dotnet", "TNO.Services.Scheduler.dll"] diff --git a/services/net/scheduler/SchedulerManager.cs b/services/net/scheduler/SchedulerManager.cs index 08e88d4d4a..683f11b893 100644 --- a/services/net/scheduler/SchedulerManager.cs +++ b/services/net/scheduler/SchedulerManager.cs @@ -1,9 +1,8 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.EventSchedule; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.Entities; @@ -24,17 +23,17 @@ public class SchedulerManager : ServiceManager /// Creates a new instance of a SchedulerManager object, initializes with specified parameters. /// /// - /// - /// + /// + /// /// /// public SchedulerManager( IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger logger) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { } #endregion diff --git a/services/net/scheduler/appsettings.Development.json b/services/net/scheduler/appsettings.Development.json index 1a1c6624e7..f13a02cd8e 100644 --- a/services/net/scheduler/appsettings.Development.json +++ b/services/net/scheduler/appsettings.Development.json @@ -15,12 +15,5 @@ "OIDC": { "Token": "/realms/mmi/protocol/openid-connect/token" } - }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false } } diff --git a/services/net/scheduler/appsettings.Staging.json b/services/net/scheduler/appsettings.Staging.json index 118d4c23fa..8701d165bc 100644 --- a/services/net/scheduler/appsettings.Staging.json +++ b/services/net/scheduler/appsettings.Staging.json @@ -15,12 +15,5 @@ "OIDC": { "Token": "/realms/mmi/protocol/openid-connect/token" } - }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false } } diff --git a/services/net/scheduler/appsettings.json b/services/net/scheduler/appsettings.json index 87b1c02af9..5168b7d16e 100644 --- a/services/net/scheduler/appsettings.json +++ b/services/net/scheduler/appsettings.json @@ -38,12 +38,10 @@ "EventTypes": ["Notification", "Report", "CleanFolder"], "SendEmailOnFailure": true }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/smtp-retry/.dockerignore b/services/net/smtp-retry/.dockerignore new file mode 100644 index 0000000000..7ed9d732a6 --- /dev/null +++ b/services/net/smtp-retry/.dockerignore @@ -0,0 +1,18 @@ +.vs/ +.env + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +**/[Bb]in/ +**/[Oo]bj/ +**/[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn diff --git a/services/net/smtp-retry/Config/SmtpRetryOptions.cs b/services/net/smtp-retry/Config/SmtpRetryOptions.cs new file mode 100644 index 0000000000..3d7e3dbb42 --- /dev/null +++ b/services/net/smtp-retry/Config/SmtpRetryOptions.cs @@ -0,0 +1,40 @@ + +using TNO.Services.Config; + +namespace MMI.Services.SmtpRetry.Config; + +/// +/// SmtpRetryOptions class, configuration options for notification service +/// +public class SmtpRetryOptions : ServiceOptions +{ + #region Properties + /// + /// get/set - Number of minutes before a retry can be attempted (default: 5) + /// This will ensure a message must have been sent at least X minutes in the past before a new attempt will be made. + /// + public int RetryTimeLimit { get; set; } = 5; + + /// + /// get/set - Number of minutes in the past that messages should be checked if they are sent (default: 60) + /// This will make a request for all messages that were sent in this time frame. + /// + public int RetryTimeScope { get; set; } = 60; + + /// + /// get/set - Add a delay between each email sent so that we don't send too many within a timeframe. + /// Set the number of milliseconds to wait after sending each request. + /// + public int ArtificialDelayMs { get; set; } = 500; + + /// + /// get/set - Whether to retry sending report emails. + /// + public bool RetryReports { get; set; } = true; + + /// + /// get/set - Whether to retry sending notification emails. + /// + public bool RetryNotifications { get; set; } = true; + #endregion +} diff --git a/services/net/smtp-retry/Dockerfile b/services/net/smtp-retry/Dockerfile new file mode 100644 index 0000000000..b5b7d7218c --- /dev/null +++ b/services/net/smtp-retry/Dockerfile @@ -0,0 +1,33 @@ + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build + +ENV DOTNET_CLI_HOME=/tmp +ENV PATH="$PATH:/tmp/.dotnet/tools" +ENV ASPNETCORE_ENVIRONMENT=Production + +# Switch to root for package installs +USER 0 + +WORKDIR /src +COPY services/net/smtp-retry services/net/smtp-retry +COPY libs/net libs/net + +RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u "$1"; shift; done } && \ + fix_permissions "/tmp" + +WORKDIR /src/services/net/smtp-retry +RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy + +WORKDIR /app +COPY --from=build /build . + +# [Optional] Uncomment this section to install additional OS packages. +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends procps curl libc6-dev libgdiplus + +# Run container by default as user with id 1001 (default) +USER 1001 + +ENTRYPOINT ["dotnet", "TNO.Services.SmtpRetry.dll"] diff --git a/services/net/smtp-retry/MMI.Services.SmtpRetry.csproj b/services/net/smtp-retry/MMI.Services.SmtpRetry.csproj new file mode 100644 index 0000000000..f9f303f1d0 --- /dev/null +++ b/services/net/smtp-retry/MMI.Services.SmtpRetry.csproj @@ -0,0 +1,31 @@ + + + Exe + net9.0 + enable + enable + MMI.Services.SmtpRetry + 1.0.0.0 + 1.0.0.0 + true + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/services/net/smtp-retry/Program.cs b/services/net/smtp-retry/Program.cs new file mode 100644 index 0000000000..bb08ded86e --- /dev/null +++ b/services/net/smtp-retry/Program.cs @@ -0,0 +1,19 @@ +namespace MMI.Services.SmtpRetry; + +/// +/// Program static class, runs program. +/// +public static class Program +{ + /// + /// Create an instance of the SmtpRetryService and run it. + /// + /// + /// + public static Task Main(string[] args) + { + // Run the SmtpRetry service console program. + var program = new SmtpRetryService(args); + return program.RunAsync(); + } +} diff --git a/services/net/smtp-retry/SmtpRetryManager.cs b/services/net/smtp-retry/SmtpRetryManager.cs new file mode 100644 index 0000000000..e6bda477a8 --- /dev/null +++ b/services/net/smtp-retry/SmtpRetryManager.cs @@ -0,0 +1,373 @@ +using System.Net.Mail; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MMI.Services.SmtpRetry.Config; +using MMI.SmtpEmail; +using TNO.Core.Exceptions; +using TNO.Entities; +using TNO.Services; +using TNO.Services.Managers; + +namespace MMI.Services.SmtpRetry; + +/// +/// SmtpRetryManager class, provides a service which checks if there are any emails that have not been successfully sent.. +/// +public class SmtpRetryManager : ServiceManager +{ + #region Variables + private readonly JsonSerializerOptions _serializationOptions; + private readonly ClaimsPrincipal _user; + #endregion + + #region Constructors + /// + /// Creates a new instance of a ChesRetryManager object, initializes with specified parameters. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public SmtpRetryManager( + IApiService api, + ClaimsPrincipal user, + IEmailService emailService, + IOptions smtpOptions, + IOptions serializationOptions, + IOptions notificationOptions, + ILogger logger) + : base(api, emailService, smtpOptions, notificationOptions, logger) + { + _user = user; + _serializationOptions = serializationOptions.Value; + } + #endregion + + #region Methods + /// + /// Listen to active topics and import content. + /// + /// + public override async Task RunAsync() + { + var delay = this.Options.DefaultDelayMS; + + // Always keep looping until an unexpected failure occurs. + while (true) + { + try + { + if (this.State.Status == ServiceStatus.RequestSleep || this.State.Status == ServiceStatus.RequestPause || this.State.Status == ServiceStatus.RequestFailed) + { + // An API request or failures have requested the service to stop. + this.Logger.LogInformation("The service is stopping: '{Status}'", this.State.Status); + this.State.Stop(); + } + else if (this.State.Status == ServiceStatus.Failed && this.Options.AutoRestartAfterCriticalFailure) + { + await Task.Delay(this.Options.RetryAfterCriticalFailureDelayMS); + this.State.Resume(); + continue; + } + else if (this.State.Status != ServiceStatus.Running) + { + this.Logger.LogDebug("The service is not running: '{Status}'", this.State.Status); + } + else + { + if (this.Options.RetryReports) await RetryReportsAsync(); + if (this.Options.RetryNotifications) await RetryNotificationsAsync(); + } + + // The delay ensures we don't have a run away thread. + this.Logger.LogDebug("Service sleeping for {delay} ms", delay); + await Task.Delay(delay); + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Service had an unexpected failure."); + this.State.RecordFailure(); + await this.SendErrorEmailAsync("Service had an Unexpected Failure", ex); + await Task.Delay(delay); + } + } + } + + /// + /// Make a request to fetch the report instance and user report instance for the message. + /// Try to promote the email to force it to be sent. + /// + /// + /// + private async Task RetryReportInstanceAsync(TNO.API.Areas.Services.Models.Report.SmtpReportMessagesModel reportMessage) + { + var reportInstance = await this.Api.GetReportInstanceAsync(reportMessage.InstanceId); + if (reportInstance == null) return false; + + // If there is a userId, get the record for the user report instance to check the status of the email. + var userReportInstance = reportMessage.UserId.HasValue ? await this.Api.GetUserReportInstanceAsync(reportMessage.InstanceId, reportMessage.UserId.Value) : null; + if (reportMessage.UserId.HasValue && userReportInstance == null) return false; + + // If there is a userId, then this is a message for an individual user report instance, otherwise it is for the whole report. + if (userReportInstance != null) + { + if (reportMessage.Format == ReportDistributionFormat.FullText || reportMessage.Format == ReportDistributionFormat.ReceiveBoth && !String.IsNullOrWhiteSpace(reportInstance.Body)) + { + var response = JsonSerializer.Deserialize(userReportInstance.TextResponse, _serializationOptions); + if (response == null) + { + this.Logger.LogWarning("Failed to deserialize user report instance text response. Report ID: {reportId}, Instance ID: {instanceId}, User Id: {userId}", + reportMessage.ReportId, reportMessage.InstanceId, reportMessage.UserId); + return false; + } + + // The message is possibly stuck, we need to ask CHES to promote. + // Promoting doesn't change the status. + // Which means it may get picked up again in the Accepted status until it gets sent. + this.Logger.LogDebug("Retry email request. Report ID: {reportId}, Instance ID: {instanceId}, User Id: {userId}", + reportMessage.ReportId, reportMessage.InstanceId, reportMessage.UserId); + + var message = this.EmailService.CreateMailMessage(reportInstance.Subject, reportInstance.Body, response.To, response.CC, response.Bcc); + response = await this.EmailService.SendAsync(message); + + if (response.StatusCode == SmtpStatusCode.Ok) + { + response.Data = $"This email was sent by the SMTP Retry Service."; + userReportInstance.TextResponse = response.ToJsonDocument(_serializationOptions); + userReportInstance.TextStatus = ReportStatus.Completed; + reportMessage.Status = ReportStatus.Completed; + await this.Api.AddOrUpdateUserReportInstanceAsync(userReportInstance); + } + } + else if (reportMessage.Format == ReportDistributionFormat.LinkOnly || reportMessage.Format == ReportDistributionFormat.ReceiveBoth && !String.IsNullOrWhiteSpace(reportInstance.LinkOnlyBody)) + { + var response = JsonSerializer.Deserialize(userReportInstance.LinkResponse, _serializationOptions); + if (response == null) + { + this.Logger.LogWarning("Failed to deserialize user report instance link only response. Report ID: {reportId}, Instance ID: {instanceId}, User Id: {userId}", + reportMessage.ReportId, reportMessage.InstanceId, reportMessage.UserId); + return false; + } + + // The message is possibly stuck, we need to ask CHES to promote. + // Promoting doesn't change the status. + // Which means it may get picked up again in the Accepted status until it gets sent. + this.Logger.LogDebug("Retry email request. Report ID: {reportId}, Instance ID: {instanceId}, User Id: {userId}", + reportMessage.ReportId, reportMessage.InstanceId, reportMessage.UserId); + + var message = this.EmailService.CreateMailMessage(reportInstance.Subject, reportInstance.LinkOnlyBody, response.To, response.CC, response.Bcc); + response = await this.EmailService.SendAsync(message); + + if (response.StatusCode == SmtpStatusCode.Ok) + { + response.Data = $"This email was sent by the SMTP Retry Service."; + userReportInstance.LinkResponse = response.ToJsonDocument(_serializationOptions); + userReportInstance.LinkStatus = ReportStatus.Completed; + reportMessage.Status = ReportStatus.Completed; + await this.Api.AddOrUpdateUserReportInstanceAsync(userReportInstance); + } + } + return true; + } + else + { + this.Logger.LogWarning("Unable to retry sending this report. Report ID: {reportId}, Instance ID: {instanceId}", reportMessage.ReportId, reportMessage.InstanceId); + return false; + } + } + + + /// + /// Make a request to fetch the AV overview report instance and user report instance for the message. + /// Try to promote the email to force it to be sent. + /// + /// + /// + private async Task RetryAVOverviewReportInstanceAsync(TNO.API.Areas.Services.Models.Report.SmtpReportMessagesModel reportMessage) + { + + var reportInstance = await this.Api.GetAVOverviewInstanceAsync(reportMessage.InstanceId); + if (reportInstance == null) return false; + + // If there is a userId, get the record for the user report instance to check the status of the email. + var userReportInstance = reportMessage.UserId.HasValue ? await this.Api.GetUserAVOverviewInstanceAsync(reportMessage.InstanceId, reportMessage.UserId.Value) : null; + if (reportMessage.UserId.HasValue && userReportInstance == null) return false; + + if (userReportInstance != null) + { + var response = JsonSerializer.Deserialize(userReportInstance.Response, _serializationOptions); + if (response == null) + { + this.Logger.LogWarning("Failed to deserialize AV user report instance text response. Report ID: {reportId}, Instance ID: {instanceId}, User Id: {userId}", + reportMessage.ReportId, reportMessage.InstanceId, reportMessage.UserId); + return false; + } + + var message = this.EmailService.CreateMailMessage(reportInstance.Subject, reportInstance.Body, response.To, response.CC, response.Bcc); + response = await this.EmailService.SendAsync(message); + + if (response.StatusCode == SmtpStatusCode.Ok) + { + response.Data = $"This email was sent by the SMTP Retry Service."; + userReportInstance.Response = response.ToJsonDocument(_serializationOptions); + userReportInstance.Status = ReportStatus.Completed; + reportMessage.Status = ReportStatus.Completed; + await this.Api.AddOrUpdateUserAVOverviewInstanceAsync(userReportInstance); + } + return true; + } + else + { + this.Logger.LogWarning("Unable to retry sending this AV overview report. Report ID: {reportId}, Instance ID: {instanceId}", reportMessage.ReportId, reportMessage.InstanceId); + return false; + } + } + + /// + /// Make a request to fetch all reports that have emails in accepted status. + /// Try to promote these emails to force them to be sent. + /// + /// + private async Task RetryReportsAsync() + { + // Request all reports for the last hour that have failed to send. + var now = DateTime.UtcNow; + + // This returns an array of report user instances that have failed to send emails. + var reportMessages = await this.Api.GetSmtpMessagesAsync(ReportStatus.Failed, now.AddMinutes(-1 * this.Options.RetryTimeScope)) ?? []; + foreach (var reportMessage in reportMessages) + { + try + { + // Only check the status of the SMTP message if a report was sent more than 5 minutes ago. + if (reportMessage.SentOn.HasValue && reportMessage.SentOn.Value.AddMinutes(this.Options.RetryTimeLimit) <= now) + { + if (reportMessage.ReportType == ReportType.Content) + { + var updated = await RetryReportInstanceAsync(reportMessage); + if (!updated) continue; + } + else + { + var updated = await RetryAVOverviewReportInstanceAsync(reportMessage); + if (!updated) continue; + } + } + + // Slow down the number of email requests. + if (this.Options.ArtificialDelayMs > 0) + await Task.Delay(this.Options.ArtificialDelayMs); + } + catch (SmtpException ex) + { + this.Logger.LogError(ex, "Failed to send email. Report ID: {reportId}, Instance Id: {instanceId}, User Id: {userId}", reportMessage.ReportId, reportMessage.InstanceId, reportMessage.UserId); + continue; + } + } + + var reportGroups = reportMessages.GroupBy(r => new { r.InstanceId, r.ReportType }); + foreach (var group in reportGroups) + { + // If all user report instances have been updated then the report instance should be updated as well. + if (group.All(r => r.Status == ReportStatus.Completed)) + { + try + { + if (group.Key.ReportType == ReportType.Content) + { + var reportInstance = await this.Api.GetReportInstanceAsync(group.Key.InstanceId); + if (reportInstance == null) continue; + // We should update the full response for the report instance, but we can at least update the status to completed so that it doesn't get picked up again in the retry. + this.Logger.LogInformation("Update report status to completed. Instance ID: {instanceId}", group.Key); + reportInstance.Status = ReportStatus.Completed; + reportInstance.Response = JsonDocument.Parse($"{{ \"Data\": \"This report was marked as completed by the SMTP Retry Service.\" }}"); + await this.Api.UpdateReportInstanceAsync(reportInstance, false); + } + else + { + var reportInstance = await this.Api.GetAVOverviewInstanceAsync(group.Key.InstanceId); + if (reportInstance == null) continue; + this.Logger.LogInformation("Update AV overview report status to completed. Instance ID: {instanceId}", group.Key); + reportInstance.Status = ReportStatus.Completed; + reportInstance.Response = JsonDocument.Parse($"{{ \"Data\": \"This report was marked as completed by the SMTP Retry Service.\" }}"); + await this.Api.UpdateAVOverviewInstanceAsync(reportInstance); + } + } + catch (NoContentException ex) + { + this.Logger.LogError(ex, "Report instance does not exist. Instance ID: {instanceId}", group.Key); + } + } + } + } + + /// + /// Make a request to fetch all notifications that have emails in accepted status. + /// Try to resend these emails to force them to be sent. + /// + /// + private async Task RetryNotificationsAsync() + { + // Request all notifications for the last hour that SMTP has not confirmed the email has been sent. + var now = DateTime.UtcNow; + var notificationMessages = await this.Api.GetSmtpMessagesAsync(NotificationStatus.Accepted, now.AddMinutes(-1 * this.Options.RetryTimeScope)) ?? []; + + foreach (var notificationMessage in notificationMessages) + { + // Only check the status of the SMTP message if a notification was sent more than 5 minutes ago. + if (notificationMessage.SentOn.HasValue && notificationMessage.SentOn.Value.AddMinutes(this.Options.RetryTimeLimit) <= now) + { + var responses = JsonSerializer.Deserialize(notificationMessage.Responses, _serializationOptions); + if (responses == null) break; + + var completed = 0; + foreach (var response in responses) + { + // Don't try to resend if the email was sent successfully. + if (response == null || response.StatusCode == SmtpStatusCode.Ok) break; + + try + { + var notificationInstance = await this.Api.GetNotificationInstanceAsync(notificationMessage.InstanceId); + if (notificationInstance == null) break; + + var message = this.EmailService.CreateMailMessage(notificationInstance.Subject, notificationInstance.Body, response.To, response.CC, response.Bcc); + var responseUpdate = await this.EmailService.SendAsync(message); + + this.Logger.LogInformation("Sent email. Notification ID: {notificationId}, Instance ID: {instanceId}", notificationMessage.NotificationId, notificationMessage.InstanceId); + + // Keep track of how many of the emails have been sent. + if (responseUpdate.StatusCode == SmtpStatusCode.Ok) completed++; + } + catch (SmtpException ex) + { + this.Logger.LogError(ex, "Failed to send email. Notification ID: {notificationId}, Instance Id: {instanceId}", notificationMessage.NotificationId, notificationMessage.InstanceId); + continue; + } + + // Slow down the number of email requests. + if (this.Options.ArtificialDelayMs > 0) + await Task.Delay(this.Options.ArtificialDelayMs); + } + + // If all messages for this notification instance have been sent, update the status. + if (notificationMessage.Status != NotificationStatus.Completed && responses.Length == completed) + { + this.Logger.LogInformation("Update notification status. Notification ID: {notificationId}, Instance ID: {instanceId}, Status: {status}", notificationMessage.NotificationId, notificationMessage.InstanceId, NotificationStatus.Completed); + await this.Api.UpdateNotificationInstanceAsync(notificationMessage.InstanceId, NotificationStatus.Completed); + } + } + } + } + #endregion +} diff --git a/services/net/smtp-retry/SmtpRetryService.cs b/services/net/smtp-retry/SmtpRetryService.cs new file mode 100644 index 0000000000..57ee508a74 --- /dev/null +++ b/services/net/smtp-retry/SmtpRetryService.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using MMI.Services.SmtpRetry.Config; +using TNO.Services; +using TNO.Services.Runners; + +namespace MMI.Services.SmtpRetry; + +/// +/// SmtpRetryService abstract class, provides a console application that runs service, and an api. +/// +public class SmtpRetryService : BaseService +{ + #region Variables + #endregion + + #region Properties + #endregion + + #region Constructors + /// + /// Creates a new instance of a SmtpRetryService object, initializes with arguments. + /// + /// + public SmtpRetryService(string[] args) : base(args) + { + } + #endregion + + #region Methods + /// + /// Configure dependency injection. + /// + /// + /// + protected override IServiceCollection ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + services + .Configure(this.Configuration.GetSection("Service")) + .AddSingleton(); + + // TODO: Figure out how to validate without resulting in aggregating the config values. + // services.AddOptions() + // .Bind(this.Configuration.GetSection("Service")) + // .ValidateDataAnnotations(); + + return services; + } + #endregion +} diff --git a/services/net/smtp-retry/appsettings.Development.json b/services/net/smtp-retry/appsettings.Development.json new file mode 100644 index 0000000000..02056fecd3 --- /dev/null +++ b/services/net/smtp-retry/appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "TNO": "Debug" + } + }, + "Service": { + "MaxFailLimit": 5, + "ApiUrl": "http://host.docker.internal:40010/api" + }, + "CHES": { + "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", + "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", + "From": "Media Monitoring Insights ", + "EmailEnabled": true, + "EmailAuthorized": false + } +} diff --git a/services/net/smtp-retry/appsettings.Staging.json b/services/net/smtp-retry/appsettings.Staging.json new file mode 100644 index 0000000000..3b2706aa6b --- /dev/null +++ b/services/net/smtp-retry/appsettings.Staging.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Error", + "TNO": "Information" + } + }, + "Service": { + "MaxFailLimit": 5, + "ApiUrl": "http://api:8080" + }, + "CHES": { + "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", + "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", + "From": "Media Monitoring Insights ", + "EmailEnabled": true, + "EmailAuthorized": false + } +} diff --git a/services/net/smtp-retry/appsettings.json b/services/net/smtp-retry/appsettings.json new file mode 100644 index 0000000000..0d1aeca03c --- /dev/null +++ b/services/net/smtp-retry/appsettings.json @@ -0,0 +1,69 @@ +{ + "BaseUrl": "/", + "Logging": { + "Console": { + "DisableColors": true + }, + "LogLevel": { + "Default": "Warning", + "Microsoft": "Error", + "TNO": "Information" + } + }, + "Serilog": { + "Using": ["Serilog.Sinks.Console"], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Error", + "System.Net.Http": "Warning", + "TNO": "Debug" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} level={CustomLevel}] {Message:lj}{NewLine}{Exception}" + } + } + ], + "Enrich": ["FromLogContext"] + }, + "AllowedHosts": "*", + "Service": { + "MaxFailLimit": 5, + "ApiUrl": "http://api:8080", + "DefaultDelayMS": 60000 + }, + "CHES": { + "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", + "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "From": "Media Monitoring Insights ", + "EmailEnabled": true, + "EmailAuthorized": false + }, + "Smtp": { + "From": "Media Monitoring Insights ", + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 + }, + "Auth": { + "Keycloak": { + "Authority": "https://loginproxy.gov.bc.ca/auth", + "Audience": "mmi-service-account", + "Secret": "{DO NOT STORE SECRET HERE}" + }, + "OIDC": { + "Token": "/realms/mmi/protocol/openid-connect/token" + } + }, + "Serialization": { + "Json": { + "PropertyNamingPolicy": "CamelCase", + "PropertyNameCaseInsensitive": true, + "DefaultIgnoreCondition": "WhenWritingNull", + "WriteIndented": true + } + } +} diff --git a/services/net/syndication/Dockerfile b/services/net/syndication/Dockerfile index 9e4e1c3700..df8137197e 100644 --- a/services/net/syndication/Dockerfile +++ b/services/net/syndication/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -20,7 +20,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/syndication RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . @@ -32,4 +32,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.Syndication.dll +ENTRYPOINT ["dotnet", "TNO.Services.Syndication.dll"] diff --git a/services/net/syndication/SyndicationIngestActionManager.cs b/services/net/syndication/SyndicationIngestActionManager.cs index 38249992ab..90dda5a02c 100644 --- a/services/net/syndication/SyndicationIngestActionManager.cs +++ b/services/net/syndication/SyndicationIngestActionManager.cs @@ -1,8 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Ingest; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Models.Extensions; using TNO.Services.Actions.Managers; using TNO.Services.Syndication.Config; @@ -20,20 +19,18 @@ public class SyndicationIngestActionManager : IngestActionManager /// /// - /// - /// + /// /// /// /// public SyndicationIngestActionManager( IngestModel ingest, IApiService api, - IChesService ches, - IOptions chesOptions, + IEmailService emailService, IIngestAction action, IOptions options, ILogger logger) - : base(ingest, api, ches, chesOptions, action, options, logger) + : base(ingest, api, emailService, action, options, logger) { } #endregion diff --git a/services/net/syndication/SyndicationManager.cs b/services/net/syndication/SyndicationManager.cs index d112cf53a0..0192d43256 100644 --- a/services/net/syndication/SyndicationManager.cs +++ b/services/net/syndication/SyndicationManager.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using TNO.Ches; -using TNO.Ches.Configuration; +using MMI.SmtpEmail; using TNO.Services.Managers; using TNO.Services.Syndication.Config; @@ -18,20 +17,20 @@ public class SyndicationManager : IngestManager /// /// - /// - /// + /// + /// /// /// /// public SyndicationManager( IServiceProvider serviceProvider, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IngestManagerFactory factory, IOptions options, ILogger logger) - : base(serviceProvider, api, chesService, chesOptions, factory, options, logger) + : base(serviceProvider, api, emailService, smtpOptions, factory, options, logger) { } #endregion diff --git a/services/net/syndication/appsettings.Development.json b/services/net/syndication/appsettings.Development.json index 1a1c6624e7..f13a02cd8e 100644 --- a/services/net/syndication/appsettings.Development.json +++ b/services/net/syndication/appsettings.Development.json @@ -15,12 +15,5 @@ "OIDC": { "Token": "/realms/mmi/protocol/openid-connect/token" } - }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false } } diff --git a/services/net/syndication/appsettings.Staging.json b/services/net/syndication/appsettings.Staging.json index 118d4c23fa..8701d165bc 100644 --- a/services/net/syndication/appsettings.Staging.json +++ b/services/net/syndication/appsettings.Staging.json @@ -15,12 +15,5 @@ "OIDC": { "Token": "/realms/mmi/protocol/openid-connect/token" } - }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false } } diff --git a/services/net/syndication/appsettings.json b/services/net/syndication/appsettings.json index ad555ba5be..3dd6ef7744 100644 --- a/services/net/syndication/appsettings.json +++ b/services/net/syndication/appsettings.json @@ -39,12 +39,10 @@ "InvalidEncodings": "�e(TM):_'__':_'", "SendEmailOnFailure": true }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": { diff --git a/services/net/transcription/Dockerfile b/services/net/transcription/Dockerfile index a122ff7678..90ac2911d0 100644 --- a/services/net/transcription/Dockerfile +++ b/services/net/transcription/Dockerfile @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 as build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ENV DOTNET_CLI_HOME=/tmp ENV PATH="$PATH:/tmp/.dotnet/tools" @@ -18,7 +18,7 @@ RUN fix_permissions() { while [ $# -gt 0 ] ; do chgrp -R 0 "$1" && chmod -R g=u WORKDIR /src/services/net/transcription RUN dotnet build -c $ASPNETCORE_ENVIRONMENT -o /build -FROM mcr.microsoft.com/dotnet/aspnet:9.0 as deploy +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy WORKDIR /app COPY --from=build /build . @@ -40,4 +40,4 @@ RUN apt-get -y install \ # Run container by default as user with id 1001 (default) USER 1001 -ENTRYPOINT dotnet TNO.Services.Transcription.dll +ENTRYPOINT ["dotnet", "TNO.Services.Transcription.dll"] diff --git a/services/net/transcription/TranscriptionManager.cs b/services/net/transcription/TranscriptionManager.cs index d85915908d..28deefc947 100644 --- a/services/net/transcription/TranscriptionManager.cs +++ b/services/net/transcription/TranscriptionManager.cs @@ -5,9 +5,8 @@ using Microsoft.CognitiveServices.Speech.Audio; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MMI.SmtpEmail; using TNO.API.Areas.Services.Models.Content; -using TNO.Ches; -using TNO.Ches.Configuration; using TNO.Core.Exceptions; using TNO.Core.Extensions; using TNO.Core.Storage; @@ -47,20 +46,20 @@ public class TranscriptionManager : ServiceManager /// /// /// - /// - /// + /// + /// /// /// /// public TranscriptionManager( IKafkaListener listener, IApiService api, - IChesService chesService, - IOptions chesOptions, + IEmailService emailService, + IOptions smtpOptions, IOptions options, ILogger logger, IS3StorageService s3StorageService) - : base(api, chesService, chesOptions, options, logger) + : base(api, emailService, smtpOptions, options, logger) { this.Listener = listener; this.Listener.IsLongRunningJob = true; diff --git a/services/net/transcription/appsettings.Development.json b/services/net/transcription/appsettings.Development.json index c1af09d4af..5e9c8b15ef 100644 --- a/services/net/transcription/appsettings.Development.json +++ b/services/net/transcription/appsettings.Development.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://host.docker.internal:40010/api" }, - "CHES": { - "AuthUrl": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-dev.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "host.docker.internal:40102" diff --git a/services/net/transcription/appsettings.Staging.json b/services/net/transcription/appsettings.Staging.json index 51639d3285..c4183898d5 100644 --- a/services/net/transcription/appsettings.Staging.json +++ b/services/net/transcription/appsettings.Staging.json @@ -10,13 +10,6 @@ "MaxFailLimit": 5, "ApiUrl": "http://api:8080" }, - "CHES": { - "AuthUrl": "https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches-test.api.gov.bc.ca/api/v1", - "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false - }, "Kafka": { "Consumer": { "BootstrapServers": "kafka-broker-0.kafka-headless:9092,kafka-broker-1.kafka-headless:9092,kafka-broker-2.kafka-headless:9092" diff --git a/services/net/transcription/appsettings.json b/services/net/transcription/appsettings.json index a701c262e2..272e3dbd50 100644 --- a/services/net/transcription/appsettings.json +++ b/services/net/transcription/appsettings.json @@ -43,12 +43,10 @@ "SendEmailOnFailure": true, "NoticeEmailTo": "" }, - "CHES": { - "AuthUrl": "https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", - "HostUri": "https://ches.api.gov.bc.ca/api/v1", + "Smtp": { "From": "Media Monitoring Insights ", - "EmailEnabled": true, - "EmailAuthorized": false + "Host": "apps.smtp.gov.bc.ca", + "Port": 25 }, "Auth": { "Keycloak": {