Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f39eb9d8fe | ||
|
|
593219db8c | ||
|
|
020bdee36f | ||
|
|
8e0c099867 | ||
|
|
ef134a5b3c | ||
|
|
717b891554 | ||
|
|
91d325ce4e | ||
|
|
3cc8514136 | ||
|
|
11311421ad | ||
|
|
f4024d563a | ||
|
|
78b03b6996 | ||
|
|
2d0f2b494a | ||
|
|
d428f6e17c | ||
|
|
5829bffaa0 | ||
|
|
b703b0c64d | ||
|
|
6d554d19a7 | ||
|
|
ec777fabf8 | ||
|
|
41bf9b63bf | ||
|
|
940630f0ed | ||
|
|
6de3d30a8b | ||
|
|
831d57a8a0 | ||
|
|
2d74f7f1bd | ||
|
|
ebf35d7f12 | ||
|
|
77307f4bf0 | ||
|
|
464c21e41d | ||
|
|
364fe65e0d | ||
|
|
b239c402fa | ||
|
|
f4a1064003 | ||
|
|
5f18478675 | ||
|
|
e71ba99f9c | ||
|
|
8de761e4e6 | ||
|
|
ca6d04d528 | ||
|
|
f607eaf152 | ||
|
|
b631c6aa4b | ||
|
|
cb59635882 | ||
|
|
1cf019b3b9 | ||
|
|
0a4108d345 | ||
|
|
afaad43c47 | ||
|
|
bccdab2ba8 | ||
|
|
5bdd2a5786 | ||
|
|
1acdb0ea7b | ||
|
|
2047e440d8 | ||
|
|
1771d74b43 | ||
|
|
bad91e8090 | ||
|
|
f9c0c5fe16 | ||
|
|
43c777dfe2 | ||
|
|
5dba8d3dd4 | ||
|
|
0d78affd3c | ||
|
|
4bac2f58c2 | ||
|
|
2a8d72963d | ||
|
|
4b658bdfd5 | ||
|
|
9945b773db | ||
|
|
f98b102a2e | ||
|
|
f29ac65d7c | ||
|
|
8450db51be | ||
|
|
5c1e18a9a9 | ||
|
|
4f1b05bf33 | ||
|
|
3db6546a2b | ||
|
|
e270bf8db2 | ||
|
|
48dd9aaa1d | ||
|
|
3fa9f257ba | ||
|
|
12663a588f | ||
|
|
4738a20f80 | ||
|
|
7446659c0c | ||
|
|
e0b1b00f63 | ||
|
|
1c1e2b9038 |
@@ -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
|
||||
+11
-2
@@ -11,6 +11,7 @@
|
||||
*.sln.docstates
|
||||
*.env
|
||||
*.env.json
|
||||
certificates/
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
@@ -51,6 +52,8 @@ bld/
|
||||
|
||||
# Build results on 'Bin' directories
|
||||
**/[Bb]in/*
|
||||
dist/assets/
|
||||
dist/
|
||||
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
||||
# (https://github.com/github/gitignore/pull/3736)
|
||||
#!**/[Bb]in/*.refresh
|
||||
@@ -58,8 +61,7 @@ bld/
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
.vscode/
|
||||
bin/
|
||||
certificates/
|
||||
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
@@ -347,6 +349,7 @@ _Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
**/.paket/paket.exe
|
||||
**/.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
@@ -432,3 +435,9 @@ FodyWeavers.xsd
|
||||
*.msix
|
||||
*.msm
|
||||
*.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
@@ -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"]
|
||||
@@ -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()),
|
||||
})
|
||||
.WithDescription("Gets an applicant's invite")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetApplicantInviteEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Invite>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class GetApplicantInvitesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems()),
|
||||
})
|
||||
.WithDescription("Gets an applicant's invite list")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetApplicantInvitesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Invite[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class GetInvitesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of sent invitations")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetInvitesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Invite[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class SendInviteEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(SendInviteEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Invite>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -34,9 +34,9 @@ public class CreateEvidenceEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
};
|
||||
})
|
||||
.RequireAuthorization()
|
||||
.DisableAntiforgery()
|
||||
.WithDescription("Upload supporting evidence file to S3 and register metadata (M14)")
|
||||
.DisableAntiforgery()
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateEvidenceEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class CreateIndicatorEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Define indicators and baselines/targets (M08)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class CreateMilestoneEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Define contractual milestones (M07)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateMilestoneRequest).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class GetEvidenceEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a specific evidence record and assiciated file URLs")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetEvidenceEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Domain.Models.Evidence>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -21,6 +21,7 @@ public class GetEvidenceFilesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of all uploaded evidence files metadata")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.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())
|
||||
})
|
||||
.WithDescription("Get a specific indicator.")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetIndicatorEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Indicator>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetIndicatorsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems()),
|
||||
})
|
||||
.WithDescription("Get a list of indicators")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Indicator[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetMilestoneEvidenceEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of all uploaded evidence metadata for a specific mliestone")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetMilestoneEvidenceEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetProjectEvidenceEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems()),
|
||||
})
|
||||
.WithDescription("Get a list of all uploaded evidence metadata for a specific project")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProjectEvidenceEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class RecordIndicatorAmountEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Record actual values for indicators (M08)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,8 @@ public sealed class UpdateEvidenceEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Updates a specific evidence record metadata")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.DisableAntiforgery()
|
||||
.WithName(typeof(UpdateEvidenceEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class ApproveDisbursementEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems()),
|
||||
})
|
||||
.WithDescription("Approve disbursement and release payment (M06)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class CreateDisbursementEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems()),
|
||||
})
|
||||
.WithDescription("Create a new disbursement schedule (M06)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class CreateRiskEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Add risk to risk register (M09)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateRiskEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class GetDisbursementEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a specific disbursement")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetDisbursementEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Disbursement>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetDisbursementsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of disbursements")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Disbursement[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class GetRiskEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a specific risk")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetRiskEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Risk>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -21,6 +21,7 @@ public class GetRisksEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of all risks")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetRisksEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class UpdateDisbursementEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Update the details of a specific disbursement")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdateDisbursementEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class UpdateRiskEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Update the details of a specific risk")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdateRiskEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class CreateOrganisationEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Create a new organization profile")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed class CreatePortfolioEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Create a new organization portfolio")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreatePortfolioEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -21,6 +21,7 @@ public class DashboardEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get executive portfolio dashboard metrics and KPIs")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(DashboardEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Dashboard>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GetInstrumentsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems()),
|
||||
})
|
||||
.WithDescription("Get all funding instruments")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Instrument[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class GetOrganisationEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get organization profile by ID")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetOrganisationEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Organisation>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetOrganisationsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a paginated directory of organizations")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Organisation[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class GetPortfolioEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a specific portfolios")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetPortfolioEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Portfolio[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class GetPortfolioProgrammesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a programme's portfolios")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetPortfolioProgrammesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Programme[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GetPortfoliosEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get all portfolios")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Portfolio[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GetProgrammesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get all programmes")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Programme[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Update an existing organization profile")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdateOrganisationEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed class UpdatePortfolioEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Update an existing organization portfolio")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdatePortfolioEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -25,6 +25,7 @@ public class CreateAwardEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Register an approved funding award (M01)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -25,6 +25,7 @@ public class CreateBudgetEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Create a new budget line (M05)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class CreateContractEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Create a contract record from an approved award (M02)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class ExecuteContractEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Execute the contract and lock the contractual version")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class GetAwardEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get award by ID")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetAwardEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Award>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetAwardsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of awards")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Award[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class GetBudgetEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get budget by ID")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetBudgetEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Budget>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetBudgetsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get all budgets")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Budget[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class GetContractEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get contract by ID")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetContractEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.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())
|
||||
})
|
||||
.WithDescription("Get all contracts")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.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 => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithDescription("Add a new activity under project (M04)")
|
||||
.WithName(typeof(CreateActivityEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed class CreateProgrammeEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Create programme")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateProgrammeEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -25,6 +25,7 @@ public class CreateProjectEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Create a new project (M04)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateProjectEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetActivitiesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of activities for a project")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Domain.Models.Activity[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GetActivityEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Gets a specific activity by it's id")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetActivityEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Domain.Models.Activity>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class GetMilestoneEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Gets a specific milestone")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetMilestoneEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Milestone>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetMilestonesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of all milestones")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Milestone[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class GetProgrammeEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get specific programme")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProgrammeEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Programme>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class GetProgrammeProjectsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems()),
|
||||
})
|
||||
.WithDescription("Get a list of all projects under a programme")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProgrammeProjectsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Project[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed class GetProgrammesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of all programmes")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName("Projects"))
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Programme[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class GetProjectEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get project by ID")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProjectEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Project[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GetProjectIndicatorsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get get a detailed list of project indicators")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProjectIndicatorsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Indicator[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GetProjectMilestonesEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of milestones for a project")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Milestone[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GetProjectRisksEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of risks for a project")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Risk[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GetProjectsEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Get a list of projects")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProjectsEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Project[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class UpdateMilestoneEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Update project milestone details")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdateMilestoneEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class UpdateProgrammeEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Update existing programme")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdateProgrammeEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class UpdateProjectEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Update project details")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -24,6 +24,7 @@ public class UpdateProjectStatusEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Update project status (M04)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdateProjectStatusEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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.Tools;
|
||||
|
||||
[ApiVersionTarget(1)]
|
||||
public sealed class EncodeIdEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapGet("api/tools/hashid/{id64:long}", async (long id64, IToolService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
await service.EncodeLong(id64, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var hash } => Results.Ok(hash),
|
||||
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Hashes an int64 identifier")
|
||||
.WithName(typeof(EncodeIdEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<string>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Tools)
|
||||
.CacheOutput();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ public sealed class GetUserEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Gets the metadata of a single user.")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetUserEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<User>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GetUsersEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetUsersEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<User[]>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -30,6 +30,7 @@ public class SyncUserEndpoint : IEndpoint
|
||||
};
|
||||
})
|
||||
.WithDescription("Trigger integration and data synchronization in the background")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(SyncUserEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class UpdateUserStatusEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Changes the Activity status of the user in the system")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(UpdateUserStatusEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
||||
<PackageReference Include="IdentityModel" Version="6.2.0" />
|
||||
<PackageReference Include="KubernetesClient" Version="19.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.12" />
|
||||
|
||||
<Using Include="Microsoft.AspNetCore.Authentication" />
|
||||
<Using Include="Microsoft.AspNetCore.Authentication.Cookies" />
|
||||
@@ -60,8 +60,8 @@
|
||||
|
||||
<!-- API Documentation -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.17.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
|
||||
|
||||
<Using Include="Scalar.AspNetCore" />
|
||||
<Using Include="Microsoft.OpenApi" />
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
using Microsoft.FeatureManagement;
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Filters;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Health;
|
||||
using PostFundManagement.Domain.Mediator;
|
||||
using PostFundManagement.Infrastructure.Database.Services;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer()
|
||||
.AddApiVersioning(options => options.ApiVersionReader = new HeaderApiVersionReader());
|
||||
builder.Services.AddFeatureManagement();
|
||||
|
||||
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.AddTransient<ITenantContextService, TenantContextService>();
|
||||
|
||||
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
|
||||
builder.Services.AddApiServices(builder.Configuration);
|
||||
@@ -91,26 +109,33 @@ builder.Services.AddOpenApi(options =>
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddCors(options => options.AddPolicy("AllowFrontend", policy =>
|
||||
policy.WithOrigins("https://localhost:3000", "https://localhost:8076", "https://console.ifri.khongisa.co.za")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials()));
|
||||
var allowedOrigins = builder.Configuration.GetSection("CorsOrigins").Get<string[]>() ?? [];
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(CorsPolicy, policy =>
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
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();
|
||||
|
||||
app.UseHsts();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseCors("AllowFrontend");
|
||||
app.UseCors(CorsPolicy);
|
||||
app.UseOutputCache();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
@@ -123,7 +148,16 @@ var versionGroups = new Dictionary<int, RouteGroupBuilder>
|
||||
{
|
||||
{
|
||||
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);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
{
|
||||
"EnableServiceCaching": true,
|
||||
"CorsOrigins": [
|
||||
"https://console.ifri.khongisa.co.za",
|
||||
"https://localhost:7208"
|
||||
],
|
||||
"GeneralS3Settings": {
|
||||
"ServiceUrl": "http://192.168.1.177:30900",
|
||||
"ServiceUrl": "https://s3.garage.khongisa.co.za",
|
||||
"Region": "garage",
|
||||
"BucketName": "pfm.general",
|
||||
"CdnBaseUrl": "https://pfm.general.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"
|
||||
"BucketName": "pfm",
|
||||
"CdnBaseUrl": "https://pfm.cdn.khongisa.co.za"
|
||||
},
|
||||
"Monitoring": {
|
||||
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
|
||||
@@ -27,8 +20,11 @@
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Default": "Warning",
|
||||
"Quartz": "Error",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database": "Error",
|
||||
"Microsoft.EntityFrameworkCore.Update": "Error"
|
||||
}
|
||||
},
|
||||
"EmailProcessorCron": "0 */5 * * * ?",
|
||||
|
||||
@@ -10,14 +10,55 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||
protected abstract string BucketName { get; }
|
||||
protected abstract string CdnBaseUrl { get; }
|
||||
|
||||
public virtual async ValueTask<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
|
||||
public string GenerateCdnUrl(string fileKey) => $"{CdnBaseUrl}/{fileKey}";
|
||||
|
||||
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))
|
||||
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))
|
||||
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();
|
||||
|
||||
@@ -31,7 +72,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||
if (string.IsNullOrWhiteSpace(fileHash))
|
||||
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
|
||||
{
|
||||
@@ -48,11 +89,11 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||
|
||||
return response.HttpStatusCode != HttpStatusCode.OK
|
||||
? 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) =>
|
||||
await Result.Try((Func<ValueTask<Result>>)(async () =>
|
||||
public virtual async Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result>>)(async () =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BucketName))
|
||||
return Result.Fail(NotFoundError.Create("Bucket name is not configured."));
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
using PostFundManagement.Application.Services.Communications;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface ICommunicationsService
|
||||
{
|
||||
ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Notification[]>> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Notification>> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,50 @@
|
||||
using PostFundManagement.Application.Services.Evidence;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IEvidenceService
|
||||
{
|
||||
ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[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);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken);
|
||||
[Log]
|
||||
Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken);
|
||||
|
||||
ValueTask<Result<Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
using PostFundManagement.Application.Services.Execution;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IExecutionService
|
||||
{
|
||||
ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,81 @@
|
||||
using PostFundManagement.Application.Services.Governance;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IGovernanceService
|
||||
{
|
||||
ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Tenant[]>> GetTenantsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Tenant>> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateTenantAsync(UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<TenantOrganisation[]>> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<TenantOrganisation>> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,55 @@
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IGrantsService
|
||||
{
|
||||
ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IIdentityService
|
||||
{
|
||||
ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,46 +1,157 @@
|
||||
using PostFundManagement.Application.Services.Projects;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IProjectService
|
||||
{
|
||||
ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateChangeRequestAsync(UpdatehangeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<ChangeRequest[]>> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<ChangeRequest>> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<ComplianceItem[]>> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<ComplianceItem>> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Beneficiary[]>> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Beneficiary>> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Issue[]>> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Issue>> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Outcome[]>> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<SiteVisit[]>> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<SiteVisit>> GetSiteVisitAsync(string SiteVisitIdHash, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
public interface IToolService
|
||||
{
|
||||
ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken);
|
||||
Task<Result<string>> EncodeLong(long id, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
using PostFundManagement.Application.Services.Users;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
using PostFundManagement.Domain.Models;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions.Services;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default);
|
||||
[Log]
|
||||
[Audit]
|
||||
Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken);
|
||||
[Log]
|
||||
Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -2,37 +2,35 @@ using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Quartz;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Extensions;
|
||||
namespace PostFundManagement.Application.Extensions;
|
||||
|
||||
public static class Quartz
|
||||
{
|
||||
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.AddQuartz(config =>
|
||||
{
|
||||
config.SchedulerName = schedulerName;
|
||||
config.SchedulerId = "AUTO";
|
||||
|
||||
config.UseSimpleTypeLoader();
|
||||
config.UseDefaultThreadPool(options => options.MaxConcurrency = 0);
|
||||
config.UseTimeZoneConverter();
|
||||
|
||||
config.UsePersistentStore(storage =>
|
||||
{
|
||||
storage.PerformSchemaValidation = false;
|
||||
storage.ProvisionSchema();
|
||||
storage.UsePostgres(options => options.ConnectionString = connectionString!);
|
||||
|
||||
storage.UseSystemTextJsonSerializer();
|
||||
storage.SetProperty("quartz.jobStore.clustered", "true");
|
||||
|
||||
storage.UsePostgres(options =>
|
||||
{
|
||||
options.ConnectionString = connectionString!;
|
||||
options.TablePrefix = "quartz.qrtz_";
|
||||
});
|
||||
storage.UseClustering(cluster =>
|
||||
{
|
||||
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
||||
@@ -46,38 +44,39 @@ public static class Quartz
|
||||
|
||||
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.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
|
||||
services.AddQuartzHostedService(options =>
|
||||
{
|
||||
options.WaitForJobsToComplete = true;
|
||||
});
|
||||
|
||||
services.AddQuartz(config =>
|
||||
{
|
||||
config.SchedulerName = schedulerName;
|
||||
config.SchedulerId = "AUTO";
|
||||
config.InterruptJobsOnShutdown = true;
|
||||
config.InterruptJobsOnShutdownWithWait = true;
|
||||
config.MaxBatchSize = 5;
|
||||
|
||||
config.UseSimpleTypeLoader();
|
||||
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
|
||||
config.UseDefaultThreadPool(options => options.MaxConcurrency = 5);
|
||||
config.UseTimeZoneConverter();
|
||||
|
||||
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
config.UsePersistentStore(storage =>
|
||||
{
|
||||
storage.PerformSchemaValidation = false;
|
||||
storage.ProvisionSchema();
|
||||
storage.UsePostgres(options => options.ConnectionString = connectionString!);
|
||||
|
||||
storage.UseSystemTextJsonSerializer();
|
||||
storage.SetProperty("quartz.jobStore.clustered", "true");
|
||||
|
||||
storage.UsePostgres(options =>
|
||||
{
|
||||
options.ConnectionString = connectionString!;
|
||||
options.TablePrefix = "quartz.qrtz_";
|
||||
});
|
||||
storage.UseClustering(cluster =>
|
||||
{
|
||||
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
||||
@@ -91,15 +90,7 @@ public static class Quartz
|
||||
|
||||
private static IServiceCollection ConfigureCommon(this IServiceCollection services)
|
||||
{
|
||||
services.Configure<QuartzOptions>(options =>
|
||||
{
|
||||
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.Configure<QuartzOptions>(options => options.Scheduling.IgnoreDuplicates = true);
|
||||
services.AddTransient<RetryJobListener>();
|
||||
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
|
||||
|
||||
|
||||
@@ -25,40 +25,6 @@ public static class S3
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using PostFundManagement.Application.Services.Shared;
|
||||
using PostFundManagement.Domain.Security.Configuration;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Security.Sdk;
|
||||
using PostFundManagement.Infrastructure.Database;
|
||||
|
||||
@@ -97,6 +98,7 @@ public static class Security
|
||||
options.Scope.Clear();
|
||||
options.Scope.Add("openid");
|
||||
options.Scope.Add("profile");
|
||||
options.Scope.Add("role");
|
||||
options.Scope.Add("email");
|
||||
|
||||
options.Events = new OpenIdConnectEvents
|
||||
@@ -149,7 +151,7 @@ public static class Security
|
||||
});
|
||||
|
||||
services.AddAuthorizationBuilder()
|
||||
.AddPolicy("ApiScope", policy =>
|
||||
.AddPolicy(Constants.MainScope, policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim("scope", authOptions.Audience!);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using PostFundManagement.Application.Abstractions;
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Interceptors;
|
||||
using PostFundManagement.Application.Services.Communications;
|
||||
using PostFundManagement.Application.Services.Evidence;
|
||||
using PostFundManagement.Application.Services.Execution;
|
||||
@@ -7,7 +8,6 @@ using PostFundManagement.Application.Services.Governance;
|
||||
using PostFundManagement.Application.Services.Grants;
|
||||
using PostFundManagement.Application.Services.Projects;
|
||||
using PostFundManagement.Application.Services.Shared;
|
||||
using PostFundManagement.Application.Services.Tools;
|
||||
using PostFundManagement.Application.Services.Users;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
|
||||
@@ -15,34 +15,71 @@ namespace PostFundManagement.Application.Extensions;
|
||||
|
||||
public static class Services
|
||||
{
|
||||
public static IServiceCollection AddLoggedCachedServiceScoped<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 loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
|
||||
|
||||
var actual = sp.GetRequiredService<TImplementation>();
|
||||
|
||||
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
|
||||
|
||||
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
|
||||
loggingInterceptor.ToInterceptor());
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddLoggedAuditedCachedServiceScoped<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 auditInterceptor = sp.GetRequiredService<AuditInterceptor>();
|
||||
var loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
|
||||
|
||||
var actual = sp.GetRequiredService<TImplementation>();
|
||||
|
||||
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
|
||||
|
||||
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
|
||||
loggingInterceptor.ToInterceptor(),
|
||||
auditInterceptor.ToInterceptor());
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<ICommunicationsService, CommunicationsService>();
|
||||
services.Decorate<ICommunicationsService, CommunicationsCacheService>();
|
||||
services.AddScoped<AuditInterceptor>();
|
||||
services.AddScoped<LoggingInterceptor>();
|
||||
services.AddSingleton<ProxyGenerator>();
|
||||
|
||||
services.AddScoped<IEvidenceService, EvidenceService>();
|
||||
services.Decorate<IEvidenceService, EvidenceCacheService>();
|
||||
services.AddLoggedAuditedCachedServiceScoped<ICommunicationsService, CommunicationsService, CommunicationsCacheService>();
|
||||
services.AddLoggedAuditedCachedServiceScoped<IEvidenceService, EvidenceService, EvidenceCacheService>();
|
||||
services.AddLoggedAuditedCachedServiceScoped<IUserService, UserService, UserCacheService>();
|
||||
services.AddLoggedAuditedCachedServiceScoped<IExecutionService, ExecutionService, ExecutionCacheService>();
|
||||
services.AddLoggedAuditedCachedServiceScoped<IGovernanceService, GovernanceService, GovernanceCacheService>();
|
||||
services.AddLoggedAuditedCachedServiceScoped<IGrantsService, GrantsService, GrantsCacheService>();
|
||||
services.AddLoggedAuditedCachedServiceScoped<IProjectService, ProjectService, ProjectCacheService>();
|
||||
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.Decorate<IUserService, UserCacheService>();
|
||||
|
||||
services.AddScoped<IIdentityService, IdentityService>();
|
||||
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.Decorate<IToolService, ToolCacheService>();
|
||||
services.AddLoggedCachedServiceScoped<IIdentityService, IdentityService, IdentityCacheService>();
|
||||
|
||||
services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
|
||||
@@ -25,8 +25,12 @@ public sealed class ActiveUserFilter(IIdentityService identityService) : IEndpoi
|
||||
var path = context.HttpContext.Request.Path.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path) && !path!.EndsWith("integration/sync", StringComparison.InvariantCultureIgnoreCase))
|
||||
if ((await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted)).IsFailed)
|
||||
return (object?)Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
|
||||
{
|
||||
var userAuthResult = await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted);
|
||||
|
||||
if (userAuthResult.IsFailed)
|
||||
return Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
context.HttpContext.Items[UserIdItemKey] = userId;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using PostFundManagement.Domain.Attributes;
|
||||
|
||||
namespace PostFundManagement.Application.Interceptors;
|
||||
|
||||
public sealed class LoggingInterceptor(ILogger<LoggingInterceptor> logger) : IAsyncInterceptor
|
||||
{
|
||||
public void InterceptSynchronous(IInvocation invocation)
|
||||
{
|
||||
invocation.Proceed();
|
||||
|
||||
CheckAndLogIfFailed(invocation, invocation.ReturnValue);
|
||||
}
|
||||
|
||||
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<LogAttribute>()
|
||||
?? invocation.Method.GetCustomAttribute<LogAttribute>();
|
||||
|
||||
if (auditAttr == null) return resultObj;
|
||||
|
||||
CheckAndLogIfFailed(invocation, resultObj);
|
||||
|
||||
return resultObj;
|
||||
}
|
||||
|
||||
private void CheckAndLogIfFailed(IInvocation invocation, object? resultObj)
|
||||
{
|
||||
if (resultObj is IResultBase result && result.IsFailed)
|
||||
{
|
||||
try
|
||||
{
|
||||
var method = invocation.MethodInvocationTarget ?? invocation.Method;
|
||||
var parameters = BuildPayloadSummary(method.GetParameters(), invocation.Arguments);
|
||||
|
||||
var errorMessages = new List<string>();
|
||||
|
||||
if (result.Reasons != null)
|
||||
{
|
||||
foreach (var reason in result.Reasons)
|
||||
if (reason is IError error)
|
||||
errorMessages.Add(error.Message);
|
||||
}
|
||||
|
||||
var errorsString = string.Join("; ", errorMessages);
|
||||
|
||||
logger.LogError("Method {MethodName} failed. Errors: [{ErrorType}] {Errors}. Parameters: {@Parameters}",
|
||||
method.Name, result.Reasons?.FirstOrDefault()?.GetType().Name.Replace("Error", string.Empty), errorsString, parameters);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to write error log for method {MethodName}.", invocation.Method.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = new List<string>();
|
||||
|
||||
foreach (var f in files)
|
||||
fileSummaries.Add($"'{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];
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,10 @@
|
||||
|
||||
<!-- API Documentation -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.11" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.17.2" />
|
||||
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.12" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
|
||||
|
||||
<Using Include="Scalar.AspNetCore" />
|
||||
<Using Include="Microsoft.OpenApi" />
|
||||
@@ -36,6 +37,8 @@
|
||||
<!-- Shared Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="Humanizer" />
|
||||
<Using Include="Castle.DynamicProxy" />
|
||||
<Using Include="Microsoft.FeatureManagement" />
|
||||
<Using Include="System.ComponentModel.DataAnnotations" />
|
||||
<Using Include="Microsoft.Extensions.Caching.Distributed" />
|
||||
<Using Include="Microsoft.Extensions.Caching.Hybrid" />
|
||||
|
||||
+74
-9
@@ -4,14 +4,22 @@ using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Shared;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Models;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
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) =>
|
||||
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
|
||||
public async Task<Result> CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||
await service.CreateNotificationAsync(request, userId, cancellationToken);
|
||||
|
||||
public async Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
|
||||
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 cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
@@ -28,14 +36,17 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
|
||||
return Result.Ok(cached!.FromJson<Invite>());
|
||||
}));
|
||||
|
||||
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
|
||||
public async Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
|
||||
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 cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
|
||||
if(string.IsNullOrWhiteSpace(cached))
|
||||
if (string.IsNullOrWhiteSpace(cached))
|
||||
{
|
||||
var result = await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken);
|
||||
|
||||
@@ -49,9 +60,12 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
|
||||
return Result.Ok(cached.FromJson<Invite[]>());
|
||||
}));
|
||||
|
||||
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
|
||||
public async Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
|
||||
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 cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
@@ -70,6 +84,57 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
|
||||
return Result.Ok(cached.FromJson<Invite[]>());
|
||||
}));
|
||||
|
||||
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||
public async Task<Result<Notification>> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Notification>>>)(async () =>
|
||||
{
|
||||
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
|
||||
return await service.GetNotificationAsync(notificationIdHash, cancellationToken);
|
||||
|
||||
var cacheKey = HashService.ToMd5Hash($"getnotification-{notificationIdHash}").Value;
|
||||
|
||||
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cached))
|
||||
{
|
||||
var result = await service.GetNotificationAsync(notificationIdHash, cancellationToken);
|
||||
|
||||
if (result.IsFailed) return result;
|
||||
|
||||
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
|
||||
|
||||
return Result.Ok(result.Value);
|
||||
}
|
||||
|
||||
return Result.Ok(cached.FromJson<Notification>());
|
||||
}));
|
||||
|
||||
public async Task<Result<Notification[]>> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Notification[]>>>)(async () =>
|
||||
{
|
||||
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
|
||||
return await service.GetNotificationsAsync(organisationIdHash, cancellationToken);
|
||||
|
||||
var cacheKey = HashService.ToMd5Hash($"getnotifications-{organisationIdHash}").Value;
|
||||
|
||||
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cached))
|
||||
{
|
||||
var result = await service.GetNotificationsAsync(organisationIdHash, cancellationToken);
|
||||
|
||||
if (result.IsFailed) return result;
|
||||
|
||||
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
|
||||
|
||||
return Result.Ok(result.Value);
|
||||
}
|
||||
|
||||
return Result.Ok(cached.FromJson<Notification[]>());
|
||||
}));
|
||||
|
||||
public async Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||
await service.SendInviteAsync(request, userId, cancellationToken);
|
||||
|
||||
public async Task<Result> UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
|
||||
await service.UpdateNotificationAsync(request, userId, cancellationToken);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user