Compare commits
119
Commits
endpoints
...
f200ccbf60
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f200ccbf60 | ||
|
|
464c21e41d | ||
|
|
27f57150e1 | ||
|
|
364fe65e0d | ||
|
|
79c63107e9 | ||
|
|
b239c402fa | ||
|
|
d945731252 | ||
|
|
f4a1064003 | ||
|
|
c979a4632e | ||
|
|
5f18478675 | ||
|
|
9eed3cff6b | ||
|
|
e71ba99f9c | ||
|
|
6db1a2f367 | ||
|
|
8de761e4e6 | ||
|
|
ca6d04d528 | ||
|
|
06d9952cd7 | ||
|
|
f607eaf152 | ||
|
|
92d481f75d | ||
|
|
b631c6aa4b | ||
|
|
1c36602995 | ||
|
|
cb59635882 | ||
|
|
5ae0c500ac | ||
|
|
1cf019b3b9 | ||
|
|
db04f800e9 | ||
|
|
0a4108d345 | ||
|
|
e7a19611f0 | ||
|
|
afaad43c47 | ||
|
|
5ad24da7cf | ||
|
|
bccdab2ba8 | ||
|
|
156dae40b7 | ||
|
|
5bdd2a5786 | ||
|
|
1b67d9cf8b | ||
|
|
1acdb0ea7b | ||
|
|
1bd05e64e5 | ||
|
|
2047e440d8 | ||
|
|
303dba3c6f | ||
|
|
1771d74b43 | ||
|
|
1dd4f54a1d | ||
|
|
bad91e8090 | ||
|
|
fae1e080c4 | ||
|
|
f9c0c5fe16 | ||
|
|
4de23d27b3 | ||
|
|
43c777dfe2 | ||
|
|
d4bce12dbb | ||
|
|
5dba8d3dd4 | ||
|
|
299b876e1c | ||
|
|
0d78affd3c | ||
|
|
dde1dbe056 | ||
|
|
4bac2f58c2 | ||
|
|
b81e6feb45 | ||
|
|
2a8d72963d | ||
|
|
4138a1941f | ||
|
|
4b658bdfd5 | ||
|
|
17af31d810 | ||
|
|
9945b773db | ||
|
|
b85cf6a1cb | ||
|
|
f98b102a2e | ||
|
|
81ddc11746 | ||
|
|
f29ac65d7c | ||
|
|
5b94672ff5 | ||
|
|
8450db51be | ||
|
|
c535392ef1 | ||
|
|
5c1e18a9a9 | ||
|
|
0607de1802 | ||
|
|
4f1b05bf33 | ||
|
|
4c1e52e452 | ||
|
|
3db6546a2b | ||
|
|
2414d74d1c | ||
|
|
e270bf8db2 | ||
|
|
48dd9aaa1d | ||
|
|
93d9661a7f | ||
|
|
3fa9f257ba | ||
|
|
dba401fe94 | ||
|
|
12663a588f | ||
|
|
42d73fe6cb | ||
|
|
4738a20f80 | ||
|
|
1475887a7f | ||
|
|
7446659c0c | ||
|
|
730664d9c4 | ||
|
|
e0b1b00f63 | ||
|
|
f38e569a34 | ||
|
|
1c1e2b9038 | ||
|
|
7e79171f97 | ||
|
|
d7b868f2c1 | ||
|
|
d43a09b371 | ||
|
|
0dc0de1502 | ||
|
|
de47dd4a4f | ||
|
|
2810cc669b | ||
|
|
734af8c86f | ||
|
|
a754c42d14 | ||
|
|
bd7ca9372f | ||
|
|
0d01d91ab4 | ||
|
|
2c817a8818 | ||
|
|
4c3a96917b | ||
|
|
e5c3a75795 | ||
|
|
1bc7e65960 | ||
|
|
094126d84b | ||
|
|
2506241a25 | ||
|
|
cfe3530639 | ||
|
|
985557bee4 | ||
|
|
f0b8b3051a | ||
|
|
6c88b9e533 | ||
|
|
7d67408d2c | ||
|
|
d1d58a6183 | ||
|
|
60ad154841 | ||
|
|
2c13170859 | ||
|
|
d87c8aa3c9 | ||
|
|
6af7815ad1 | ||
|
|
a169f6c3f6 | ||
|
|
65ea753c19 | ||
|
|
e6d2663265 | ||
|
|
91b5814b60 | ||
|
|
0c4c848568 | ||
|
|
fb05150ebf | ||
|
|
2f9c20020b | ||
|
|
a771957db3 | ||
|
|
46267a2a34 | ||
|
|
a7f360cc2a | ||
|
|
67c08b40ca |
@@ -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
|
||||
-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 -v n
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed class EncodeIdEndpoint : IEndpoint
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Hashes an int64 identifier")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(EncodeIdEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<string>(StatusCodes.Status200OK)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -91,11 +91,18 @@ 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();
|
||||
|
||||
@@ -107,10 +114,10 @@ if (!scheduler!.IsStarted)
|
||||
|
||||
app.UseHsts();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseCors("AllowFrontend");
|
||||
app.UseCors(CorsPolicy);
|
||||
app.UseOutputCache();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
@@ -123,7 +130,7 @@ var versionGroups = new Dictionary<int, RouteGroupBuilder>
|
||||
{
|
||||
{
|
||||
1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet)
|
||||
.RequireAuthorization("ApiScope").AddEndpointFilter<ActiveUserFilter>()
|
||||
.RequireCors(CorsPolicy).AddEndpointFilter<ActiveUserFilter>()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -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) =>
|
||||
public string GenerateCdnUrl(string fileKey) => $"{CdnBaseUrl}/{fileKey}";
|
||||
|
||||
public virtual async ValueTask<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<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 ValueTask<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<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,7 +89,7 @@ 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) =>
|
||||
@@ -63,7 +104,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||
Key = fileKey
|
||||
}, cancellationToken);
|
||||
|
||||
return Result.FailIf(response.HttpStatusCode == HttpStatusCode.NoContent,
|
||||
return Result.FailIf(response.HttpStatusCode == HttpStatusCode.NoContent,
|
||||
UpdateError.Create($"Failed to delete {fileKey} from S3."));
|
||||
}));
|
||||
}
|
||||
@@ -27,4 +27,6 @@ public interface IEvidenceService
|
||||
ValueTask<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -25,4 +25,10 @@ public interface IGrantsService
|
||||
ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -137,7 +139,7 @@ public static class Security
|
||||
{
|
||||
options.MapInboundClaims = false;
|
||||
options.Authority = authOptions.Authority;
|
||||
options.Audience = authOptions.Audience;
|
||||
options.Audience = authOptions.Audience;
|
||||
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
@@ -149,7 +151,7 @@ public static class Security
|
||||
});
|
||||
|
||||
services.AddAuthorizationBuilder()
|
||||
.AddPolicy("ApiScope", policy =>
|
||||
.AddPolicy(Constants.MainScope, policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim("scope", authOptions.Audience!);
|
||||
|
||||
@@ -3,7 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Communications;
|
||||
|
||||
public record InviteRequest([Required] long OrganisationId,
|
||||
public sealed record InviteRequest([Required] long OrganisationId,
|
||||
[Required] long InvitedOrganisationId,
|
||||
[Required] long AwardId,
|
||||
[Required] long ContractId,
|
||||
|
||||
@@ -60,6 +60,27 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
|
||||
return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>());
|
||||
}));
|
||||
|
||||
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
|
||||
{
|
||||
var cacheKey = HashService.ToMd5Hash($"getevidenceurl-{evidenceIdHash}").Value;
|
||||
|
||||
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cached))
|
||||
{
|
||||
var result = await service.GetEvidenceUrlAsync(evidenceIdHash, cancellationToken);
|
||||
|
||||
if (result.IsFailed) return result;
|
||||
|
||||
await cache.SetStringAsync(cacheKey, result.Value, cancellationToken);
|
||||
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
return Result.Ok(cached);
|
||||
}));
|
||||
|
||||
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () =>
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ using PostFundManagement.Infrastructure.Database;
|
||||
namespace PostFundManagement.Application.Services.Evidence;
|
||||
|
||||
public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
|
||||
[FromKeyedServices(Constants.EvidenceBucketName)] IS3Service s3Service) : IEvidenceService
|
||||
[FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IEvidenceService
|
||||
{
|
||||
public async ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result>>)(async () =>
|
||||
@@ -22,13 +22,12 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
||||
|
||||
await using var stream = file.OpenReadStream();
|
||||
|
||||
var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken);
|
||||
var uploadResult = await s3Service.UploadFileAsync(file.FileName, "evidence", stream,
|
||||
file.ContentType, cancellationToken);
|
||||
|
||||
if (uploadResult.IsFailed)
|
||||
return Result.Fail(uploadResult.Errors);
|
||||
|
||||
var documentUrl = uploadResult.Value;
|
||||
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var evidence = new Domain.Entities.Evidence
|
||||
@@ -41,7 +40,7 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Version = 1,
|
||||
Type = type,
|
||||
DocumentUrl = documentUrl,
|
||||
DocumentUrl = uploadResult.Value,
|
||||
Status = ApprovalStatus.Pending
|
||||
};
|
||||
|
||||
@@ -238,4 +237,31 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
||||
|
||||
return Result.OkIf(changedTotal > 0, UpdateError.New);
|
||||
}));
|
||||
|
||||
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
|
||||
{
|
||||
var decodeResult = hashService.DecodeLongIdHash(evidenceIdHash);
|
||||
|
||||
if (decodeResult.IsFailed)
|
||||
return Result.Fail(IdDecodeError.Create()
|
||||
.CausedBy(decodeResult.Errors));
|
||||
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var evidence = await context.Evidences.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken);
|
||||
|
||||
if (evidence is null)
|
||||
return Result.Fail<string>(NotFoundError.New);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(evidence.DocumentUrl))
|
||||
return Result.Fail<string>(ValidationError.Create("Ecidence is invalid, has no saved documentUrl"));
|
||||
|
||||
var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(evidence.DocumentUrl,
|
||||
TimeSpan.FromMinutes(15), null, cancellationToken);
|
||||
|
||||
return Result.OkIf(secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value),
|
||||
NotFoundError.Create("Failed to generate secure file url"))
|
||||
.ToResult(secureDownloadUrlResult.Value);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Evidence;
|
||||
|
||||
public record UpdateEvidenceRequest([Required] long EvidenceId,
|
||||
public sealed record UpdateEvidenceRequest([Required] long EvidenceId,
|
||||
[Required] int Version,
|
||||
[Required] EvidenceType EvidenceType,
|
||||
[Required] ApprovalStatus Status);
|
||||
|
||||
public record RecordActualRequest([Required] decimal ActualAmount);
|
||||
public sealed record RecordActualRequest([Required] decimal ActualAmount);
|
||||
|
||||
public record CreateMilestoneRequest([Required] long ProjectId,
|
||||
public sealed record CreateMilestoneRequest([Required] long ProjectId,
|
||||
[Required] DateTime DueAt,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required] Priority Priority);
|
||||
|
||||
public record CreateIndicatorRequest([Required] long ProjectId,
|
||||
public sealed record CreateIndicatorRequest([Required] long ProjectId,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required] UnitOfMeasure UnitOfMeasure,
|
||||
[Required] decimal BaselineAmount,
|
||||
|
||||
@@ -3,17 +3,17 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Execution;
|
||||
|
||||
public record UpdateDisbursementRequest([Required] long DisbursementId, long? MilestoneId, [Required] decimal Amount);
|
||||
public sealed record UpdateDisbursementRequest([Required] long DisbursementId, long? MilestoneId, [Required] decimal Amount);
|
||||
|
||||
public record UpdateRiskRequest([Required] long RiskId,
|
||||
public sealed record UpdateRiskRequest([Required] long RiskId,
|
||||
[Required] RiskLikelihood Likelihood,
|
||||
[Required] RiskImpact Impact,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required][MaxLength(TextLength)] string Description);
|
||||
|
||||
public record CreateDisbursementRequest([Required] long ProjectId, long? MilestoneId, [Required] decimal Amount);
|
||||
public sealed record CreateDisbursementRequest([Required] long ProjectId, long? MilestoneId, [Required] decimal Amount);
|
||||
|
||||
public record CreateRiskRequest([Required] long ProjectId,
|
||||
public sealed record CreateRiskRequest([Required] long ProjectId,
|
||||
[Required] RiskLikelihood Likelihood,
|
||||
[Required] RiskImpact Impact,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
|
||||
@@ -3,28 +3,28 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Governance;
|
||||
|
||||
public record UpdatePortfolioRequest([Required] long PortfolioId,
|
||||
public sealed record UpdatePortfolioRequest([Required] long PortfolioId,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required][MaxLength(TextLength)] string Description,
|
||||
[Required] Guid OwnerId);
|
||||
|
||||
public record CreatePortfolioRequest([Required][MaxLength(LabelLength)] string Name,
|
||||
public sealed record CreatePortfolioRequest([Required][MaxLength(LabelLength)] string Name,
|
||||
[Required][MaxLength(TextLength)] string Description,
|
||||
[Required] long OrganisationId,
|
||||
[Required] Guid OwnerId);
|
||||
|
||||
public record UpdateOrganisationRequest([Required] long OrganisationId,
|
||||
public sealed record UpdateOrganisationRequest([Required] long OrganisationId,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required][MaxLength(LabelLength)] string RegistrationNo,
|
||||
[Required][EmailAddress][MaxLength(LabelLength)] string Email,
|
||||
[Required] OrganisationType Type);
|
||||
|
||||
public record CreateOrganisationRequest([Required][MaxLength(LabelLength)] string Name,
|
||||
public sealed record CreateOrganisationRequest([Required][MaxLength(LabelLength)] string Name,
|
||||
[Required][MaxLength(LabelLength)] string RegistrationNo,
|
||||
[Required][EmailAddress][MaxLength(LabelLength)] string Email,
|
||||
[Required] OrganisationType Type = OrganisationType.PrivateLimitedCompany);
|
||||
|
||||
public record Dashboard([Required] decimal PortfolioValue,
|
||||
public sealed record Dashboard([Required] decimal PortfolioValue,
|
||||
[Required] int ActiveAwards,
|
||||
[Required] decimal Disbursed,
|
||||
[Required] double OnTrackPercentage,
|
||||
|
||||
@@ -146,4 +146,31 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
|
||||
|
||||
return Result.Ok(cached.FromJson<Contract[]>());
|
||||
}));
|
||||
|
||||
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
|
||||
{
|
||||
var cacheKey = HashService.ToMd5Hash($"getcontracturl-{contractIdHash}").Value;
|
||||
|
||||
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
|
||||
if(string.IsNullOrWhiteSpace(cached))
|
||||
{
|
||||
var result = await service.GetContractUrlAsync(contractIdHash, cancellationToken);
|
||||
|
||||
if(result.IsFailed) return result;
|
||||
|
||||
await cache.SetStringAsync(cacheKey, result.Value, cancellationToken);
|
||||
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
return Result.Ok(cached!);
|
||||
}));
|
||||
|
||||
public async ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
|
||||
await service.UpdateContractAsync(request, cancellationToken);
|
||||
|
||||
public async ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
|
||||
await service.UploadContractAsync(contractIdHash, userId, file, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Services.Shared;
|
||||
using PostFundManagement.Domain;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Models;
|
||||
@@ -8,7 +9,8 @@ using PostFundManagement.Infrastructure.Database;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Grants;
|
||||
|
||||
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IGrantsService
|
||||
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
|
||||
[FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IGrantsService
|
||||
{
|
||||
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
|
||||
@@ -201,4 +203,73 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
|
||||
|
||||
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
|
||||
}));
|
||||
|
||||
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
|
||||
{
|
||||
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
|
||||
|
||||
if (decodeResult.IsFailed)
|
||||
return Result.Fail(IdDecodeError.Create()
|
||||
.CausedBy(decodeResult.Errors));
|
||||
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var contract = await context.Contracts.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken);
|
||||
|
||||
if (contract is null)
|
||||
return Result.Fail<string>(NotFoundError.New);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(contract.SignedDocumentUrl))
|
||||
return Result.Fail<string>(ValidationError.Create("Ecidence is invalid, has no saved documentUrl"));
|
||||
|
||||
var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(contract.SignedDocumentUrl,
|
||||
TimeSpan.FromMinutes(15), null, cancellationToken);
|
||||
|
||||
return Result.OkIf(secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value),
|
||||
NotFoundError.Create("Failed to generate secure file url"))
|
||||
.ToResult(secureDownloadUrlResult.Value);
|
||||
}));
|
||||
|
||||
public async ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result>>)(async () =>
|
||||
{
|
||||
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
|
||||
|
||||
if (decodeResult.IsFailed)
|
||||
return Result.Fail(IdDecodeError.Create()
|
||||
.CausedBy(decodeResult.Errors));
|
||||
|
||||
await using var stream = file.OpenReadStream();
|
||||
|
||||
var uploadResult = await s3Service.UploadFileAsync(file.FileName, "contracts", stream,
|
||||
file.ContentType, cancellationToken);
|
||||
|
||||
if (uploadResult.IsFailed)
|
||||
return Result.Fail(uploadResult.Errors);
|
||||
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var changedTotal = await context.Contracts.Where(c => c.Id == decodeResult.Value)
|
||||
.ExecuteUpdateAsync(f => f
|
||||
.SetProperty(c => c.SignedDocumentUrl, uploadResult.Value), cancellationToken);
|
||||
|
||||
var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(uploadResult.Value,
|
||||
TimeSpan.FromMinutes(15), null, cancellationToken);
|
||||
|
||||
return Result.OkIf(changedTotal > 0 && secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value),
|
||||
CreateError.Create("Failed to upload contract file"));
|
||||
}));
|
||||
|
||||
public async ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result>>)(async () =>
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var changedTotal = await context.Contracts.Where(c => c.Id == request.ContractId)
|
||||
.ExecuteUpdateAsync(f => f
|
||||
.SetProperty(u => u.Status, request.Status), cancellationToken);
|
||||
|
||||
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update the contract."));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
using PostFundManagement.Domain;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Grants;
|
||||
|
||||
public record CreateAwardRequest([Required] long OrganisationId, [Required] long ProgrammeId, [Required] decimal Amount);
|
||||
public sealed record UpdateContractRequest([Required] long ContractId, [Required] ContractStatus Status);
|
||||
|
||||
public record CreateBudgetRequest([Required] long ProjectId,
|
||||
public sealed record CreateAwardRequest([Required] long OrganisationId, [Required] long ProgrammeId, [Required] decimal Amount);
|
||||
|
||||
public sealed record CreateBudgetRequest([Required] long ProjectId,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required] decimal Amount);
|
||||
|
||||
public record CreateContractRequest([Required] long AwardId,
|
||||
public sealed record CreateContractRequest([Required] long AwardId,
|
||||
[Required] decimal TotalValue,
|
||||
[Required] DateTime EffectiveAt,
|
||||
[Required] DateTime ExpiresAt);
|
||||
|
||||
public record ExecuteContractRequest([Required] long ContractId,
|
||||
public sealed record ExecuteContractRequest([Required] long ContractId,
|
||||
[Required][MaxLength(LabelLength)] string ExternalSignatureId,
|
||||
[Required][MaxLength(LabelLength)] string SignedDocumentUrl);
|
||||
|
||||
@@ -3,35 +3,35 @@ using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Projects;
|
||||
|
||||
public record UpdateProgrammeRequest([Required] long ProgrammeId,
|
||||
public sealed record UpdateProgrammeRequest([Required] long ProgrammeId,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required] Guid OwnerId,
|
||||
[Required] ApprovalStatus Status);
|
||||
|
||||
public record CreateProgrammeRequest(long PortfolioId, string Name, Guid OwnerId);
|
||||
public sealed record CreateProgrammeRequest(long PortfolioId, string Name, Guid OwnerId);
|
||||
|
||||
public record UpdateProjectRequest([Required] long ProjectId,
|
||||
public sealed record UpdateProjectRequest([Required] long ProjectId,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required][MaxLength(TextLength)] string Description,
|
||||
DateTime? StartedAt, DateTime? EndedAt,
|
||||
[Required] IndustrySector Sector,
|
||||
[Required] ProjectType ProjectType);
|
||||
|
||||
public record UpdateMilestoneRequest([Required] long MilestoneId,
|
||||
public sealed record UpdateMilestoneRequest([Required] long MilestoneId,
|
||||
[Required] DateTime DueAt,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required] Priority Priority,
|
||||
[Required] ApprovalStatus Status);
|
||||
|
||||
public record UpdateProjectStatusRequest([Required] long ProjectId, [Required] ApprovalStatus Status);
|
||||
public sealed record UpdateProjectStatusRequest([Required] long ProjectId, [Required] ApprovalStatus Status);
|
||||
|
||||
public record CreateProjectRequest([Required] long ProgrammeId,
|
||||
public sealed record CreateProjectRequest([Required] long ProgrammeId,
|
||||
[Required][MaxLength(LabelLength)] string Name,
|
||||
[Required][MaxLength(TextLength)] string Description,
|
||||
[Required] IndustrySector Sector,
|
||||
[Required] ProjectType ProjectType);
|
||||
|
||||
public record CreateActivityRequest([Required] long ProjectId,
|
||||
public sealed record CreateActivityRequest([Required] long ProjectId,
|
||||
[Required] long? MilestoneId,
|
||||
[Required][MaxLength(LabelLength)] string Category,
|
||||
[Required] decimal Amount,
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using PostFundManagement.Application.Abstractions;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Shared;
|
||||
|
||||
public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) :
|
||||
S3ServiceBase(amazonS3), IS3Service
|
||||
{
|
||||
protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? string.Empty;
|
||||
protected override string CdnBaseUrl => configuration.GetSection($"{ContractS3SettingsSection}:CdnBaseUrl").Value ?? string.Empty;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using PostFundManagement.Application.Abstractions;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Services.Shared;
|
||||
|
||||
public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceBucketName)] IAmazonS3 amazonS3) :
|
||||
S3ServiceBase(amazonS3), IS3Service
|
||||
{
|
||||
protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? string.Empty;
|
||||
protected override string CdnBaseUrl => configuration.GetSection($"{EvidenceS3SettingsSection}:CdnBaseUrl").Value ?? string.Empty;
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace PostFundManagement.Application.Services.Users;
|
||||
|
||||
public record UserFilterRequest(ActivityStatus? Status = null, string? Email = null, string? SortBy = null, bool SortDescending = false);
|
||||
public sealed record UserFilterRequest(ActivityStatus? Status = null, string? Email = null, string? SortBy = null, bool SortDescending = false);
|
||||
|
||||
@@ -6,13 +6,13 @@ WORKDIR /app
|
||||
# Enable corepack to use Yarn
|
||||
RUN corepack enable
|
||||
|
||||
# Copy dependency definitions from the subdirectory
|
||||
# Copy dependency definitions from the UI subdirectory into the container's /app folder
|
||||
COPY PostFundManagement.ConsoleUI/package*.json PostFundManagement.ConsoleUI/yarn.lock* ./
|
||||
|
||||
# Install dependencies via Yarn
|
||||
RUN yarn install --frozen-lockfile || yarn install
|
||||
|
||||
# Copy application source from the subdirectory
|
||||
# Copy application source from the UI subdirectory into the container's /app folder
|
||||
COPY PostFundManagement.ConsoleUI/ ./
|
||||
|
||||
# Build Vite application for production
|
||||
@@ -38,4 +38,4 @@ EXPOSE 3000
|
||||
|
||||
# Set entrypoint and default command
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface AppConfig {
|
||||
export const defaultSecuritySettings: SecuritySettings = {
|
||||
Authority: (import.meta.env.VITE_OIDC_AUTHORITY as string) || 'https://sts.security.khongisa.co.za',
|
||||
Audience: (import.meta.env.VITE_OIDC_AUDIENCE as string) || 'pfm-api-dev',
|
||||
ClientId: (import.meta.env.VITE_OIDC_CLIENT_ID as string) || 'pfm-admin-dev',
|
||||
ClientId: (import.meta.env.VITE_OIDC_CLIENT_ID as string) || 'pfm-console-dev',
|
||||
ClientSecret: (import.meta.env.VITE_OIDC_CLIENT_SECRET as string) || 'secret_dcaeaa28097d9ba5b06ec324d9eac9d9c81de96e0a203cf2c6154c6dd7c89e94',
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ namespace PostFundManagement.Domain.Abstractions;
|
||||
|
||||
public interface IS3Service
|
||||
{
|
||||
ValueTask<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
|
||||
string GenerateCdnUrl(string fileKey);
|
||||
ValueTask<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default);
|
||||
ValueTask<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
|
||||
ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user