diff --git a/Directory.Build.props b/Directory.Build.props index 7786cc4..055150f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -44,6 +44,7 @@ + diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index c75e660..6819b5d 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -4,7 +4,6 @@ using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Health; using PostFundManagement.Domain.Mediator; using PostFundManagement.Infrastructure.Database.Services; -using static PostFundManagement.Domain.Extensions.Constants; var builder = WebApplication.CreateBuilder(args); @@ -45,7 +44,7 @@ builder.Services.AddSkorubaApiService(builder.Configuration); builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>)); builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>)); -builder.Services.AddQuartzScheduler(DefaultSchedulerName, builder.Configuration); +builder.Services.AddQuartzSchedulerClient(DefaultSchedulerName, builder.Configuration); builder.Services.AddEmailServices(builder.Configuration); builder.Services.AddHashServices(builder.Configuration); builder.Services.AddGarageS3(builder.Configuration); @@ -119,7 +118,9 @@ builder.Services.AddCors(options => policy.SetIsOriginAllowed(origin => { if (string.IsNullOrWhiteSpace(origin)) return false; + var clean = origin.TrimEnd('/'); + if (allowedOrigins.Any(a => string.Equals(a.TrimEnd('/'), clean, StringComparison.OrdinalIgnoreCase))) return true; @@ -133,10 +134,7 @@ builder.Services.AddCors(options => return true; } } - catch - { - // ignore parse failure - } + catch { /* eat all exceptions */ } return false; }) diff --git a/PostFundManagement.Api/appsettings.json b/PostFundManagement.Api/appsettings.json index 40d57aa..71aab85 100644 --- a/PostFundManagement.Api/appsettings.json +++ b/PostFundManagement.Api/appsettings.json @@ -31,14 +31,7 @@ "Microsoft.EntityFrameworkCore.Database": "Error", "Microsoft.EntityFrameworkCore.Update": "Error" } - }, - "EmailProcessorCron": "0 */5 * * * ?", - "Email": { - "SmtpServer": "smtp.example.com", - "SmtpPort": 587, - "SenderEmail": "noreply@example.com", - "SenderName": "Example" - }, + }, "HasherSettings": { "MinHashLength": 11 }, diff --git a/PostFundManagement.Domain/Configuration/Email/SmtpSettings.cs b/PostFundManagement.Domain/Configuration/Email/SmtpSettings.cs index 9ba2ac5..4c816e7 100644 --- a/PostFundManagement.Domain/Configuration/Email/SmtpSettings.cs +++ b/PostFundManagement.Domain/Configuration/Email/SmtpSettings.cs @@ -9,4 +9,6 @@ public sealed class SmtpSettings public string? Host { get; set; } public bool UseSsl { get; set; } + + public string? ProcessorJobCron { get; set; } } \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Database/Services/HeadlessTenantContextService.cs b/PostFundManagement.Infrastructure/Database/Services/HeadlessTenantContextService.cs new file mode 100644 index 0000000..2b1e438 --- /dev/null +++ b/PostFundManagement.Infrastructure/Database/Services/HeadlessTenantContextService.cs @@ -0,0 +1,16 @@ +using PostFundManagement.Domain.Abstractions; + +namespace PostFundManagement.Infrastructure.Database.Services; + +public sealed class HeadlessTenantContextService : ITenantContextService +{ + public string? TenantName => "Ifri Technologies"; + + public long TenantId => 1; + + public bool ByPassTenantQueryFilter { get; set; } = true; + + public bool IsAdmin => true; + + public bool HasTenantFilter => true; +} diff --git a/PostFundManagement.Jobs/Dockerfile b/PostFundManagement.Jobs/Dockerfile new file mode 100644 index 0000000..7b5e8fe --- /dev/null +++ b/PostFundManagement.Jobs/Dockerfile @@ -0,0 +1,40 @@ +# 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 props and csproj files for layer caching +COPY ["Directory.Build.props", "./"] +COPY ["Directory.Packages.props", "./"] +COPY ["PostFundManagement.snk", "./"] +COPY ["LICENSE", "./"] +COPY ["PostFundManagement.Jobs/PostFundManagement.Jobs.csproj", "PostFundManagement.Jobs/"] +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.Jobs/PostFundManagement.Jobs.csproj" + +# Copy full source code +COPY . . + +# Build from the project folder using relative filename +WORKDIR "/src/PostFundManagement.Jobs" +RUN dotnet build "PostFundManagement.Jobs.csproj" -c Release -o /app/build + +# Stage 3: Publish +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "PostFundManagement.Jobs.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.Jobs.dll"] diff --git a/PostFundManagement.Jobs/JobsWorker.cs b/PostFundManagement.Jobs/JobsWorker.cs new file mode 100644 index 0000000..8806a7d --- /dev/null +++ b/PostFundManagement.Jobs/JobsWorker.cs @@ -0,0 +1,21 @@ +using PostFundManagement.Domain.Abstractions; + +namespace PostFundManagement.Jobs; + +public class JobsWorker(IJobOrchestrator jobOrchestrator, IFeatureManager featureManager, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if(!await featureManager.IsEnabledAsync(nameof(JobsWorker))) + { + logger.LogInformation("JobsWorker is disabled."); + + return; + } + + // TODO: scheduler reccuring jobs + + logger.LogInformation("JobsWorker is stopping."); + } +} diff --git a/PostFundManagement.Jobs/PostFundManagement.Jobs.csproj b/PostFundManagement.Jobs/PostFundManagement.Jobs.csproj index ed98a43..73c3997 100644 --- a/PostFundManagement.Jobs/PostFundManagement.Jobs.csproj +++ b/PostFundManagement.Jobs/PostFundManagement.Jobs.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -7,6 +7,20 @@ dotnet-PostFundManagement.Jobs-97b98845-fddb-4212-b877-8dac9f7f86e3 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/PostFundManagement.Jobs/Program.cs b/PostFundManagement.Jobs/Program.cs index d744e20..53055b8 100644 --- a/PostFundManagement.Jobs/Program.cs +++ b/PostFundManagement.Jobs/Program.cs @@ -1,7 +1,38 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Mediator; +using PostFundManagement.Infrastructure.Database.Services; using PostFundManagement.Jobs; var builder = Host.CreateApplicationBuilder(args); -builder.Services.AddHostedService(); + +builder.Services.AddFeatureManagement(); + +builder.Services.AddSharedServices(); +builder.Services.AddHostedService(); +builder.Services.AddRegisterServicesWithCaching(builder.Configuration); +builder.Services.AddEndpointAndServiceCaching(builder.Configuration); +builder.Services.AddTransient(); + +builder.Services.AddMediator(); +builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>)); +builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>)); + +builder.Services.AddQuartzScheduler(DefaultSchedulerName, builder.Configuration); +builder.Services.AddEmailServices(builder.Configuration); +builder.Services.AddHashServices(builder.Configuration); +builder.Services.AddGarageS3(builder.Configuration); + +builder.Services.AddHttpClient(); +builder.Services.AddDataProtectionDatabase(builder.Configuration); +builder.Services.AddApplicationDbContext(builder.Configuration); var host = builder.Build(); + +var schedulerFactory = host.Services.GetRequiredService(); +var scheduler = await schedulerFactory.GetScheduler(); + +if (scheduler!.Status != SchedulerStatus.Running) + await scheduler.Start(); + host.Run(); diff --git a/PostFundManagement.Jobs/Worker.cs b/PostFundManagement.Jobs/Worker.cs deleted file mode 100644 index 99a49bb..0000000 --- a/PostFundManagement.Jobs/Worker.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace PostFundManagement.Jobs -{ - public class Worker(ILogger logger) : BackgroundService - { - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); - } - await Task.Delay(1000, stoppingToken); - } - } - } -} diff --git a/PostFundManagement.Jobs/appsettings.json b/PostFundManagement.Jobs/appsettings.json index b2dcdb6..4f50c71 100644 --- a/PostFundManagement.Jobs/appsettings.json +++ b/PostFundManagement.Jobs/appsettings.json @@ -1,8 +1,37 @@ { + "FeatureManagement": { + "JobsWorker": true, + "EmailProcessorJobEnabled": true, + "ServiceCaching": false + }, + "GeneralS3Settings": { + "ServiceUrl": "https://s3.garage.khongisa.co.za", + "Region": "garage", + "BucketName": "pfm", + "CdnBaseUrl": "https://pfm.cdn.khongisa.co.za" + }, + "Monitoring": { + "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889", + "ServiceName": "Pfm.Jobs.DEV" + }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Warning", + "Quartz": "Error", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database": "Error", + "Microsoft.EntityFrameworkCore.Update": "Error" } - } -} + }, + "Email": { + "ProcessorJobCron": "0 */5 * * * ?", + "SmtpServer": "smtp.example.com", + "SmtpPort": 587, + "SenderEmail": "noreply@example.com", + "SenderName": "Example" + }, + "HasherSettings": { + "MinHashLength": 11 + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/PostFundManagement.slnx b/PostFundManagement.slnx index c6fe96b..6e5173b 100644 --- a/PostFundManagement.slnx +++ b/PostFundManagement.slnx @@ -1,4 +1,13 @@ + + + + + + + + + @@ -9,10 +18,7 @@ - - - - - - + + + diff --git a/docker-compose.yml b/docker-compose.yml index 9499da8..9054275 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,55 @@ services: + pfm-scheduler: + image: pfm-scheduler + build: + context: . + dockerfile: ./PostFundManagement.Jobs/Dockerfile + network: host + container_name: pfm-scheduler + environment: + # Service layer caching feature flag + - FeatureManagement__JobsWorker=${FEATURE_JOBS_WORKER:-true}} + - FeatureManagement__EmailProcessorJobEnabled=${FEATURE_EMAIL_PROCESSOR_JOB_ENABLED:-true}} + - FeatureManagement__ServiceCaching=${FEATURE_SERVICE_CACHING:-false}} + + # Header forwarding overrides + - DOTNET_ENVIRONMENT=${ENVIRONMENT:-Development} + + # Postgres Database Overrides + - ConnectionStrings__PfmDatabase=Server=${POSTGRES_HOST:-192.168.1.170};Port=${POSTGRES_PORT:-5432};Database=${POSTGRES_DB:-pfm};User Id=${POSTGRES_USER:-pfm-user};Password=${POSTGRES_PASSWORD:-}; + - ConnectionStrings__PfmSchedulerDatabase=Server=${POSTGRES_HOST:-192.168.1.170};Port=${POSTGRES_PORT:-5432};Database=pfm-scheduler;User Id=${POSTGRES_USER:-pfm-user};Password=${POSTGRES_PASSWORD:-}; + + # Garage S3 Settings + - GeneralS3Settings__ServiceUrl=${GENERAL_S3_SERVICE_URL:-https://s3.garage.khongisa.co.za} + - GeneralS3Settings__Region=${GENERAL_S3_REGION:-garage} + - GeneralS3Settings__BucketName=${GENERAL_S3_BUCKET_NAME:-pfm} + - GeneralS3Settings__CdnBaseUrl=${GENERAL_S3_CDN_BASE_URL:-https://pfm.cdn.khongisa.co.za} + - GeneralS3Settings__AccessKey=${GENERAL_S3_ACCESS_KEY:-} + - GeneralS3Settings__SecretKey=${GENERAL_S3_SECRET_KEY:-} + + # HashIds overrides + - HasherSettings__MinHashLength=${HASHER_MIN_HASH_LENGTH:-11} + - HasherSettings__Salt=${HASHER_SALT:-} + + # Data Protection Overrides + - DataProtection__Certificate=${DATA_PROTECTION_CERTIFICATE:-} + - DataProtection__Password=${DATA_PROTECTION_CERTIFICATE_PASSWORD:-} + + # Monitoring Overrides + - Monitoring__Address=${MONITORING_ADDRESS:-http://aspire-dashboard-service.aspire.svc.cluster.local:18889} + - Monitoring__ServiceName=${MONITORING_SERVICE_NAME:-Pfm.DEV} + - Monitoring__ApiKey=${MONITORING_API_KEY:-} + + # Email Processor Overrides + - Email__ProcessorJobCron=${EMAIL_PROCESSOR_CRON:-0 */5 * * * ?} + - Email__Credentials__Password=${EMAIL_CREDENTIALS_PASSWORD:-} + - Email__Credentials__Username=${EMAIL_CREDENTIALS_USERNAME:-} + - Email__SmtpServer=${EMAIL_SMTP_SERVER:-} + - Email__SmtpPort=${EMAIL_SMTP_PORT:-587} + + # Redis overrides + - ConnectionStrings__Redis=${REDIS_CONNECTION_STRING:-192.168.1.171:6379,ssl=false,abortConnect=false} + restart: unless-stopped pfm-api: image: pfm-api build: @@ -8,10 +59,7 @@ services: container_name: pfm-api ports: - "8075:8080" - environment: - # Service layer caching feature flag - - EnableServiceCaching=false - + environment: # Header forwarding overrides - ASPNETCORE_ENVIRONMENT=${ENVIRONMENT:-Development} - ForwardedHeadersConfiguration__Enabled=true @@ -61,13 +109,6 @@ services: - Monitoring__Address=${MONITORING_ADDRESS:-http://aspire-dashboard-service.aspire.svc.cluster.local:18889} - Monitoring__ServiceName=${MONITORING_SERVICE_NAME:-Pfm.DEV} - Monitoring__ApiKey=${MONITORING_API_KEY:-} - - # Email Processor Overrides - - EmailProcessorCron=${EMAIL_PROCESSOR_CRON:-} - - Email__Credentials__Password=${EMAIL_CREDENTIALS_PASSWORD:-} - - Email__Credentials__Username=${EMAIL_CREDENTIALS_USERNAME:-} - - Email__SmtpServer=${EMAIL_SMTP_SERVER:-} - - Email__SmtpPort=${EMAIL_SMTP_PORT:-587} # Redis overrides - ConnectionStrings__Redis=${REDIS_CONNECTION_STRING:-192.168.1.171:6379,ssl=false,abortConnect=false}