15 Commits
Author SHA1 Message Date
hermes 2d0f2b494a Test 2 reoslutions 2026-09-20 15:25:53 +02:00
hermes d428f6e17c Remediated test #1 results 2026-09-20 13:51:01 +02:00
hermes 5829bffaa0 Refactored UI endpoint usage 2026-09-20 08:26:06 +02:00
hermes b703b0c64d Added service cache feature flag
Updated scheduler and moved to its own dedicated database
2026-09-18 16:11:14 +02:00
hermes 6d554d19a7 Implemented featureflag for service layer caching 2026-09-18 14:58:42 +02:00
hermes ec777fabf8 Implemented session and account lockout management
Enabled account sync control and session expiration management
Applied some fixes to the govername page and project page
2026-09-18 10:19:24 +02:00
hermes 41bf9b63bf Added dist/assets/ to gitignore 2026-09-17 07:38:52 +02:00
hermes 940630f0ed Untracking dist files 2026-09-17 07:38:16 +02:00
hermes 6de3d30a8b Removed package.json 2026-09-17 07:23:18 +02:00
hermes 831d57a8a0 Refactored UI and compose file 2026-09-17 07:07:05 +02:00
hermes 2d74f7f1bd Removed docker debug code 2026-09-15 12:13:16 +02:00
hermes ebf35d7f12 Refactored ApiVersioning service registration 2026-09-15 12:09:10 +02:00
hermes 77307f4bf0 Added OPTIONS header handling 2026-09-15 08:51:48 +02:00
hermes 464c21e41d Added RequireCors() to RouteGroup 2026-09-15 08:44:43 +02:00
hermes 364fe65e0d Moved .RequireAuthentication() to endpoints, removed it from EndpointGroup 2026-09-15 08:37:08 +02:00
154 changed files with 34948 additions and 11887 deletions
+12 -3
View File
@@ -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
@@ -431,4 +434,10 @@ FodyWeavers.xsd
*.msi
*.msix
*.msm
*.msp
*.msp
/PostFundManagement.ConsoleUI/dist/config.json
/PostFundManagement.ConsoleUI/dist/env-config.js
/PostFundManagement.ConsoleUI/dist/assets/index-BaOzkVJi.css
/PostFundManagement.ConsoleUI/dist/assets/index-BU8qSe7k.js
/PostFundManagement.ConsoleUI/dist/favicon.svg
/PostFundManagement.ConsoleUI/dist/index.html
+1 -1
View File
@@ -22,7 +22,7 @@ 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
RUN dotnet build "PostFundManagement.Api.csproj" -c Release -o /app/build
# Stage 3: Publish
FROM build AS publish
@@ -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)
@@ -23,6 +23,7 @@ public sealed class GetEvidenceUrlEndpoint : IEndpoint
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)
@@ -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)
@@ -23,6 +23,7 @@ public sealed class GetContractUrlEndpoint : IEndpoint
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)
@@ -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)
@@ -25,6 +25,7 @@ public sealed class UpdateContractEndpoint : IEndpoint
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)
@@ -33,6 +33,8 @@ public sealed class UploadContractEndpoint : IEndpoint
};
})
.WithDescription("Uploads a contract file securely")
.RequireAuthorization(Constants.MainScope)
.DisableAntiforgery()
.WithName(typeof(UploadContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -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)
@@ -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" />
+33 -9
View File
@@ -1,16 +1,31 @@
using Microsoft.FeatureManagement;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Filters;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Health;
using PostFundManagement.Domain.Mediator;
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.AddEndpoints(Assembly.GetExecutingAssembly());
@@ -95,7 +110,7 @@ var allowedOrigins = builder.Configuration.GetSection("CorsOrigins").Get<string[
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowConfiguredOrigins", policy =>
options.AddPolicy(CorsPolicy, policy =>
{
policy.WithOrigins(allowedOrigins)
.AllowAnyHeader()
@@ -107,15 +122,15 @@ builder.Services.AddCors(options =>
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("AllowConfiguredOrigins");
app.UseCors(CorsPolicy);
app.UseOutputCache();
app.UseAuthentication();
@@ -130,7 +145,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
View File
@@ -1,4 +1,5 @@
{
"EnableServiceCaching": true,
"CorsOrigins": [
"https://console.ifri.khongisa.co.za",
"https://localhost:7208"
@@ -2,7 +2,7 @@ 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
{
@@ -10,35 +10,33 @@ public static class Quartz
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
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);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
});
});
});
});
return services;
@@ -48,36 +46,37 @@ public static class Quartz
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
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,18 +90,10 @@ 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>();
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;
@@ -138,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
{
@@ -150,7 +151,7 @@ public static class Security
});
services.AddAuthorizationBuilder()
.AddPolicy("ApiScope", policy =>
.AddPolicy(Constants.MainScope, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", authOptions.Audience!);
@@ -24,9 +24,9 @@
<!-- 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="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 +36,7 @@
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Humanizer" />
<Using Include="Microsoft.FeatureManagement" />
<Using Include="System.ComponentModel.DataAnnotations" />
<Using Include="Microsoft.Extensions.Caching.Distributed" />
<Using Include="Microsoft.Extensions.Caching.Hybrid" />
@@ -4,14 +4,19 @@ 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 () =>
{
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);
@@ -31,11 +36,14 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<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);
@@ -52,6 +60,9 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<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);
@@ -4,10 +4,12 @@ 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.Evidence;
public sealed class EvidenceCacheService(EvidenceService service, IDistributedCache cache) : ServiceCacheBase, IEvidenceService
public sealed class EvidenceCacheService(EvidenceService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IEvidenceService
{
public async ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
await service.CreateEvidenceAsync(userId, form, file, cancellationToken);
@@ -21,6 +23,9 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceAsync(evidenceId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getevidence-{evidenceId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -42,6 +47,9 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceFilesAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getevidencefiles-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -63,6 +71,9 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceUrlAsync(evidenceIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getevidenceurl-{evidenceIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -84,6 +95,9 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIndicatorAsync(indicatorId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getindicator-{indicatorId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -105,6 +119,9 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIndicatorsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getindicators-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -126,11 +143,14 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetMilestoneEvidencesAsync(milestoneIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getmilestoneevidences-{milestoneIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetMilestoneEvidencesAsync(milestoneIdHash, cancellationToken);
@@ -147,11 +167,14 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectEvidencesAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprojectevidences-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProjectEvidencesAsync(projectIdHash, cancellationToken);
@@ -4,10 +4,12 @@ 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.Execution;
public sealed class ExecutionCacheService(ExecutionService service, IDistributedCache cache) : ServiceCacheBase, IExecutionService
public sealed class ExecutionCacheService(ExecutionService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IExecutionService
{
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await service.ApproveDisbursementAsync(disbursementIdHash, userId, cancellationToken);
@@ -21,6 +23,9 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDisbursementAsync(disbursementIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getdisbursement-{disbursementIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -42,6 +47,9 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDisbursementsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getdisbursements-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -63,6 +71,9 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRiskAsync(riskIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getrisk-{riskIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -84,6 +95,9 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRisksAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getrisks-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -4,10 +4,12 @@ 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.Governance;
public sealed class GovernanceCacheService(GovernanceService service, IDistributedCache cache) : ServiceCacheBase, IGovernanceService
public sealed class GovernanceCacheService(GovernanceService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IGovernanceService
{
public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateOrganisationAsync(request, userId, cancellationToken);
@@ -18,6 +20,9 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDashboardAsync(cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getdashboard").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -39,6 +44,9 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetInstrumentsAsync(programmeIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getinstruments").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -60,6 +68,9 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOrganisationAsync(organisationIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getorganisation-{organisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -81,6 +92,9 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOrganisationsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getorganisations-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -102,6 +116,9 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPorfoliosAsync(organisationIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfolios-{organisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -123,11 +140,14 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPortfolioAsync(portfolioIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfolio-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetPortfolioAsync(portfolioIdHash, cancellationToken);
@@ -144,11 +164,14 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPortfolioProgrammesAsync(portfolioIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfolioprogrammes-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetPortfolioProgrammesAsync(portfolioIdHash, cancellationToken);
@@ -165,6 +188,9 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammesAsync(portfolioIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprogrammes-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -4,10 +4,12 @@ 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.Grants;
public sealed class GrantsCacheService(GrantsService service, IDistributedCache cache) : ServiceCacheBase, IGrantsService
public sealed class GrantsCacheService(GrantsService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IGrantsService
{
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateAwardAsync(request, userId, cancellationToken);
@@ -24,6 +26,9 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetAwardAsync(awardIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getaward-{awardIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -45,6 +50,9 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetAwardsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getawards-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -66,6 +74,9 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBudgetAsync(budgetIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getbudget-{budgetIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -87,6 +98,9 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBudgetsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getbudgets-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -108,6 +122,9 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractAsync(contractIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getctract-{contractIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -129,6 +146,9 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getcontracts-{pagination.Index}-{pagination.PageSize}-{pagination.Page}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -150,15 +170,18 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractUrlAsync(contractIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getcontracturl-{contractIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetContractUrlAsync(contractIdHash, cancellationToken);
if(result.IsFailed) return result;
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value, cancellationToken);
@@ -4,10 +4,12 @@ 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.Projects;
public sealed class ProjectCacheService(ProjectService service, IDistributedCache cache) : ServiceCacheBase, IProjectService
public sealed class ProjectCacheService(ProjectService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IProjectService
{
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateActivityAsync(request, userId, cancellationToken);
@@ -21,6 +23,9 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetActivitiesAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getactivities-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -42,11 +47,14 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetActivityAsync(activityIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getactivity-{activityIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetActivityAsync(activityIdHash, cancellationToken);
@@ -63,6 +71,9 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetMilestoneAsync(milestoneIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getmilestone-{milestoneIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -84,11 +95,14 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetMilestonesAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getmilestones-{pagination.Index}-{pagination.PageSize}-{pagination.Page}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetMilestonesAsync(pagination, cancellationToken);
@@ -105,11 +119,14 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammeAsync(programmeIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprogramme-{programmeIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProgrammeAsync(programmeIdHash, cancellationToken);
@@ -126,11 +143,14 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammeProjectsAsync(programmeIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprogrammeprojects-{programmeIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProgrammeProjectsAsync(programmeIdHash, cancellationToken);
@@ -147,11 +167,14 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammesAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprogrammes-{pagination.Index}-{pagination.PageSize}-{pagination.Page}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProgrammesAsync(pagination, cancellationToken);
@@ -168,6 +191,9 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getproject-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -189,6 +215,9 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectIndicatorsAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprojectindicators-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -197,7 +226,7 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
{
var result = await service.GetProjectIndicatorsAsync(projectIdHash, cancellationToken);
if(result.IsFailed) return result;
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
@@ -210,15 +239,18 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectMilestonesAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprojectmilestones-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProjectMilestonesAsync(projectIdHash, cancellationToken);
if(result.IsFailed) return result;
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
@@ -231,6 +263,9 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectRisksAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprojectrisks-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -239,7 +274,7 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
{
var result = await service.GetProjectRisksAsync(projectIdHash, cancellationToken);
if(result.IsFailed) return result;
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
@@ -252,6 +287,9 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprojects-{pagination.Index}-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -260,7 +298,7 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
{
var result = await service.GetProjectsAsync(pagination, cancellationToken);
if(result.IsFailed) return result;
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
@@ -1,14 +1,19 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Tools;
public sealed class ToolCacheService(ToolService service, IDistributedCache cache) : ServiceCacheBase, IToolService
public sealed class ToolCacheService(ToolService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IToolService
{
public async ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.EncodeLong(id, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"encodelonghash-{id}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -4,14 +4,19 @@ 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.Users;
public sealed class UserCacheService(UserService service, IDistributedCache cache) : ServiceCacheBase, IUserService
public sealed class UserCacheService(UserService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IUserService
{
public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<User>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetUserAsync(userId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getuser-{userId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -33,6 +38,9 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<User[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetUsersAsync(pagination, request, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getusers-{pagination.Page}-{pagination.PageSize}-" +
$"{request.Email}-{request.Status}-{request.SortBy}-{request.SortDescending}").Value;
+10
View File
@@ -16,3 +16,13 @@ VITE_OIDC_CLIENT_SECRET="secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f02
# PFM Backend API Base URL
VITE_API_BASE_URL="https://api.ifri.khongisa.co.za/"
# Protocol / SSL Settings for local development
# HTTPS is enabled by default when running locally via @vitejs/plugin-basic-ssl.
# Set to 'false' if you ever need to run local dev over plain HTTP.
HTTPS="true"
VITE_ENABLE_HTTPS="true"
# HasherSettings for HashIds compatibility with backend pfm-api
VITE_HASHER_MIN_LENGTH="11"
VITE_HASHER_SALT="TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ=="
+20
View File
@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/b2519d4f-e797-44fe-be2e-0d30bec6814d
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,19 +0,0 @@
version: '3.8'
services:
pfm-console:
build:
context: .
dockerfile: Dockerfile
container_name: pfm-console
ports:
- "8076:3000"
environment:
# OIDC Identity Provider Configuration
- OIDC_AUTHORITY=${OIDC_AUTHORITY}
- OIDC_AUDIENCE=${OIDC_AUDIENCE}
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
# Backend API Configuration
- API_BASE_URL=${API_BASE_URL}
restart: unless-stopped
@@ -1,4 +1,102 @@
#!/bin/sh
set -e
# Target directory for Nginx static assets in container
TARGET_DIR="/usr/share/nginx/html"
# If not in standard nginx container, fallback to ./dist or ./public if present
if [ ! -d "$TARGET_DIR" ]; then
if [ -d "./dist" ]; then
TARGET_DIR="./dist"
elif [ -d "./public" ]; then
TARGET_DIR="./public"
fi
fi
# Prioritize deployment environment variables from all common naming patterns:
# 1. API Base URL
API_BASE_URL="${VITE_API_BASE_URL:-${API_BASE_URL:-${ApiSettings__BaseUrl:-${ApiSettings_BaseUrl:-${PFM_API_BASE_URL:-}}}}}"
# 2. OIDC Security Authority
OIDC_AUTHORITY="${VITE_OIDC_AUTHORITY:-${OIDC_AUTHORITY:-${SecuritySettings__Authority:-${SecuritySettings_Authority:-${AUTHORITY:-}}}}}"
# 3. OIDC Audience
OIDC_AUDIENCE="${VITE_OIDC_AUDIENCE:-${OIDC_AUDIENCE:-${SecuritySettings__Audience:-${SecuritySettings_Audience:-${AUDIENCE:-}}}}}"
# 4. OIDC Client ID
OIDC_CLIENT_ID="${VITE_OIDC_CLIENT_ID:-${OIDC_CLIENT_ID:-${SecuritySettings__ClientId:-${SecuritySettings_ClientId:-${CLIENT_ID:-}}}}}"
# 5. OIDC Client Secret
OIDC_CLIENT_SECRET="${VITE_OIDC_CLIENT_SECRET:-${OIDC_CLIENT_SECRET:-${SecuritySettings__ClientSecret:-${SecuritySettings_ClientSecret:-${CLIENT_SECRET:-}}}}}"
# 6. OIDC Scope
OIDC_SCOPE="${VITE_OIDC_SCOPE:-${OIDC_SCOPE:-${SecuritySettings__Scope:-${SecuritySettings_Scope:-${SCOPE:-}}}}}"
# 7. Hasher Settings
HASHER_SALT="${VITE_HASHER_SALT:-${HASHER_SALT:-${HasherSettings__Salt:-${HasherSettings_Salt:-}}}}}"
HASHER_MIN_LENGTH="${VITE_HASHER_MIN_LENGTH:-${HASHER_MIN_LENGTH:-${HasherSettings__MinLength:-${HasherSettings_MinLength:-}}}}}"
if [ -d "$TARGET_DIR" ]; then
echo "[docker-entrypoint] Generating runtime configuration files in $TARGET_DIR..."
# Generate env-config.js for instant, synchronous window.__RUNTIME_CONFIG__ initialization
cat <<EOF > "$TARGET_DIR/env-config.js"
// Generated dynamically at container startup from deployment environment variables
window.__RUNTIME_CONFIG__ = {
API_BASE_URL: "${API_BASE_URL}",
VITE_API_BASE_URL: "${API_BASE_URL}",
OIDC_AUTHORITY: "${OIDC_AUTHORITY}",
VITE_OIDC_AUTHORITY: "${OIDC_AUTHORITY}",
OIDC_AUDIENCE: "${OIDC_AUDIENCE}",
VITE_OIDC_AUDIENCE: "${OIDC_AUDIENCE}",
OIDC_CLIENT_ID: "${OIDC_CLIENT_ID}",
VITE_OIDC_CLIENT_ID: "${OIDC_CLIENT_ID}",
OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET}",
VITE_OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET}",
OIDC_SCOPE: "${OIDC_SCOPE}",
VITE_OIDC_SCOPE: "${OIDC_SCOPE}",
HASHER_SALT: "${HASHER_SALT}",
VITE_HASHER_SALT: "${HASHER_SALT}",
HASHER_MIN_LENGTH: "${HASHER_MIN_LENGTH}",
VITE_HASHER_MIN_LENGTH: "${HASHER_MIN_LENGTH}",
ApiSettings: {
BaseUrl: "${API_BASE_URL}"
},
SecuritySettings: {
Authority: "${OIDC_AUTHORITY}",
Audience: "${OIDC_AUDIENCE}",
ClientId: "${OIDC_CLIENT_ID}",
ClientSecret: "${OIDC_CLIENT_SECRET}",
Scope: "${OIDC_SCOPE}"
},
HasherSettings: {
Salt: "${HASHER_SALT}",
MinHashLength: "${HASHER_MIN_LENGTH}"
}
};
EOF
# Generate config.json for REST-based runtime retrieval
cat <<EOF > "$TARGET_DIR/config.json"
{
"ApiSettings": {
"BaseUrl": "${API_BASE_URL}"
},
"SecuritySettings": {
"Authority": "${OIDC_AUTHORITY}",
"Audience": "${OIDC_AUDIENCE}",
"ClientId": "${OIDC_CLIENT_ID}",
"ClientSecret": "${OIDC_CLIENT_SECRET}",
"Scope": "${OIDC_SCOPE}"
},
"HasherSettings": {
"Salt": "${HASHER_SALT}",
"MinHashLength": "${HASHER_MIN_LENGTH}"
}
}
EOF
echo "[docker-entrypoint] Successfully wrote $TARGET_DIR/env-config.js and $TARGET_DIR/config.json."
fi
exec "$@"
+106
View File
@@ -0,0 +1,106 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<defs>
<!-- Ambient Radial Glow -->
<radialGradient id="pfm-ambient-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#38bdf8" stop-opacity="0.4" />
<stop offset="70%" stop-color="#2563eb" stop-opacity="0.1" />
<stop offset="100%" stop-color="#1e1b4b" stop-opacity="0" />
</radialGradient>
<!-- Top facet: Governance & Portfolios (Indigo / Radiant Violet) -->
<linearGradient id="pfm-grad-top" x1="12" y1="8" x2="52" y2="26" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#a5b4fc" />
<stop offset="35%" stop-color="#6366f1" />
<stop offset="100%" stop-color="#4338ca" />
</linearGradient>
<!-- Left facet: Capital & Disbursement Vault (Electric Sapphire) -->
<linearGradient id="pfm-grad-left" x1="10" y1="22" x2="30" y2="56" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#38bdf8" />
<stop offset="45%" stop-color="#2563eb" />
<stop offset="100%" stop-color="#1e40af" />
</linearGradient>
<!-- Right facet: Milestones, Evidence & Impact Delivery (Cyan to Emerald) -->
<linearGradient id="pfm-grad-right" x1="34" y1="28" x2="54" y2="56" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#22d3ee" />
<stop offset="50%" stop-color="#06b6d4" />
<stop offset="100%" stop-color="#10b981" />
</linearGradient>
<!-- Center Radiant Diamond -->
<linearGradient id="pfm-grad-core" x1="27" y1="27" x2="37" y2="37" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="40%" stop-color="#e0f2fe" />
<stop offset="100%" stop-color="#38bdf8" />
</linearGradient>
<!-- Soft outer glow filter -->
<filter id="pfm-glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="1.5" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- Deep Contrast Foundation Disk for Dark & Light browser tabs -->
<circle cx="32" cy="32" r="31" fill="#090d16" />
<circle cx="32" cy="32" r="30" stroke="#1e293b" stroke-width="1" />
<circle cx="32" cy="32" r="28" fill="url(#pfm-ambient-glow)" />
<!-- Outer Hexagonal Structural Shield Outline -->
<path
d="M32 6.5 L53.5 19 C55 19.8 56 21.5 56 23.2 L56 40.8 C56 42.5 55 44.2 53.5 45 L32 57.5 C30.5 58.3 28.5 58.3 27 57.5 L5.5 45 C4 44.2 3 42.5 3 40.8 L3 23.2 C3 21.5 4 19.8 5.5 19 L27 6.5 C28.5 5.7 30.5 5.7 32 6.5 Z"
stroke="#3b82f6"
stroke-opacity="0.3"
stroke-width="1.2"
fill="none"
/>
<!-- Facet 1: Top Canopy (Governance / Oversight Architecture) -->
<path
d="M32 9 L51.5 20.2 L43 25.2 L32 18.8 L21 25.2 L12.5 20.2 Z"
fill="url(#pfm-grad-top)"
/>
<!-- Facet 2: Left Vault (Fund Management & Capital Allocation) -->
<path
d="M10 23 L19.2 28.4 L19.2 44.5 L30 50.8 L30 55.5 L10 43.8 Z"
fill="url(#pfm-grad-left)"
/>
<!-- Facet 3: Right Wing (Milestone Performance & Impact Assurance) -->
<path
d="M54 23 L54 43.8 L34 55.5 L34 50.8 L44.8 44.5 L44.8 28.4 Z"
fill="url(#pfm-grad-right)"
/>
<!-- Center Geometric Hub (The PFM Nexus Plate) -->
<path
d="M22 28.5 L32 22.8 L42 28.5 L42 35.5 L32 41.2 L22 35.5 Z"
fill="#0b1120"
stroke="#334155"
stroke-width="1"
/>
<!-- Kinetic Stepped Prisms -->
<path
d="M25 31.5 L32 27.5 L39 31.5 L32 35.5 Z"
fill="url(#pfm-grad-top)"
opacity="0.9"
/>
<path
d="M25 37 L32 41 L39 37 L32 33 L25 37 Z"
fill="url(#pfm-grad-right)"
opacity="0.9"
/>
<!-- Center Radiant Milestone Diamond -->
<polygon
points="32,27 36.5,32 32,37 27.5,32"
fill="url(#pfm-grad-core)"
filter="url(#pfm-glow)"
/>
<!-- Center Pure White Optical Star Point -->
<circle cx="32" cy="32" r="1.5" fill="#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+12 -4
View File
@@ -1,6 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Post Fund Management Console</title>
@@ -9,10 +9,18 @@
<meta property="og:description" content="Enterprise backoffice portal for Funders to manage portfolios, programmes, projects, grants, disbursements, and evidence workflows." />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate icon" href="/favicon.svg" />
<link rel="mask-icon" href="/favicon.svg" color="#2563eb" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<!-- Runtime Environment Configuration (Populated dynamically during deployment) -->
<script src="/env-config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</body>
</html>
+1 -1
View File
@@ -2,5 +2,5 @@
"name": "Post Fund Management Console",
"description": "Enterprise backoffice portal for Funders to manage portfolios, programmes, projects, grants, disbursements, and evidence workflows.",
"requestFramePermissions": [],
"majorCapabilities": [ "MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API" ]
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
}
+9 -9
View File
@@ -12,17 +12,17 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/json image/svg+xml;
# Cache control for static assets
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 1y;
access_log off;
add_header Cache-Control "public";
# Never cache dynamic runtime configuration files or index.html
location ~* ^/(?:config\.json|env-config\.js|index\.html)$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
# Disable caching for appsettings.json so changes take effect immediately
location = /appsettings.json {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0";
# Cache control for static assets (CSS, JS, fonts, images)
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff|woff2|ttf|eot)$ {
expires 1y;
access_log off;
add_header Cache-Control "public, immutable";
}
# HTML5 SPA History Mode Support

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