Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notifications funcitonality added #GCPActive #62

Merged
merged 10 commits into from
Nov 20, 2023
12 changes: 8 additions & 4 deletions src/Configuration/LocalPlatformSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class LocalPlatformSettings
string _localTestDataPath = null;

/// <summary>
/// The endpoint for the bridge
/// The path to the local storage folder
/// </summary>
public string LocalTestingStorageBasePath { get; set; }

Expand All @@ -21,12 +21,16 @@ public class LocalPlatformSettings

public string BlobStorageFolder { get; set; } = "blobs/";

public string NotificationsStorageFolder { get; set; } = "notifications/";

/// <summary>
/// Folder where static test data like profile, authorization, and register data is available for local testing.
/// </summary>
public string LocalTestingStaticTestDataPath {
public string LocalTestingStaticTestDataPath
{
get => _localTestDataPath;
set {
set
{
if (!value.EndsWith(Path.DirectorySeparatorChar) &&
!value.EndsWith(Path.AltDirectorySeparatorChar))
{
Expand All @@ -47,7 +51,7 @@ public string LocalTestingStaticTestDataPath {
/// <summary>
public string LocalAppMode { get; set; }

public string DocumentDbFolder { get; set; } = "documentdb/";
public string DocumentDbFolder { get; set; } = "documentdb/";

public string InstanceCollectionFolder { get; set; } = "instances/";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Text.RegularExpressions;

using Altinn.Notifications.Models;

using FluentValidation;

namespace Altinn.Notifications.Validators;

/// <summary>
/// Class containing validation logic for the <see cref="EmailNotificationOrderRequestExt"/> model
/// </summary>
public class EmailNotificationOrderRequestValidator : AbstractValidator<EmailNotificationOrderRequestExt>
{
/// <summary>
/// Initializes a new instance of the <see cref="EmailNotificationOrderRequestValidator"/> class.
/// </summary>
public EmailNotificationOrderRequestValidator()
{
RuleFor(order => order.Recipients)
.NotEmpty()
.WithMessage("One or more recipient is required.")
.Must(recipients => recipients.TrueForAll(a => IsValidEmail(a.EmailAddress)))
.WithMessage("A valid email address must be provided for all recipients.");

RuleFor(order => order.RequestedSendTime)
.Must(sendTime => sendTime >= DateTime.UtcNow.AddMinutes(-5))
.WithMessage("Send time must be in the future. Leave blank to send immediately.");

RuleFor(order => order.Body).NotEmpty();
RuleFor(order => order.Subject).NotEmpty();
}

/// <summary>
/// Validated as email address based on the Altinn 2 regex
/// </summary>
/// <param name="email">The string to validate as an email address</param>
/// <returns>A boolean indicating that the email is valid or not</returns>
internal static bool IsValidEmail(string? email)

Check warning on line 38 in src/Controllers/Notifications/EmailNotificationOrderRequestValidator.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if (string.IsNullOrEmpty(email))
{
return false;
}

string emailRegexPattern = @"((&quot;[^&quot;]+&quot;)|(([a-zA-Z0-9!#$%&amp;'*+\-=?\^_`{|}~])+(\.([a-zA-Z0-9!#$%&amp;'*+\-=?\^_`{|}~])+)*))@((((([a-zA-Z0-9æøåÆØÅ]([a-zA-Z0-9\-æøåÆØÅ]{0,61})[a-zA-Z0-9æøåÆØÅ]\.)|[a-zA-Z0-9æøåÆØÅ]\.){1,9})([a-zA-Z]{2,14}))|((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})))";

Regex regex = new(emailRegexPattern, RegexOptions.None, TimeSpan.FromSeconds(1));

Match match = regex.Match(email);

return match.Success;
}
}
68 changes: 68 additions & 0 deletions src/Controllers/Notifications/EmailNotificationOrdersController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Altinn.Notifications.Core.Models.Orders;
using Altinn.Notifications.Core.Services.Interfaces;
using Altinn.Notifications.Extensions;
using Altinn.Notifications.Mappers;
using Altinn.Notifications.Models;
using Altinn.Notifications.Validators;

using FluentValidation;

using LocalTest.Models;

using Microsoft.AspNetCore.Mvc;

namespace Altinn.Notifications.Controllers;

/// <summary>
/// Controller for all operations related to email notification orders
/// </summary>
[Route("notifications/api/v1/orders/email")]
[ApiController]
public class EmailNotificationOrdersController : ControllerBase
{
private readonly IValidator<EmailNotificationOrderRequestExt> _validator;
private readonly IEmailNotificationOrderService _orderService;

/// <summary>
/// Initializes a new instance of the <see cref="EmailNotificationOrdersController"/> class.
/// </summary>
public EmailNotificationOrdersController(IValidator<EmailNotificationOrderRequestExt> validator, IEmailNotificationOrderService orderService)
{
_validator = validator;
_orderService = orderService;
}

/// <summary>
/// Add an email notification order.
/// </summary>
/// <remarks>
/// The API will accept the request after som basic validation of the request.
/// The system will also attempt to verify that it will be possible to fulfill the order.
/// </remarks>
/// <returns>The id of the registered notification order</returns>
[HttpPost]
[Consumes("application/json")]
[Produces("application/json")]
public async Task<ActionResult<OrderIdExt>> Post(EmailNotificationOrderRequestExt emailNotificationOrderRequest)
{
var validationResult = _validator.Validate(emailNotificationOrderRequest);
if (!validationResult.IsValid)
{
validationResult.AddToModelState(this.ModelState);
return ValidationProblem(ModelState);
}

string creator = "localtest";

var orderRequest = emailNotificationOrderRequest.MapToOrderRequest(creator);
(NotificationOrder? registeredOrder, ServiceError? error) = await _orderService.RegisterEmailNotificationOrder(orderRequest);

if (error != null)
{
return StatusCode(error.ErrorCode, error.ErrorMessage);
}

string selfLink = registeredOrder!.GetSelfLink();
return Accepted(selfLink, new OrderIdExt(registeredOrder!.Id));
}
}
22 changes: 22 additions & 0 deletions src/Controllers/Notifications/ValidationResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using FluentValidation.Results;

using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Altinn.Notifications.Validators;

/// <summary>
/// Extension class for <see cref="ValidationResult"/>
/// </summary>
public static class ValidationResultExtensions
{
/// <summary>
/// Adds the validation result to the model state
/// </summary>
public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState)
{
foreach (var error in result.Errors)
{
modelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
}
18 changes: 18 additions & 0 deletions src/Extensions/Notifications/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Altinn.Notifications.Extensions;

/// <summary>
/// Extensions for HTTP Context
/// </summary>
public static class HttpContextExtensions
{
/// <summary>
/// Get the org string from the context items or null if it is not defined
/// </summary>
/// <remarks>
/// The org item is populated to the http context by the <see cref="Middleware.OrgExtractorMiddleware"/>
/// </remarks>
public static string? GetOrg(this HttpContext context)

Check warning on line 14 in src/Extensions/Notifications/HttpContextExtensions.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
return context.Items["Org"] as string;
}
}
80 changes: 80 additions & 0 deletions src/Extensions/Notifications/ResourceLinkExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Altinn.Notifications.Core.Models.Orders;
using Altinn.Notifications.Models;

namespace Altinn.Notifications.Extensions;

/// <summary>
/// Extension class for ResourceLinks
/// </summary>
public static class ResourceLinkExtensions
{
private static string? _baseUri;

Check warning on line 11 in src/Extensions/Notifications/ResourceLinkExtensions.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

/// <summary>
/// Initializes the ResourceLinkExtensions with the base URI from settings.
/// </summary>
/// <remarks>
/// Should be called during startup to ensure base url is set
/// </remarks>
public static void Initialize(string baseUri)
{
_baseUri = baseUri;
}

/// <summary>
/// Sets the resource links on an external notification order
/// </summary>
/// <exception cref="InvalidOperationException">Exception if class has not been initialized in Program.cs</exception>
public static void SetResourceLinks(this NotificationOrderExt order)
{
if (_baseUri == null)
{
throw new InvalidOperationException("ResourceLinkExtensions has not been initialized with the base URI.");
}

string self = _baseUri + "/notifications/api/v1/orders/" + order.Id;

order.Links = new()
{
Self = self,
Status = self + "/status",
Notifications = self + "/notifications"
};
}

/// <summary>
/// Gets the self link for the provided notification order
/// </summary>
/// <exception cref="InvalidOperationException">Exception if class has not been initialized in Program.cs</exception>
public static void NotificationSummaryResourceLinks(this NotificationOrderWithStatusExt order)
{
if (_baseUri == null)
{
throw new InvalidOperationException("ResourceLinkExtensions has not been initialized with the base URI.");
}

string baseUri = $"{_baseUri}/notifications/api/v1/orders/{order!.Id}/notifications/";

if (order.NotificationsStatusSummary?.Email != null)
{
order.NotificationsStatusSummary.Email.Links = new()
{
Self = baseUri + "email"
};
}
}

/// <summary>
/// Gets the self link for the provided notification order
/// </summary>
/// <exception cref="InvalidOperationException">Exception if class has not been initialized in Program.cs</exception>
public static string GetSelfLink(this NotificationOrder order)
{
if (_baseUri == null)
{
throw new InvalidOperationException("ResourceLinkExtensions has not been initialized with the base URI.");
}

return _baseUri + "/notifications/api/v1/orders/" + order!.Id;
}
}
1 change: 1 addition & 0 deletions src/LocalTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="Altinn.Platform.Models" Version="1.2.0" />
<PackageReference Include="Altinn.Platform.Storage.Interface" Version="3.22.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="FluentValidation" Version="11.8.0" />
<PackageReference Include="JWTCookieAuthentication" Version="2.4.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
Expand Down
Loading