59 Commits
Author SHA1 Message Date
hermes 3cc8514136 Added automatic audit feature 2026-09-22 06:09:51 +02:00
hermes 11311421ad Removed ValueTask generally where not needed
Removed relationships from audit log
2026-09-22 05:44:25 +02:00
hermes f4024d563a Implemented centralised audit logging 2026-09-21 16:32:56 +02:00
hermes 78b03b6996 Colocated entity type configurations with entities
Added tenant entities and models
Modified db context
2026-09-21 09:34:49 +02:00
hermes 2d0f2b494a Test 2 reoslutions 2026-09-20 15:25:53 +02:00
hermes d428f6e17c Remediated test #1 results 2026-09-20 13:51:01 +02:00
hermes 5829bffaa0 Refactored UI endpoint usage 2026-09-20 08:26:06 +02:00
hermes b703b0c64d Added service cache feature flag
Updated scheduler and moved to its own dedicated database
2026-09-18 16:11:14 +02:00
hermes 6d554d19a7 Implemented featureflag for service layer caching 2026-09-18 14:58:42 +02:00
hermes ec777fabf8 Implemented session and account lockout management
Enabled account sync control and session expiration management
Applied some fixes to the govername page and project page
2026-09-18 10:19:24 +02:00
hermes 41bf9b63bf Added dist/assets/ to gitignore 2026-09-17 07:38:52 +02:00
hermes 940630f0ed Untracking dist files 2026-09-17 07:38:16 +02:00
hermes 6de3d30a8b Removed package.json 2026-09-17 07:23:18 +02:00
hermes 831d57a8a0 Refactored UI and compose file 2026-09-17 07:07:05 +02:00
hermes 2d74f7f1bd Removed docker debug code 2026-09-15 12:13:16 +02:00
hermes ebf35d7f12 Refactored ApiVersioning service registration 2026-09-15 12:09:10 +02:00
hermes 77307f4bf0 Added OPTIONS header handling 2026-09-15 08:51:48 +02:00
hermes 464c21e41d Added RequireCors() to RouteGroup 2026-09-15 08:44:43 +02:00
hermes 364fe65e0d Moved .RequireAuthentication() to endpoints, removed it from EndpointGroup 2026-09-15 08:37:08 +02:00
hermes b239c402fa Applied configurable CORS policy to API 2026-09-15 07:29:58 +02:00
hermes f4a1064003 Fixed UI 2026-09-14 11:43:09 +02:00
hermes 5f18478675 Debug refactor 2026-09-14 10:46:21 +02:00
hermes e71ba99f9c Debugs refactor 2026-09-14 10:38:25 +02:00
hermes 8de761e4e6 Fixed base image .net versions 2026-09-14 10:34:09 +02:00
hermes ca6d04d528 Debug refactor 2026-09-14 10:33:18 +02:00
hermes f607eaf152 Rebuilt pfm-api dockerfile 2026-09-14 10:28:48 +02:00
hermes b631c6aa4b Light refactor 2026-09-14 10:22:56 +02:00
hermes cb59635882 Simplified fpm-api dockerfile 2026-09-14 10:20:42 +02:00
hermes 1cf019b3b9 Added debug code 2026-09-14 10:13:31 +02:00
hermes 0a4108d345 Changed pfm-api dockerfile paths 2026-09-14 10:10:12 +02:00
hermes afaad43c47 Simplified dockerfile (pfm-api) 2026-09-14 10:03:45 +02:00
hermes bccdab2ba8 Refactored compose file to assign same context to both services 2026-09-14 10:01:19 +02:00
hermes 5bdd2a5786 Fixed missing file copy 2026-09-14 09:50:03 +02:00
hermes 1acdb0ea7b Debug refactor on dockerfile 2026-09-14 09:46:34 +02:00
hermes 2047e440d8 Added better dotnet build handling 2026-09-14 09:41:47 +02:00
hermes 1771d74b43 Added more verbosity to dotnet build 2026-09-14 09:35:03 +02:00
hermes bad91e8090 Added .dockerignore 2026-09-14 09:25:46 +02:00
hermes f9c0c5fe16 Added stream redirection to dotnet commands 2026-09-14 09:13:46 +02:00
hermes 43c777dfe2 Commented out pfm-console to see if the api image will build 2026-09-14 09:06:15 +02:00
hermes 5dba8d3dd4 Removed stream redirection 2026-09-13 18:15:16 +02:00
hermes 0d78affd3c Fixed Api docker file paths 2026-09-13 18:12:49 +02:00
hermes 4bac2f58c2 Added debug code to dotnet build and publish lines 2026-09-13 18:09:38 +02:00
hermes 2a8d72963d Moved api dockerfile to project folder 2026-09-13 18:07:32 +02:00
hermes 4b658bdfd5 Corrected compose service contexts 2026-09-13 18:01:31 +02:00
hermes 9945b773db Fixed dockerfile paths 2026-09-13 17:55:57 +02:00
hermes f98b102a2e Light refactor 2026-09-13 17:51:25 +02:00
hermes f29ac65d7c Refactored API dockerfile 2026-09-13 17:50:08 +02:00
hermes 8450db51be Stable API run 2026-09-13 17:39:29 +02:00
hermes 5c1e18a9a9 Added debug term 2026-09-12 12:22:24 +02:00
hermes 4f1b05bf33 Second fix 2026-09-12 12:19:15 +02:00
hermes 3db6546a2b Light refactor 2026-09-12 12:13:52 +02:00
hermes e270bf8db2 FROM fix 2026-09-12 12:09:58 +02:00
hermes 48dd9aaa1d Light refactor 2026-09-12 12:08:55 +02:00
hermes 3fa9f257ba Rewrite pfm-api docker file 2026-09-12 12:04:23 +02:00
hermes 12663a588f Fixed publish alias 2026-09-12 11:57:58 +02:00
hermes 4738a20f80 Added error output to dotnet build line 2026-09-12 11:54:36 +02:00
hermes 7446659c0c Added working directory switch 2026-09-12 11:49:26 +02:00
hermes e0b1b00f63 PFM Api docker refactor 2026-09-12 11:45:07 +02:00
hermes 1c1e2b9038 Implemented missing file handling endpoints
Added contract update endpoint
2026-09-12 11:26:04 +02:00
273 changed files with 44961 additions and 13481 deletions
+21
View File
@@ -0,0 +1,21 @@
# Exclude build output and caches
**/bin/
**/obj/
**/out/
.vs/
.vscode/
# Exclude git internal files
.git/
.gitignore
# Exclude local environment/secret files
.env
*.env
*.user
*.suo
# Exclude node modules and temporary logs
**/node_modules/
**/dist/
*.log
+12 -3
View File
@@ -11,6 +11,7 @@
*.sln.docstates *.sln.docstates
*.env *.env
*.env.json *.env.json
certificates/
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs
@@ -51,6 +52,8 @@ bld/
# Build results on 'Bin' directories # Build results on 'Bin' directories
**/[Bb]in/* **/[Bb]in/*
dist/assets/
dist/
# Uncomment if you have tasks that rely on *.refresh files to move binaries # Uncomment if you have tasks that rely on *.refresh files to move binaries
# (https://github.com/github/gitignore/pull/3736) # (https://github.com/github/gitignore/pull/3736)
#!**/[Bb]in/*.refresh #!**/[Bb]in/*.refresh
@@ -58,8 +61,7 @@ bld/
# Visual Studio 2015/2017 cache/options directory # Visual Studio 2015/2017 cache/options directory
.vs/ .vs/
.vscode/ .vscode/
bin/
certificates/
# Uncomment if you have tasks that create the project's static files in wwwroot # Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/ #wwwroot/
@@ -347,6 +349,7 @@ _Pvt_Extensions
# Paket dependency manager # Paket dependency manager
**/.paket/paket.exe **/.paket/paket.exe
**/.paket/paket.exe
paket-files/ paket-files/
# FAKE - F# Make # FAKE - F# Make
@@ -431,4 +434,10 @@ FodyWeavers.xsd
*.msi *.msi
*.msix *.msix
*.msm *.msm
*.msp *.msp
/PostFundManagement.ConsoleUI/dist/config.json
/PostFundManagement.ConsoleUI/dist/env-config.js
/PostFundManagement.ConsoleUI/dist/assets/index-BaOzkVJi.css
/PostFundManagement.ConsoleUI/dist/assets/index-BU8qSe7k.js
/PostFundManagement.ConsoleUI/dist/favicon.svg
/PostFundManagement.ConsoleUI/dist/index.html
-28
View File
@@ -1,28 +0,0 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM node:22-bookworm-slim AS node
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG TARGETARCH
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
NUGET_XMLDOC_MODE=skip
WORKDIR /app_source
COPY . .
RUN dotnet restore "PostFundManagement.slnx"
WORKDIR "/app_source/PostFundManagement.Api"
RUN dotnet build "PostFundManagement.Api.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "PostFundManagement.Api.csproj" -c Release --no-restore -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
ENTRYPOINT ["dotnet", "PostFundManagement.Api.dll"]
+36
View File
@@ -0,0 +1,36 @@
# Stage 1: Runtime base
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
# Stage 2: Build & Restore
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy csproj files for layer caching
COPY ["PostFundManagement.Api/PostFundManagement.Api.csproj", "PostFundManagement.Api/"]
COPY ["PostFundManagement.Application/PostFundManagement.Application.csproj", "PostFundManagement.Application/"]
COPY ["PostFundManagement.Domain/PostFundManagement.Domain.csproj", "PostFundManagement.Domain/"]
COPY ["PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj", "PostFundManagement.Infrastructure/"]
# Restore packages
RUN dotnet restore "PostFundManagement.Api/PostFundManagement.Api.csproj"
# Copy full source code
COPY . .
# Build from the project folder using relative filename
WORKDIR "/src/PostFundManagement.Api"
RUN dotnet build "PostFundManagement.Api.csproj" -c Release -o /app/build
# Stage 3: Publish
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "PostFundManagement.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Stage 4: Final Runtime
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "PostFundManagement.Api.dll"]
@@ -23,6 +23,7 @@ public sealed class GetApplicantInviteEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Gets an applicant's invite") .WithDescription("Gets an applicant's invite")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetApplicantInviteEndpoint).ToEndpointName()) .WithName(typeof(GetApplicantInviteEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Invite>(StatusCodes.Status200OK) .Produces<Invite>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class GetApplicantInvitesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Gets an applicant's invite list") .WithDescription("Gets an applicant's invite list")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetApplicantInvitesEndpoint).ToEndpointName()) .WithName(typeof(GetApplicantInvitesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Invite[]>(StatusCodes.Status200OK) .Produces<Invite[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class GetInvitesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of sent invitations") .WithDescription("Get a list of sent invitations")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetInvitesEndpoint).ToEndpointName()) .WithName(typeof(GetInvitesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Invite[]>(StatusCodes.Status200OK) .Produces<Invite[]>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public sealed class SendInviteEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)") .WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(SendInviteEndpoint).ToEndpointName()) .WithName(typeof(SendInviteEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Invite>(StatusCodes.Status200OK) .Produces<Invite>(StatusCodes.Status200OK)
@@ -34,9 +34,9 @@ public class CreateEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}; };
}) })
.RequireAuthorization()
.DisableAntiforgery()
.WithDescription("Upload supporting evidence file to S3 and register metadata (M14)") .WithDescription("Upload supporting evidence file to S3 and register metadata (M14)")
.DisableAntiforgery()
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateEvidenceEndpoint).ToEndpointName()) .WithName(typeof(CreateEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class CreateIndicatorEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Define indicators and baselines/targets (M08)") .WithDescription("Define indicators and baselines/targets (M08)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName()) .WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class CreateMilestoneEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Define contractual milestones (M07)") .WithDescription("Define contractual milestones (M07)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateMilestoneRequest).ToEndpointName()) .WithName(typeof(CreateMilestoneRequest).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class GetEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a specific evidence record and assiciated file URLs") .WithDescription("Get a specific evidence record and assiciated file URLs")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetEvidenceEndpoint).ToEndpointName()) .WithName(typeof(GetEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Evidence>(StatusCodes.Status200OK) .Produces<Domain.Models.Evidence>(StatusCodes.Status200OK)
@@ -21,6 +21,7 @@ public class GetEvidenceFilesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of all uploaded evidence files metadata") .WithDescription("Get a list of all uploaded evidence files metadata")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName()) .WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK) .Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
@@ -0,0 +1,37 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence;
[ApiVersionTarget(1)]
public sealed class GetEvidenceUrlEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/evidence/{evidenceId}/securelink", async (string evidenceId, IEvidenceService service,
CancellationToken cancellationToken = default) =>
await service.GetEvidenceUrlAsync(evidenceIdHash: evidenceId, cancellationToken) switch
{
{ IsSuccess: true, Value: var evidenceUrl } => Results.Ok(evidenceUrl),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a secure download URL of the evidence file, URL expires (default: 15min)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetEvidenceUrlEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK)
.Produces<string>(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -24,6 +24,7 @@ public sealed class GetIndicatorEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a specific indicator.") .WithDescription("Get a specific indicator.")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetIndicatorEndpoint).ToEndpointName()) .WithName(typeof(GetIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Indicator>(StatusCodes.Status200OK) .Produces<Indicator>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetIndicatorsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Get a list of indicators") .WithDescription("Get a list of indicators")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName()) .WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Indicator[]>(StatusCodes.Status200OK) .Produces<Indicator[]>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetMilestoneEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of all uploaded evidence metadata for a specific mliestone") .WithDescription("Get a list of all uploaded evidence metadata for a specific mliestone")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetMilestoneEvidenceEndpoint).ToEndpointName()) .WithName(typeof(GetMilestoneEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK) .Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetProjectEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Get a list of all uploaded evidence metadata for a specific project") .WithDescription("Get a list of all uploaded evidence metadata for a specific project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectEvidenceEndpoint).ToEndpointName()) .WithName(typeof(GetProjectEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK) .Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class RecordIndicatorAmountEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Record actual values for indicators (M08)") .WithDescription("Record actual values for indicators (M08)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName()) .WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,8 @@ public sealed class UpdateEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Updates a specific evidence record metadata") .WithDescription("Updates a specific evidence record metadata")
.RequireAuthorization(Constants.MainScope)
.DisableAntiforgery()
.WithName(typeof(UpdateEvidenceEndpoint).ToEndpointName()) .WithName(typeof(UpdateEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class ApproveDisbursementEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Approve disbursement and release payment (M06)") .WithDescription("Approve disbursement and release payment (M06)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName()) .WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class CreateDisbursementEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Create a new disbursement schedule (M06)") .WithDescription("Create a new disbursement schedule (M06)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName()) .WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class CreateRiskEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Add risk to risk register (M09)") .WithDescription("Add risk to risk register (M09)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateRiskEndpoint).ToEndpointName()) .WithName(typeof(CreateRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public sealed class GetDisbursementEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a specific disbursement") .WithDescription("Get a specific disbursement")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetDisbursementEndpoint).ToEndpointName()) .WithName(typeof(GetDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Disbursement>(StatusCodes.Status200OK) .Produces<Disbursement>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetDisbursementsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of disbursements") .WithDescription("Get a list of disbursements")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName()) .WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Disbursement[]>(StatusCodes.Status200OK) .Produces<Disbursement[]>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public sealed class GetRiskEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a specific risk") .WithDescription("Get a specific risk")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetRiskEndpoint).ToEndpointName()) .WithName(typeof(GetRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Risk>(StatusCodes.Status200OK) .Produces<Risk>(StatusCodes.Status200OK)
@@ -21,6 +21,7 @@ public class GetRisksEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of all risks") .WithDescription("Get a list of all risks")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetRisksEndpoint).ToEndpointName()) .WithName(typeof(GetRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class UpdateDisbursementEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update the details of a specific disbursement") .WithDescription("Update the details of a specific disbursement")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateDisbursementEndpoint).ToEndpointName()) .WithName(typeof(UpdateDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class UpdateRiskEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update the details of a specific risk") .WithDescription("Update the details of a specific risk")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateRiskEndpoint).ToEndpointName()) .WithName(typeof(UpdateRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class CreateOrganisationEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Create a new organization profile") .WithDescription("Create a new organization profile")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName()) .WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -25,6 +25,7 @@ public sealed class CreatePortfolioEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Create a new organization portfolio") .WithDescription("Create a new organization portfolio")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreatePortfolioEndpoint).ToEndpointName()) .WithName(typeof(CreatePortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -21,6 +21,7 @@ public class DashboardEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get executive portfolio dashboard metrics and KPIs") .WithDescription("Get executive portfolio dashboard metrics and KPIs")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(DashboardEndpoint).ToEndpointName()) .WithName(typeof(DashboardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Dashboard>(StatusCodes.Status200OK) .Produces<Dashboard>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class GetInstrumentsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Get all funding instruments") .WithDescription("Get all funding instruments")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName()) .WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Instrument[]>(StatusCodes.Status200OK) .Produces<Instrument[]>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class GetOrganisationEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get organization profile by ID") .WithDescription("Get organization profile by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetOrganisationEndpoint).ToEndpointName()) .WithName(typeof(GetOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Organisation>(StatusCodes.Status200OK) .Produces<Organisation>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetOrganisationsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a paginated directory of organizations") .WithDescription("Get a paginated directory of organizations")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName()) .WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Organisation[]>(StatusCodes.Status200OK) .Produces<Organisation[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class GetPortfolioEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a specific portfolios") .WithDescription("Get a specific portfolios")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetPortfolioEndpoint).ToEndpointName()) .WithName(typeof(GetPortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Portfolio[]>(StatusCodes.Status200OK) .Produces<Portfolio[]>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public sealed class GetPortfolioProgrammesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a programme's portfolios") .WithDescription("Get a programme's portfolios")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetPortfolioProgrammesEndpoint).ToEndpointName()) .WithName(typeof(GetPortfolioProgrammesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK) .Produces<Programme[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class GetPortfoliosEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get all portfolios") .WithDescription("Get all portfolios")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName()) .WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Portfolio[]>(StatusCodes.Status200OK) .Produces<Portfolio[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class GetProgrammesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get all programmes") .WithDescription("Get all programmes")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName()) .WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK) .Produces<Programme[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update an existing organization profile") .WithDescription("Update an existing organization profile")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateOrganisationEndpoint).ToEndpointName()) .WithName(typeof(UpdateOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -25,6 +25,7 @@ public sealed class UpdatePortfolioEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update an existing organization portfolio") .WithDescription("Update an existing organization portfolio")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdatePortfolioEndpoint).ToEndpointName()) .WithName(typeof(UpdatePortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -25,6 +25,7 @@ public class CreateAwardEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Register an approved funding award (M01)") .WithDescription("Register an approved funding award (M01)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateAwardEndpoint).ToEndpointName()) .WithName(typeof(CreateAwardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -25,6 +25,7 @@ public class CreateBudgetEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Create a new budget line (M05)") .WithDescription("Create a new budget line (M05)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName()) .WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class CreateContractEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Create a contract record from an approved award (M02)") .WithDescription("Create a contract record from an approved award (M02)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateContractEndpoint).ToEndpointName()) .WithName(typeof(CreateContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class ExecuteContractEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Execute the contract and lock the contractual version") .WithDescription("Execute the contract and lock the contractual version")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName()) .WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class GetAwardEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get award by ID") .WithDescription("Get award by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetAwardEndpoint).ToEndpointName()) .WithName(typeof(GetAwardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Award>(StatusCodes.Status200OK) .Produces<Award>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetAwardsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of awards") .WithDescription("Get a list of awards")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetAwardsEndpoint).ToEndpointName()) .WithName(typeof(GetAwardsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Award[]>(StatusCodes.Status200OK) .Produces<Award[]>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class GetBudgetEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get budget by ID") .WithDescription("Get budget by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetBudgetEndpoint).ToEndpointName()) .WithName(typeof(GetBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Budget>(StatusCodes.Status200OK) .Produces<Budget>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetBudgetsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get all budgets") .WithDescription("Get all budgets")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName()) .WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Budget[]>(StatusCodes.Status200OK) .Produces<Budget[]>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class GetContractEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get contract by ID") .WithDescription("Get contract by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetContractEndpoint).ToEndpointName()) .WithName(typeof(GetContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Contract>(StatusCodes.Status200OK) .Produces<Contract>(StatusCodes.Status200OK)
@@ -0,0 +1,37 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public sealed class GetContractUrlEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/contracts/{contractId}/securelink", async (string contractId, IGrantsService service,
CancellationToken cancellationToken = default) =>
await service.GetContractUrlAsync(contractId, cancellationToken) switch
{
{ IsSuccess: true, Value: var contractUrl} => Results.Ok(contractUrl),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a secure download URL, has expiry period (default 15min)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetContractUrlEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -22,6 +22,7 @@ public class GetContractsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get all contracts") .WithDescription("Get all contracts")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetContractsEndpoint).ToEndpointName()) .WithName(typeof(GetContractsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Contract[]>(StatusCodes.Status200OK) .Produces<Contract[]>(StatusCodes.Status200OK)
@@ -0,0 +1,39 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public sealed class UpdateContractEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/grants/contracts", async (UpdateContractRequest request, IGrantsService service,
CancellationToken cancellationToken = default) =>
await service.UpdateContractAsync(request, cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Updates a specific contract")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -0,0 +1,48 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public sealed class UploadContractEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/contracts/{contractId}", async (string contractId, HttpRequest request, IGrantsService service,
CancellationToken cancellationToken = default) =>
{
if (!request.HasFormContentType)
return Results.BadRequest("Request must be a multipart form.");
var form = await request.ReadFormAsync(cancellationToken);
var file = form.Files.GetFile("file");
if (file == null || file.Length == 0)
return Results.BadRequest("No file uploaded or file is empty.");
return await service.UploadContractAsync(contractId, request.HttpContext.GetUserId(), file, cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
};
})
.WithDescription("Uploads a contract file securely")
.RequireAuthorization(Constants.MainScope)
.DisableAntiforgery()
.WithName(typeof(UploadContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -23,6 +23,7 @@ public class CreateActivityEndpoint : IEndpoint
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()), var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.RequireAuthorization(Constants.MainScope)
.WithDescription("Add a new activity under project (M04)") .WithDescription("Add a new activity under project (M04)")
.WithName(typeof(CreateActivityEndpoint).ToEndpointName()) .WithName(typeof(CreateActivityEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
@@ -25,6 +25,7 @@ public sealed class CreateProgrammeEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Create programme") .WithDescription("Create programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateProgrammeEndpoint).ToEndpointName()) .WithName(typeof(CreateProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -25,6 +25,7 @@ public class CreateProjectEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Create a new project (M04)") .WithDescription("Create a new project (M04)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateProjectEndpoint).ToEndpointName()) .WithName(typeof(CreateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetActivitiesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of activities for a project") .WithDescription("Get a list of activities for a project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName()) .WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Activity[]>(StatusCodes.Status200OK) .Produces<Domain.Models.Activity[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class GetActivityEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Gets a specific activity by it's id") .WithDescription("Gets a specific activity by it's id")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetActivityEndpoint).ToEndpointName()) .WithName(typeof(GetActivityEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Activity>(StatusCodes.Status200OK) .Produces<Domain.Models.Activity>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class GetMilestoneEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Gets a specific milestone") .WithDescription("Gets a specific milestone")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetMilestoneEndpoint).ToEndpointName()) .WithName(typeof(GetMilestoneEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Milestone>(StatusCodes.Status200OK) .Produces<Milestone>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetMilestonesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of all milestones") .WithDescription("Get a list of all milestones")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName()) .WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Milestone[]>(StatusCodes.Status200OK) .Produces<Milestone[]>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public sealed class GetProgrammeEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get specific programme") .WithDescription("Get specific programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammeEndpoint).ToEndpointName()) .WithName(typeof(GetProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Programme>(StatusCodes.Status200OK) .Produces<Programme>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public sealed class GetProgrammeProjectsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Get a list of all projects under a programme") .WithDescription("Get a list of all projects under a programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammeProjectsEndpoint).ToEndpointName()) .WithName(typeof(GetProgrammeProjectsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Project[]>(StatusCodes.Status200OK) .Produces<Project[]>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public sealed class GetProgrammesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of all programmes") .WithDescription("Get a list of all programmes")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName("Projects")) .WithName(typeof(GetProgrammesEndpoint).ToEndpointName("Projects"))
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK) .Produces<Programme[]>(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class GetProjectEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get project by ID") .WithDescription("Get project by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectEndpoint).ToEndpointName()) .WithName(typeof(GetProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Project[]>(StatusCodes.Status200OK) .Produces<Project[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class GetProjectIndicatorsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get get a detailed list of project indicators") .WithDescription("Get get a detailed list of project indicators")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectIndicatorsEndpoint).ToEndpointName()) .WithName(typeof(GetProjectIndicatorsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Indicator[]>(StatusCodes.Status200OK) .Produces<Indicator[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class GetProjectMilestonesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of milestones for a project") .WithDescription("Get a list of milestones for a project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName()) .WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Milestone[]>(StatusCodes.Status200OK) .Produces<Milestone[]>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class GetProjectRisksEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of risks for a project") .WithDescription("Get a list of risks for a project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName()) .WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Risk[]>(StatusCodes.Status200OK) .Produces<Risk[]>(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public class GetProjectsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a list of projects") .WithDescription("Get a list of projects")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectsEndpoint).ToEndpointName()) .WithName(typeof(GetProjectsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Project[]>(StatusCodes.Status200OK) .Produces<Project[]>(StatusCodes.Status200OK)
@@ -21,6 +21,7 @@ public sealed class UpdateMilestoneEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update project milestone details") .WithDescription("Update project milestone details")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateMilestoneEndpoint).ToEndpointName()) .WithName(typeof(UpdateMilestoneEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public sealed class UpdateProgrammeEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update existing programme") .WithDescription("Update existing programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateProgrammeEndpoint).ToEndpointName()) .WithName(typeof(UpdateProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class UpdateProjectEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update project details") .WithDescription("Update project details")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName()) .WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -24,6 +24,7 @@ public class UpdateProjectStatusEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update project status (M04)") .WithDescription("Update project status (M04)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateProjectStatusEndpoint).ToEndpointName()) .WithName(typeof(UpdateProjectStatusEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -22,6 +22,7 @@ public sealed class EncodeIdEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Hashes an int64 identifier") .WithDescription("Hashes an int64 identifier")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(EncodeIdEndpoint).ToEndpointName()) .WithName(typeof(EncodeIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK) .Produces<string>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class GetUserEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Gets the metadata of a single user.") .WithDescription("Gets the metadata of a single user.")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetUserEndpoint).ToEndpointName()) .WithName(typeof(GetUserEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<User>(StatusCodes.Status200OK) .Produces<User>(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public class GetUsersEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting") .WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetUsersEndpoint).ToEndpointName()) .WithName(typeof(GetUsersEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<User[]>(StatusCodes.Status200OK) .Produces<User[]>(StatusCodes.Status200OK)
@@ -30,6 +30,7 @@ public class SyncUserEndpoint : IEndpoint
}; };
}) })
.WithDescription("Trigger integration and data synchronization in the background") .WithDescription("Trigger integration and data synchronization in the background")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(SyncUserEndpoint).ToEndpointName()) .WithName(typeof(SyncUserEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -23,6 +23,7 @@ public sealed class UpdateUserStatusEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Changes the Activity status of the user in the system") .WithDescription("Changes the Activity status of the user in the system")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateUserStatusEndpoint).ToEndpointName()) .WithName(typeof(UpdateUserStatusEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
@@ -25,8 +25,8 @@
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="IdentityModel" Version="6.2.0" /> <PackageReference Include="IdentityModel" Version="6.2.0" />
<PackageReference Include="KubernetesClient" Version="19.0.2" /> <PackageReference Include="KubernetesClient" Version="19.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.11" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.12" />
<Using Include="Microsoft.AspNetCore.Authentication" /> <Using Include="Microsoft.AspNetCore.Authentication" />
<Using Include="Microsoft.AspNetCore.Authentication.Cookies" /> <Using Include="Microsoft.AspNetCore.Authentication.Cookies" />
@@ -60,8 +60,8 @@
<!-- API Documentation --> <!-- API Documentation -->
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.2" /> <PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
<Using Include="Scalar.AspNetCore" /> <Using Include="Scalar.AspNetCore" />
<Using Include="Microsoft.OpenApi" /> <Using Include="Microsoft.OpenApi" />
+48 -14
View File
@@ -1,17 +1,35 @@
using Microsoft.FeatureManagement;
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Filters; using PostFundManagement.Application.Filters;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Health; using PostFundManagement.Domain.Health;
using PostFundManagement.Domain.Mediator; using PostFundManagement.Domain.Mediator;
using PostFundManagement.Infrastructure.Database.Services;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer() builder.Services.AddFeatureManagement();
.AddApiVersioning(options => options.ApiVersionReader = new HeaderApiVersionReader());
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("x-api-version"),
new MediaTypeApiVersionReader("version")
);
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
builder.Services.AddApiVersioning().AddApiExplorer();
builder.Services.AddScoped<ActiveUserFilter>(); builder.Services.AddScoped<ActiveUserFilter>();
builder.Services.AddTransient<ITenantContextService, TenantContextService>();
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly()); builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
builder.Services.AddApiServices(builder.Configuration); builder.Services.AddApiServices(builder.Configuration);
@@ -91,26 +109,33 @@ builder.Services.AddOpenApi(options =>
}); });
}); });
builder.Services.AddCors(options => options.AddPolicy("AllowFrontend", policy => var allowedOrigins = builder.Configuration.GetSection("CorsOrigins").Get<string[]>() ?? [];
policy.WithOrigins("https://localhost:3000", "https://localhost:8076", "https://console.ifri.khongisa.co.za")
.AllowAnyHeader() builder.Services.AddCors(options =>
.AllowAnyMethod() {
.AllowCredentials())); options.AddPolicy(CorsPolicy, policy =>
{
policy.WithOrigins(allowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build(); var app = builder.Build();
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>(); var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler(DefaultSchedulerName); var scheduler = await schedulerFactory.GetScheduler();
if (!scheduler!.IsStarted) if (scheduler!.Status != SchedulerStatus.Running)
await scheduler.Start(); await scheduler.Start();
app.UseHsts(); app.UseHsts();
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
app.UseCors("AllowFrontend"); app.UseCors(CorsPolicy);
app.UseOutputCache(); app.UseOutputCache();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
@@ -123,7 +148,16 @@ var versionGroups = new Dictionary<int, RouteGroupBuilder>
{ {
{ {
1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet) 1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet)
.RequireAuthorization("ApiScope").AddEndpointFilter<ActiveUserFilter>() .RequireCors(CorsPolicy)
.AddEndpointFilter(async (context, next) =>
{
if (HttpMethods.IsOptions(context.HttpContext.Request.Method))
return await next(context);
var filter = context.HttpContext.RequestServices.GetRequiredService<ActiveUserFilter>();
return await filter.InvokeAsync(context, next);
})
} }
}; };
+8 -15
View File
@@ -1,21 +1,14 @@
{ {
"EnableServiceCaching": true,
"CorsOrigins": [
"https://console.ifri.khongisa.co.za",
"https://localhost:7208"
],
"GeneralS3Settings": { "GeneralS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900", "ServiceUrl": "https://s3.garage.khongisa.co.za",
"Region": "garage", "Region": "garage",
"BucketName": "pfm.general", "BucketName": "pfm",
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za" "CdnBaseUrl": "https://pfm.cdn.khongisa.co.za"
},
"ContractS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "pfm.general",
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
},
"EvidenceS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "pfm.general",
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
}, },
"Monitoring": { "Monitoring": {
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
@@ -10,14 +10,55 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
protected abstract string BucketName { get; } protected abstract string BucketName { get; }
protected abstract string CdnBaseUrl { get; } protected abstract string CdnBaseUrl { get; }
public virtual async ValueTask<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) => public string GenerateCdnUrl(string fileKey) => $"{CdnBaseUrl}/{fileKey}";
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
public virtual async Task<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
if (string.IsNullOrWhiteSpace(BucketName)) if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail<string>(Domain.Errors.ValidationError.Create("Bucket name is not configured.")); return Result.Fail(ValidationError.Create("Bucket name is not configured."));
if (string.IsNullOrWhiteSpace(fileKey))
return Result.Fail(ValidationError.Create("File key is required."));
var request = new GetPreSignedUrlRequest
{
BucketName = BucketName,
Key = fileKey,
Expires = DateTime.UtcNow.Add(duration),
Verb = HttpVerb.GET
};
if (!string.IsNullOrWhiteSpace(contentType))
{
request.ResponseHeaderOverrides.ContentType = contentType;
request.ResponseHeaderOverrides.ContentDisposition = "inline";
}
var presignedUrl = await Client.GetPreSignedURLAsync(request);
return Result.FailIf(string.IsNullOrWhiteSpace(presignedUrl),
CreateError.Create("Failed to create presigned URL"))
.ToResult(presignedUrl);
}));
private static string CreateFileKey(string path, string fileName, string fileHash)
{
var cleanSubPath = path.Replace('\\', '/').Trim('/');
return string.IsNullOrWhiteSpace(cleanSubPath)
? $"{fileHash.ToLower(CultureInfo.InvariantCulture)}_{fileName}"
: $"{cleanSubPath}/{fileHash.ToLower(CultureInfo.InvariantCulture)}_{fileName}";
}
public virtual async Task<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail<string>(ValidationError.Create("Bucket name is not configured."));
if (string.IsNullOrWhiteSpace(CdnBaseUrl)) if (string.IsNullOrWhiteSpace(CdnBaseUrl))
return Result.Fail<string>(Domain.Errors.ValidationError.Create("CDN base URL is not configured.")); return Result.Fail<string>(ValidationError.Create("CDN base URL is not configured."));
await using var stream = new MemoryStream(); await using var stream = new MemoryStream();
@@ -31,7 +72,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
if (string.IsNullOrWhiteSpace(fileHash)) if (string.IsNullOrWhiteSpace(fileHash))
return Result.Fail<string>(CreateError.Create("Failed to compute file hash.")); return Result.Fail<string>(CreateError.Create("Failed to compute file hash."));
var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}"; var fileKey = CreateFileKey(subPath, fileName, fileHash);
var putRequest = new PutObjectRequest var putRequest = new PutObjectRequest
{ {
@@ -48,11 +89,11 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
return response.HttpStatusCode != HttpStatusCode.OK return response.HttpStatusCode != HttpStatusCode.OK
? Result.Fail<string>(CreateError.Create($"Failed to upload {fileName} to S3.")) ? Result.Fail<string>(CreateError.Create($"Failed to upload {fileName} to S3."))
: Result.Ok($"{CdnBaseUrl}/{fileKey}"); : Result.Ok(fileKey);
})); }));
public virtual async ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) => public virtual async Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
if (string.IsNullOrWhiteSpace(BucketName)) if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail(NotFoundError.Create("Bucket name is not configured.")); return Result.Fail(NotFoundError.Create("Bucket name is not configured."));
@@ -63,7 +104,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
Key = fileKey Key = fileKey
}, cancellationToken); }, cancellationToken);
return Result.FailIf(response.HttpStatusCode == HttpStatusCode.NoContent, return Result.FailIf(response.HttpStatusCode == HttpStatusCode.NoContent,
UpdateError.Create($"Failed to delete {fileKey} from S3.")); UpdateError.Create($"Failed to delete {fileKey} from S3."));
})); }));
} }
@@ -1,16 +1,18 @@
using PostFundManagement.Application.Services.Communications; using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface ICommunicationsService public interface ICommunicationsService
{ {
ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default); Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default); Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
} }
@@ -1,30 +1,38 @@
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IEvidenceService public interface IEvidenceService
{ {
ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default);
ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken); Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken);
ValueTask<Result<Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default); Task<Result<Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default);
ValueTask<Result<Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default); Task<Result<Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default);
Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default);
} }
@@ -1,26 +1,32 @@
using PostFundManagement.Application.Services.Execution; using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IExecutionService public interface IExecutionService
{ {
ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default); Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default); Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default); [Audit]
Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
} }
@@ -1,32 +1,37 @@
using PostFundManagement.Application.Services.Governance; using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IGovernanceService public interface IGovernanceService
{ {
ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default); Task<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default); Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default); Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default); Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default); [Audit]
Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default);
[Audit]
Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
[Audit]
Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
} }
@@ -1,28 +1,41 @@
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IGrantsService public interface IGrantsService
{ {
ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default); Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default); Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default); Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
[Audit]
Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
[Audit]
Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default);
} }
@@ -1,6 +1,9 @@
namespace PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Domain.Attributes;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IIdentityService public interface IIdentityService
{ {
ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default);
} }
@@ -1,46 +1,54 @@
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IProjectService public interface IProjectService
{ {
ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default); Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default); Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default); Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default); [Audit]
Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default); Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default); Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default); Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
} }
@@ -2,5 +2,5 @@
public interface IToolService public interface IToolService
{ {
ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken); Task<Result<string>> EncodeLong(long id, CancellationToken cancellationToken);
} }
@@ -1,16 +1,19 @@
using PostFundManagement.Application.Services.Users; using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IUserService public interface IUserService
{ {
ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default); [Audit]
Task<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default);
ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default); Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default); [Audit]
Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default);
ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken); Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken);
} }
@@ -2,43 +2,41 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Quartz; using PostFundManagement.Domain.Quartz;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Extensions; namespace PostFundManagement.Application.Extensions;
public static class Quartz public static class Quartz
{ {
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration) public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{ {
var connectionString = configuration.GetConnectionString(DatabaseConfigName); var connectionString = configuration.GetConnectionString(SchedulerDatabaseConfigName);
services.Configure<QuartzOptions>(options =>
{
options.Properties["quartz.scheduler.instanceName"] = schedulerName;
options.Properties["quartz.scheduler.instanceId"] = "AUTO";
});
services.ConfigureCommon(); services.ConfigureCommon();
services.AddQuartz(config => services.AddQuartz(config =>
{ {
config.SchedulerName = schedulerName;
config.SchedulerId = "AUTO";
config.UseSimpleTypeLoader(); config.UseSimpleTypeLoader();
config.UseDefaultThreadPool(options => options.MaxConcurrency = 0); config.UseDefaultThreadPool(options => options.MaxConcurrency = 0);
config.UseTimeZoneConverter(); config.UseTimeZoneConverter();
config.UsePersistentStore(storage => config.UsePersistentStore(storage =>
{ {
storage.PerformSchemaValidation = false; storage.ProvisionSchema();
storage.UsePostgres(options => options.ConnectionString = connectionString!);
storage.UseSystemTextJsonSerializer(); storage.UseSystemTextJsonSerializer();
storage.SetProperty("quartz.jobStore.clustered", "true");
storage.UsePostgres(options =>
{
options.ConnectionString = connectionString!;
options.TablePrefix = "quartz.qrtz_";
});
storage.UseClustering(cluster => storage.UseClustering(cluster =>
{ {
cluster.CheckinInterval = TimeSpan.FromSeconds(30); cluster.CheckinInterval = TimeSpan.FromSeconds(30);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90); cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
}); });
}); });
}); });
return services; return services;
@@ -46,38 +44,39 @@ public static class Quartz
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration) public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{ {
var connectionString = configuration.GetConnectionString(DatabaseConfigName); var connectionString = configuration.GetConnectionString(SchedulerDatabaseConfigName);
services.Configure<QuartzOptions>(options =>
{
options.Properties["quartz.scheduler.instanceName"] = schedulerName;
options.Properties["quartz.scheduler.instanceId"] = "AUTO";
options.Properties["quartz.scheduler.batchTriggerAcquisitionMaxCount"] = "5";
options.Properties["quartz.jobStore.misfireThreshold"] = TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
options.Properties["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
options.Properties["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
});
services.ConfigureCommon(); services.ConfigureCommon();
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});
services.AddQuartz(config => services.AddQuartz(config =>
{ {
config.SchedulerName = schedulerName;
config.SchedulerId = "AUTO";
config.InterruptJobsOnShutdown = true;
config.InterruptJobsOnShutdownWithWait = true;
config.MaxBatchSize = 5;
config.UseSimpleTypeLoader(); config.UseSimpleTypeLoader();
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1); config.UseDefaultThreadPool(options => options.MaxConcurrency = 5);
config.UseTimeZoneConverter(); config.UseTimeZoneConverter();
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
config.UsePersistentStore(storage => config.UsePersistentStore(storage =>
{ {
storage.PerformSchemaValidation = false; storage.ProvisionSchema();
storage.UsePostgres(options => options.ConnectionString = connectionString!);
storage.UseSystemTextJsonSerializer(); storage.UseSystemTextJsonSerializer();
storage.SetProperty("quartz.jobStore.clustered", "true");
storage.UsePostgres(options =>
{
options.ConnectionString = connectionString!;
options.TablePrefix = "quartz.qrtz_";
});
storage.UseClustering(cluster => storage.UseClustering(cluster =>
{ {
cluster.CheckinInterval = TimeSpan.FromSeconds(30); cluster.CheckinInterval = TimeSpan.FromSeconds(30);
@@ -91,18 +90,10 @@ public static class Quartz
private static IServiceCollection ConfigureCommon(this IServiceCollection services) private static IServiceCollection ConfigureCommon(this IServiceCollection services)
{ {
services.Configure<QuartzOptions>(options => services.Configure<QuartzOptions>(options => options.Scheduling.IgnoreDuplicates = true);
{
options.Scheduling.IgnoreDuplicates = true;
options.Scheduling.OverWriteExistingData = true;
options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
});
services.AddTransient<RetryJobListener>(); services.AddTransient<RetryJobListener>();
services.AddTransient<IJobOrchestrator, JobOrchestrator>(); services.AddTransient<IJobOrchestrator, JobOrchestrator>();
return services; return services;
} }
} }
@@ -25,40 +25,6 @@ public static class S3
services.AddKeyedScoped<IS3Service, GeneralS3Service>(GeneralBucketName); services.AddKeyedScoped<IS3Service, GeneralS3Service>(GeneralBucketName);
} }
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value))
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(EvidenceBucketName, (provider, client) =>
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{EvidenceS3SettingsSection}:AccessKey").Value,
configuration.GetSection($"{EvidenceS3SettingsSection}:SecretKey").Value),
new AmazonS3Config
{
ServiceURL = configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value,
AuthenticationRegion = configuration.GetSection($"{EvidenceS3SettingsSection}:Region").Value,
ForcePathStyle = true,
EndpointDiscoveryEnabled = true,
UseHttp = configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase),
}));
services.AddKeyedScoped<IS3Service, EvidenceS3Service>(EvidenceBucketName);
}
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value))
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(ContractBucketName, (provider, client) =>
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{ContractS3SettingsSection}:AccessKey").Value,
configuration.GetSection($"{ContractS3SettingsSection}:SecretKey").Value),
new AmazonS3Config
{
ServiceURL = configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value,
AuthenticationRegion = configuration.GetSection($"{ContractS3SettingsSection}:Region").Value,
ForcePathStyle = true,
EndpointDiscoveryEnabled = true,
UseHttp = configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase),
}));
services.AddKeyedScoped<IS3Service, ContractS3Service>(ContractBucketName);
}
return services; return services;
} }
} }
@@ -1,5 +1,6 @@
using PostFundManagement.Application.Services.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain.Security.Configuration; using PostFundManagement.Domain.Security.Configuration;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security.Sdk; using PostFundManagement.Domain.Security.Sdk;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
@@ -97,6 +98,7 @@ public static class Security
options.Scope.Clear(); options.Scope.Clear();
options.Scope.Add("openid"); options.Scope.Add("openid");
options.Scope.Add("profile"); options.Scope.Add("profile");
options.Scope.Add("role");
options.Scope.Add("email"); options.Scope.Add("email");
options.Events = new OpenIdConnectEvents options.Events = new OpenIdConnectEvents
@@ -149,7 +151,7 @@ public static class Security
}); });
services.AddAuthorizationBuilder() services.AddAuthorizationBuilder()
.AddPolicy("ApiScope", policy => .AddPolicy(Constants.MainScope, policy =>
{ {
policy.RequireAuthenticatedUser(); policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", authOptions.Audience!); policy.RequireClaim("scope", authOptions.Audience!);
@@ -1,5 +1,6 @@
using PostFundManagement.Application.Abstractions; using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Interceptors;
using PostFundManagement.Application.Services.Communications; using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Application.Services.Execution; using PostFundManagement.Application.Services.Execution;
@@ -15,32 +16,62 @@ namespace PostFundManagement.Application.Extensions;
public static class Services public static class Services
{ {
public static IServiceCollection AddAuditedCachedScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped<TCacheDecorator>();
services.AddScoped(sp =>
{
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var interceptor = sp.GetRequiredService<AuditInterceptor>();
var actual = sp.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached, interceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddAuditedScoped<TInterface, TImplementation>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped(sp =>
{
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var actual = sp.GetRequiredService<TImplementation>();
var interceptor = sp.GetRequiredService<AuditInterceptor>();
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(actual, interceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddScoped<ICommunicationsService, CommunicationsService>(); services.AddScoped<AuditInterceptor>();
services.Decorate<ICommunicationsService, CommunicationsCacheService>(); services.AddSingleton<ProxyGenerator>();
services.AddScoped<IEvidenceService, EvidenceService>(); services.AddAuditedCachedScoped<ICommunicationsService, CommunicationsService, CommunicationsCacheService>();
services.Decorate<IEvidenceService, EvidenceCacheService>(); services.AddAuditedCachedScoped<IEvidenceService, EvidenceService, EvidenceCacheService>();
services.AddAuditedCachedScoped<IUserService, UserService, UserCacheService>();
services.AddScoped<IUserService, UserService>(); services.AddAuditedCachedScoped<IExecutionService, ExecutionService, ExecutionCacheService>();
services.Decorate<IUserService, UserCacheService>(); services.AddAuditedCachedScoped<IGovernanceService, GovernanceService, GovernanceCacheService>();
services.AddAuditedCachedScoped<IGrantsService, GrantsService, GrantsCacheService>();
services.AddAuditedCachedScoped<IProjectService, ProjectService, ProjectCacheService>();
services.AddScoped<IIdentityService, IdentityService>(); services.AddScoped<IIdentityService, IdentityService>();
services.Decorate<IIdentityService, IdentityCacheService>(); services.Decorate<IIdentityService, IdentityCacheService>();
services.AddScoped<IExecutionService, ExecutionService>();
services.Decorate<IExecutionService, ExecutionCacheService>();
services.AddScoped<IGovernanceService, GovernanceService>();
services.Decorate<IGovernanceService, GovernanceCacheService>();
services.AddScoped<IGrantsService, GrantsService>();
services.Decorate<IGrantsService, GrantsCacheService>();
services.AddScoped<IProjectService, ProjectService>();
services.Decorate<IProjectService, ProjectCacheService>();
services.AddScoped<IToolService, ToolService>(); services.AddScoped<IToolService, ToolService>();
services.Decorate<IToolService, ToolCacheService>(); services.Decorate<IToolService, ToolCacheService>();
@@ -26,7 +26,7 @@ public sealed class ActiveUserFilter(IIdentityService identityService) : IEndpoi
if (!string.IsNullOrWhiteSpace(path) && !path!.EndsWith("integration/sync", StringComparison.InvariantCultureIgnoreCase)) if (!string.IsNullOrWhiteSpace(path) && !path!.EndsWith("integration/sync", StringComparison.InvariantCultureIgnoreCase))
if ((await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted)).IsFailed) if ((await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted)).IsFailed)
return (object?)Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden); return Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
context.HttpContext.Items[UserIdItemKey] = userId; context.HttpContext.Items[UserIdItemKey] = userId;
context.HttpContext.Items[EmailtemKey] = email; context.HttpContext.Items[EmailtemKey] = email;
@@ -0,0 +1,211 @@
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain.Attributes;
namespace PostFundManagement.Application.Interceptors;
public sealed class AuditInterceptor(AuditService auditService, IHttpContextAccessor httpContextAccessor) : IAsyncInterceptor
{
public void InterceptSynchronous(IInvocation invocation) => invocation.Proceed();
public void InterceptAsynchronous(IInvocation invocation) =>
invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
public void InterceptAsynchronous<TResult>(IInvocation invocation) =>
invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
private static async Task InternalInterceptAsynchronous(IInvocation invocation)
{
invocation.Proceed();
var task = (Task)invocation.ReturnValue;
await task.ConfigureAwait(false);
}
private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
{
invocation.Proceed();
var task = (Task<TResult>)invocation.ReturnValue;
var resultObj = await task.ConfigureAwait(false);
var method = invocation.MethodInvocationTarget ?? invocation.Method;
var auditAttr = method.GetCustomAttribute<AuditAttribute>()
?? invocation.Method.GetCustomAttribute<AuditAttribute>();
if (auditAttr == null) return resultObj;
if (resultObj is IResultBase result && result.IsSuccess)
{
try
{
var action = ResolveActionName(method.Name);
var entityName = ResolveEntityName(method, resultObj, invocation.Arguments);
var entityId = ResolveEntityId(resultObj, invocation.Arguments);
var changes = BuildPayloadSummary(invocation.Method.GetParameters(), invocation.Arguments);
var performedBy = ResolveUserId();
await auditService.LogActionAsync(
entityId: entityId,
entityName: entityName,
action: action,
changes: changes,
performedBy: performedBy).ConfigureAwait(false);
}
catch { /* eat all exceptions, don't interrupt program flow */ }
}
return resultObj;
}
private static string ResolveActionName(string methodName) =>
methodName.EndsWith("Async", StringComparison.Ordinal)
? methodName[..^5]
: methodName;
private static string ResolveEntityName(MethodInfo method, object? resultObj, object[] args)
{
var returnType = method.ReturnType;
Type? innerType = null;
if (returnType.IsGenericType)
{
var genericDef = returnType.GetGenericTypeDefinition();
if (genericDef == typeof(Task<>) || genericDef == typeof(Task<>))
innerType = returnType.GetGenericArguments()[0];
}
if (innerType != null && innerType.IsGenericType && typeof(IResultBase).IsAssignableFrom(innerType))
{
var valueType = innerType.GetGenericArguments().FirstOrDefault();
if (valueType != null && valueType != typeof(bool) && valueType != typeof(int) && valueType != typeof(string))
return valueType.Name;
}
foreach (var arg in args)
{
if (arg == null || arg is CancellationToken or IFormFile) continue;
var argTypeName = arg.GetType().Name;
if (argTypeName.EndsWith("Request", StringComparison.Ordinal))
{
var trimmed = argTypeName[..^7];
return trimmed.Replace("Create", "").Replace("Update", "");
}
}
return ResolveActionName(method.Name);
}
private static string ResolveEntityId(object? resultObj, object[] args)
{
if (resultObj != null)
{
var valueProp = resultObj.GetType().GetProperty("Value");
if (valueProp != null)
{
var value = valueProp.GetValue(resultObj);
if (value != null)
{
var idProp = value.GetType().GetProperty("Id") ?? value.GetType().GetProperty("id");
if (idProp != null)
{
var idVal = idProp.GetValue(value);
if (idVal != null) return idVal.ToString()!;
}
}
}
}
foreach (var arg in args)
{
if (arg is long or int or Guid or string)
return arg.ToString()!;
if (arg != null && arg is not CancellationToken and not IFormFile)
{
var idProp = arg.GetType().GetProperty("Id") ?? arg.GetType().GetProperty("id");
if (idProp != null)
{
var idVal = idProp.GetValue(arg);
if (idVal != null) return idVal.ToString()!;
}
}
}
return "N/A";
}
private static string[] BuildPayloadSummary(ParameterInfo[] parameters, object[] args)
{
var entries = new List<string>();
for (var i = 0; i < parameters.Length; i++)
{
var param = parameters[i];
var arg = args[i];
if (param.ParameterType == typeof(CancellationToken))
continue;
if (arg == null)
{
entries.Add($"{param.Name}: null");
continue;
}
if (arg is IFormFile file)
{
entries.Add($"{param.Name}: [File] Name='{file.FileName}', Size={file.Length} bytes, Type='{file.ContentType}'");
continue;
}
if (arg is IEnumerable<IFormFile> files)
{
var fileSummaries = files.Select(f => $"'{f.FileName}' ({f.Length} bytes)");
entries.Add($"{param.Name}: [Files] count={fileSummaries.Count()} [{string.Join(", ", fileSummaries)}]");
continue;
}
if (arg is Stream stream)
{
entries.Add($"{param.Name}: [Stream] Length={(stream.CanSeek ? stream.Length : -1)} bytes");
continue;
}
try
{
var json = JsonSerializer.Serialize(arg);
entries.Add($"{param.Name}: {json}");
}
catch
{
entries.Add($"{param.Name}: [Unserializable]");
}
}
return [.. entries];
}
private Guid ResolveUserId()
{
var user = httpContextAccessor.HttpContext?.User;
var sub = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? user?.FindFirst("sub")?.Value;
return Guid.TryParse(sub, out var guid) ? guid : Guid.Empty;
}
}
@@ -24,9 +24,10 @@
<!-- API Documentation --> <!-- API Documentation -->
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" /> <PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.11" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.2" /> <PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.12" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
<Using Include="Scalar.AspNetCore" /> <Using Include="Scalar.AspNetCore" />
<Using Include="Microsoft.OpenApi" /> <Using Include="Microsoft.OpenApi" />
@@ -36,6 +37,8 @@
<!-- Shared Usings --> <!-- Shared Usings -->
<ItemGroup> <ItemGroup>
<Using Include="Humanizer" /> <Using Include="Humanizer" />
<Using Include="Castle.DynamicProxy" />
<Using Include="Microsoft.FeatureManagement" />
<Using Include="System.ComponentModel.DataAnnotations" /> <Using Include="System.ComponentModel.DataAnnotations" />
<Using Include="Microsoft.Extensions.Caching.Distributed" /> <Using Include="Microsoft.Extensions.Caching.Distributed" />
<Using Include="Microsoft.Extensions.Caching.Hybrid" /> <Using Include="Microsoft.Extensions.Caching.Hybrid" />
@@ -4,14 +4,19 @@ using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Communications; namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache) : ServiceCacheBase, ICommunicationsService public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, ICommunicationsService
{ {
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => public async Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () => await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetApplicantInviteAsync(applicantUserId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getapplicantinvite-{applicantUserId}").Value; var cacheKey = HashService.ToMd5Hash($"getapplicantinvite-{applicantUserId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken); var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -28,14 +33,17 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached!.FromJson<Invite>()); return Result.Ok(cached!.FromJson<Invite>());
})); }));
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => public async Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () => await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getapplicantinvites-{applicantUserId}").Value; var cacheKey = HashService.ToMd5Hash($"getapplicantinvites-{applicantUserId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken); var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached)) if (string.IsNullOrWhiteSpace(cached))
{ {
var result = await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken); var result = await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken);
@@ -49,9 +57,12 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached.FromJson<Invite[]>()); return Result.Ok(cached.FromJson<Invite[]>());
})); }));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () => await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetInvitesAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getinvites-{pagination.Page}-{pagination.PageSize}").Value; var cacheKey = HashService.ToMd5Hash($"getinvites-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken); var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -69,7 +80,7 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached.FromJson<Invite[]>()); return Result.Ok(cached.FromJson<Invite[]>());
})); }));
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.SendInviteAsync(request, userId, cancellationToken); await service.SendInviteAsync(request, userId, cancellationToken);
} }
@@ -8,9 +8,9 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Communications; namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : ICommunicationsService public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : ICommunicationsService
{ {
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () => await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -42,8 +42,8 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
.ToResult(invite.Map()); .ToResult(invite.Map());
})); }));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () => await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -56,8 +56,8 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
return Result.Ok(invites.Select(i => i.Map()).ToArray()); return Result.Ok(invites.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) => public async Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () => await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -67,8 +67,8 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
.ToResult(invite!.Map()); .ToResult(invite!.Map());
})); }));
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => public async Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () => await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

Some files were not shown because too many files have changed in this diff Show More