Skip to content

Commit

Permalink
[YOMA-66] Feature/action links (#752)
Browse files Browse the repository at this point in the history
* Added entities, repositories and domain enumerations and models

* Added services
Sharing now uses an action link
Updated routes
Link management, cliaming and instant verify still in progress

* Small refactoring

* Linting

* Entity refactoring
Unique usage now tracked provided authenticated

* Linting

* Added comments
  • Loading branch information
adrianwium authored Apr 25, 2024
1 parent f48b4f8 commit be17db2
Show file tree
Hide file tree
Showing 48 changed files with 3,689 additions and 161 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Swashbuckle.AspNetCore.Annotations;
using System.ComponentModel.DataAnnotations;
using System.Net;
using Yoma.Core.Domain.ActionLink.Models;
using Yoma.Core.Domain.Core;
using Yoma.Core.Domain.Opportunity;
using Yoma.Core.Domain.Opportunity.Interfaces;
Expand Down Expand Up @@ -66,18 +67,18 @@ public IActionResult GetPublishedOrExpiredById([FromRoute] Guid id)
return StatusCode((int)HttpStatusCode.OK, result);
}

[SwaggerOperation(Summary = "Get sharing details for published or expired opportunity by id (Anonymous)")]
[HttpGet("{id}/sharing")]
[ProducesResponseType(typeof(OpportunityInfo), (int)HttpStatusCode.OK)]
[SwaggerOperation(Summary = "Create sharing link for published or expired opportunity by id (Anonymous)")]
[HttpGet("{id}/link/sharing")]
[ProducesResponseType(typeof(LinkInfo), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[AllowAnonymous]
public async Task<IActionResult> GetSharingDetails([FromRoute] Guid id, [FromQuery] bool? includeQRCode)
public async Task<IActionResult> CreateSharingLink([FromRoute] Guid id, [FromQuery] bool? includeQRCode)
{
_logger.LogInformation("Handling request {requestName}", nameof(GetSharingDetails));
_logger.LogInformation("Handling request {requestName}", nameof(CreateSharingLink));

var result = await _opportunityService.GetSharingDetails(id, true, includeQRCode);
var result = await _opportunityService.CreateLinkSharing(id, true, includeQRCode, false);

_logger.LogInformation("Request {requestName} handled", nameof(GetSharingDetails));
_logger.LogInformation("Request {requestName} handled", nameof(CreateSharingLink));

return StatusCode((int)HttpStatusCode.OK, result);
}
Expand Down Expand Up @@ -211,40 +212,22 @@ public IActionResult ListOpportunitySearchCriteriaZltoReward([FromQuery] List<Pu
#endregion

#region Authenticated User Based Actions
[SwaggerOperation(Summary = "Get the specified opportunity info by id (User, Admin or Organization Admin role required)")]
[SwaggerOperation(Summary = "Get the specified opportunity info by id (Authenticated User)")]
[HttpGet("{id}/auth/info")]
[ProducesResponseType(typeof(OpportunityInfo), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[Authorize(Roles = $"{Constants.Role_User}, {Constants.Role_Admin}, {Constants.Role_OrganizationAdmin}")]
[Authorize(Roles = $"{Constants.Role_User}")]
public IActionResult GetInfoById([FromRoute] Guid id)
{
_logger.LogInformation("Handling request {requestName}", nameof(GetInfoById));

//by default, all users possess the user role. Therefore, organizational authorization checks are omitted here, allowing org admins to access information for all opportunities without restriction
var result = _opportunityInfoService.GetById(id, false);

_logger.LogInformation("Request {requestName} handled", nameof(GetInfoById));

return StatusCode((int)HttpStatusCode.OK, result);
}

[SwaggerOperation(Summary = "Get sharing details for the specified opportunity by id (User, Admin or Organization Admin role required)")]
[HttpGet("{id}/auth/sharing")]
[ProducesResponseType(typeof(OpportunityInfo), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[Authorize(Roles = $"{Constants.Role_User}, {Constants.Role_Admin}, {Constants.Role_OrganizationAdmin}")]
public async Task<IActionResult> GetSharingDetailsAuthed([FromRoute] Guid id, [FromQuery] bool? includeQRCode)
{
_logger.LogInformation("Handling request {requestName}", nameof(GetSharingDetailsAuthed));

//by default, all users possess the user role. Therefore, organizational authorization checks are omitted here, allowing org admins to access information for all opportunities without restriction
var result = await _opportunityService.GetSharingDetails(id, false, includeQRCode);

_logger.LogInformation("Request {requestName} handled", nameof(GetSharingDetailsAuthed));

return StatusCode((int)HttpStatusCode.OK, result);
}
#endregion
#endregion Authenticated User Based Actions

#region Administrative Actions
[SwaggerOperation(Summary = "Return a list of categories associated with opportunities, optionally filter by organization")]
Expand Down Expand Up @@ -620,6 +603,20 @@ public IActionResult ExportToCsvOpportunitySearch([FromBody] OpportunitySearchFi
return File(bytes, "text/csv", fileName);
}

[SwaggerOperation(Summary = "Create an instant-verify link for the specified opportunity")]
[HttpPost("{id}/link/instantVerify")]
[ProducesResponseType(typeof(LinkInfo), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[Authorize(Roles = $"{Constants.Role_Admin}, {Constants.Role_OrganizationAdmin}")]
public async Task<IActionResult> CreateInstantVerifyLink([FromRoute] Guid id, [FromBody] OpportunityRequestLinkInstantVerify request)
{
_logger.LogInformation("Handling request {requestName}", nameof(CreateInstantVerifyLink));

var result = await _opportunityService.CreateLinkInstantVerify(id, request, true);
_logger.LogInformation("Request {requestName} handled", nameof(CreateInstantVerifyLink));

return StatusCode((int)HttpStatusCode.OK, result);
}
#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ public OrganizationController(
#region Public Members

#region Authenticated User Based Actions
[SwaggerOperation(Summary = "Create a new organization (User, Admin or Organization Admin role required)")]
[SwaggerOperation(Summary = "Create a new organization (Authenticated User)")]
[HttpPost()]
[ProducesResponseType(typeof(Organization), (int)HttpStatusCode.OK)]
[Authorize(Roles = $"{Constants.Role_User}, {Constants.Role_Admin}, {Constants.Role_OrganizationAdmin}")]
[Authorize(Roles = $"{Constants.Role_User}")]
public async Task<IActionResult> Create([FromForm] OrganizationRequestCreate request)
{
_logger.LogInformation("Handling request {requestName}", nameof(Create));
Expand All @@ -58,10 +58,10 @@ public async Task<IActionResult> Create([FromForm] OrganizationRequestCreate req
return StatusCode((int)HttpStatusCode.OK, result);
}

[SwaggerOperation(Summary = "Return a list of provider types (User, Admin or Organization Admin role required)")]
[SwaggerOperation(Summary = "Return a list of provider types (Authenticated User)")]
[HttpGet("lookup/providerType")]
[ProducesResponseType(typeof(List<Domain.Entity.Models.Lookups.OrganizationProviderType>), (int)HttpStatusCode.OK)]
[Authorize(Roles = $"{Constants.Role_User}, {Constants.Role_Admin}, {Constants.Role_OrganizationAdmin}")]
[Authorize(Roles = $"{Constants.Role_User}")]
public IActionResult ListProviderTypes()
{
_logger.LogInformation("Handling request {requestName}", nameof(ListProviderTypes));
Expand All @@ -72,7 +72,7 @@ public IActionResult ListProviderTypes()

return StatusCode((int)HttpStatusCode.OK, result);
}
#endregion
#endregion Authenticated User Based Actions

#region Administrative Actions
[SwaggerOperation(Summary = "Get the specified organization by id")]
Expand Down
21 changes: 21 additions & 0 deletions src/api/src/domain/Yoma.Core.Domain/ActionLink/Enumerations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Yoma.Core.Domain.ActionLink
{
public enum LinkEntityType
{
Opportunity
}

public enum LinkAction
{
Share,
Verify
}

public enum LinkStatus
{
Active,
Inactive,
Expired,
LimitReached
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Yoma.Core.Domain.ActionLink.Models;
using Yoma.Core.Domain.Core.Helpers;

namespace Yoma.Core.Domain.ActionLink.Extensions
{
public static class LinkExtensions
{
public static LinkInfo ToLinkInfo(this Link value, bool? includeQRCode)
{
ArgumentNullException.ThrowIfNull(value, nameof(value));

return new LinkInfo
{
Id = value.Id,
Name = value.Name,
Description = value.Description,
StatusId = value.StatusId,
Status = value.Status,
URL = value.URL,
ShortURL = value.ShortURL,
QRCodeBase64 = includeQRCode == true ? QRCodeHelper.GenerateQRCodeBase64(value.ShortURL) : null,
UsagesLimit = value.UsagesLimit,
UsagesTotal = value.UsagesTotal,
UsagesAvailable = value.UsagesLimit.HasValue ? value.UsagesLimit - (value.UsagesTotal ?? 0) : null,
DateEnd = value.DateEnd,
DateCreated = value.DateCreated,
DateModified = value.DateModified
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Yoma.Core.Domain.ActionLink.Models;

namespace Yoma.Core.Domain.ActionLink.Interfaces
{
public interface ILinkService
{
Task<Link> Create(LinkRequestCreate request, bool ensureOrganizationAuthorization);

Task<Link> LogUsage(Guid id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Yoma.Core.Domain.ActionLink.Interfaces
{
public interface ILinkStatusService
{
Models.Lookups.LinkStatus GetByName(string name);

Models.Lookups.LinkStatus? GetByNameOrNull(string name);

Models.Lookups.LinkStatus GetById(Guid id);

Models.Lookups.LinkStatus? GetByIdOrNull(Guid id);

List<Models.Lookups.LinkStatus> List();
}
}
39 changes: 39 additions & 0 deletions src/api/src/domain/Yoma.Core.Domain/ActionLink/Models/Link.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Yoma.Core.Domain.ActionLink.Models
{
public class Link
{
public Guid Id { get; set; }

public string Name { get; set; }

public string? Description { get; set; }

public string EntityType { get; set; }

public string Action { get; set; }

public Guid StatusId { get; set; }

public LinkStatus Status { get; set; }

public Guid? OpportunityId { get; set; }

public string URL { get; set; }

public string ShortURL { get; set; }

public int? UsagesLimit { get; set; }

public int? UsagesTotal { get; set; }

public DateTimeOffset? DateEnd { get; set; }

public DateTimeOffset DateCreated { get; set; }

public Guid CreatedByUserId { get; set; }

public DateTimeOffset DateModified { get; set; }

public Guid ModifiedByUserId { get; set; }
}
}
33 changes: 33 additions & 0 deletions src/api/src/domain/Yoma.Core.Domain/ActionLink/Models/LinkInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Yoma.Core.Domain.ActionLink.Models
{
public class LinkInfo
{
public Guid Id { get; set; }

public string Name { get; set; }

public string? Description { get; set; }

public Guid StatusId { get; set; }

public LinkStatus Status { get; set; }

public string URL { get; set; }

public string ShortURL { get; set; }

public string? QRCodeBase64 { get; set; }

public int? UsagesLimit { get; set; }

public int? UsagesTotal { get; set; }

public int? UsagesAvailable { get; set; }

public DateTimeOffset? DateEnd { get; set; }

public DateTimeOffset DateCreated { get; set; }

public DateTimeOffset DateModified { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Yoma.Core.Domain.ActionLink.Models
{
public class LinkRequestCreate
{
public string Name { get; set; }

public string? Description { get; set; }

public LinkEntityType EntityType { get; set; }

public LinkAction Action { get; set; }

public Guid EntityId { get; set; }

public string URL { get; set; }

public int? UsagesLimit { get; set; }

public DateTimeOffset? DateEnd { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Yoma.Core.Domain.ActionLink.Models
{
public class LinkUsageLog
{
public Guid Id { get; set; }

public Guid LinkId { get; set; }

public Guid UserId { get; set; }

public DateTimeOffset DateCreated { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Yoma.Core.Domain.ActionLink.Models.Lookups
{
public class LinkStatus
{
public Guid Id { get; set; }

public string Name { get; set; }
}
}
Loading

0 comments on commit be17db2

Please sign in to comment.