Adds 2 units and docs for unit 3. nfr-requirements plan
This commit is contained in:
+39
@@ -0,0 +1,39 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SlpModularCms.Modules.Master.Config;
|
||||
using SlpModularCms.Modules.Master.Services;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.BackgroundServices;
|
||||
|
||||
public class IntegrityCheckBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<MasterModuleOptions> options,
|
||||
ILogger<IntegrityCheckBackgroundService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var interval = TimeSpan.FromMinutes(options.Value.IntegrityCheckIntervalMinutes);
|
||||
using var timer = new PeriodicTimer(interval);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
await ExecuteTickAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task ExecuteTickAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var service = scope.ServiceProvider.GetRequiredService<ICmsInstanceService>();
|
||||
await service.VerifyIntegrityAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Unhandled error during integrity check tick.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SlpModularCms.Modules.Master.Models;
|
||||
using SlpModularCms.Modules.Master.Services;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
[Authorize(Policy = "OwnerOnly")]
|
||||
public class CmsInstanceController(ICmsInstanceService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll()
|
||||
{
|
||||
var instances = await service.GetAllAsync();
|
||||
return Ok(instances);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Add([FromBody] CreateCmsInstanceRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = await service.AddAsync(request);
|
||||
return CreatedAtAction(nameof(GetAll), null, dto);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/status")]
|
||||
public async Task<IActionResult> UpdateStatus(Guid id, [FromBody] UpdateStatusRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await service.UpdateStatusAsync(id, request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace SlpModularCms.Modules.Master.Data.Entities;
|
||||
|
||||
public class CmsInstance
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public CmsInstanceStatus Status { get; set; } = CmsInstanceStatus.Available;
|
||||
public string? DisableMessage { get; set; }
|
||||
public DateTimeOffset? LastContactedAt { get; set; }
|
||||
public DateTimeOffset? LastStatusPushedAt { get; set; }
|
||||
public DateTimeOffset? LastIntegrityCheckFailedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SlpModularCms.Modules.Master.Data.Entities;
|
||||
|
||||
public enum CmsInstanceStatus
|
||||
{
|
||||
Available = 0,
|
||||
NotAvailable = 1,
|
||||
Inactive = 2
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SlpModularCms.Modules.Master.Data.Entities;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Data;
|
||||
|
||||
public class MasterDbContext(DbContextOptions<MasterDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<CmsInstance> CmsInstances => Set<CmsInstance>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<CmsInstance>(entity =>
|
||||
{
|
||||
entity.ToTable("MasterCmsInstances");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
|
||||
entity.Property(e => e.Url).IsRequired().HasMaxLength(500);
|
||||
entity.Property(e => e.ApiKey).IsRequired().HasMaxLength(1000);
|
||||
entity.Property(e => e.Status).IsRequired();
|
||||
entity.Property(e => e.DisableMessage).HasMaxLength(500);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http.Resilience;
|
||||
using Polly;
|
||||
using SlpModularCms.Core.Modules;
|
||||
using SlpModularCms.Modules.Master.BackgroundServices;
|
||||
using SlpModularCms.Modules.Master.Data;
|
||||
using SlpModularCms.Modules.Master.Config;
|
||||
using SlpModularCms.Modules.Master.Repositories;
|
||||
using SlpModularCms.Modules.Master.Services;
|
||||
|
||||
namespace SlpModularCms.Modules.Master;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class MasterModule : IModule
|
||||
{
|
||||
public string Name => "Master";
|
||||
public string Version => "1.0.0";
|
||||
|
||||
public void RegisterServices(IServiceCollection services)
|
||||
{
|
||||
services.AddDataProtection();
|
||||
services.AddSingleton<IApiKeyProtector, ApiKeyProtector>();
|
||||
|
||||
services.AddOptions<MasterModuleOptions>().BindConfiguration("MasterModule");
|
||||
|
||||
services.AddDbContext<MasterDbContext>((serviceProvider, options) =>
|
||||
{
|
||||
var config = serviceProvider.GetRequiredService<IConfiguration>();
|
||||
options.UseSqlServer(config.GetConnectionString("DefaultConnection"));
|
||||
});
|
||||
|
||||
services.AddScoped<ICmsInstanceRepository, CmsInstanceRepository>();
|
||||
services.AddScoped<MasterServiceDependencies>();
|
||||
services.AddScoped<ICmsInstanceService, CmsInstanceService>();
|
||||
|
||||
services.AddHttpClient<ISlaveApiClient, SlaveApiClient>()
|
||||
.AddResilienceHandler("slave-resilience", (builder, context) =>
|
||||
{
|
||||
var opts = context.ServiceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<MasterModuleOptions>>();
|
||||
builder.AddRetry(new HttpRetryStrategyOptions
|
||||
{
|
||||
MaxRetryAttempts = 2,
|
||||
Delay = TimeSpan.FromSeconds(1),
|
||||
BackoffType = DelayBackoffType.Exponential,
|
||||
UseJitter = true
|
||||
});
|
||||
builder.AddTimeout(TimeSpan.FromSeconds(opts.Value.HttpTimeoutSeconds));
|
||||
});
|
||||
|
||||
services.AddHostedService<IntegrityCheckBackgroundService>();
|
||||
services.AddHttpContextAccessor();
|
||||
}
|
||||
|
||||
public void UseModule(IApplicationBuilder app)
|
||||
{
|
||||
using var scope = app.ApplicationServices.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<MasterDbContext>();
|
||||
db.Database.Migrate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SlpModularCms.Modules.Master.Data.Entities;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Models;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public record CmsInstanceDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string Url,
|
||||
CmsInstanceStatus Status,
|
||||
string? DisableMessage,
|
||||
DateTimeOffset? LastContactedAt,
|
||||
DateTimeOffset? LastStatusPushedAt,
|
||||
DateTimeOffset? LastIntegrityCheckFailedAt
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Models;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public record CreateCmsInstanceRequest(
|
||||
string Name,
|
||||
string Url,
|
||||
string ApiKey
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SlpModularCms.Modules.Master.Data.Entities;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Models;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public record UpdateStatusRequest(
|
||||
CmsInstanceStatus Status,
|
||||
string? DisableMessage
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Models;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public record UpdateStatusResult(
|
||||
bool Success,
|
||||
bool SlaveContactSuccess
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace SlpModularCms.Modules.Master.Config;
|
||||
|
||||
public class MasterModuleOptions
|
||||
{
|
||||
public int IntegrityCheckIntervalMinutes { get; set; } = 60;
|
||||
public int HttpTimeoutSeconds { get; set; } = 10;
|
||||
public string? MasterUrl { get; set; }
|
||||
public int CacheMinutes { get; set; } = 60;
|
||||
public string? ApiKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SlpModularCms.Modules.Master.Data;
|
||||
using SlpModularCms.Modules.Master.Data.Entities;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Repositories;
|
||||
|
||||
public class CmsInstanceRepository(MasterDbContext context) : ICmsInstanceRepository
|
||||
{
|
||||
public async Task<IReadOnlyList<CmsInstance>> GetAllAsync()
|
||||
=> await context.CmsInstances.ToListAsync();
|
||||
|
||||
public async Task<IReadOnlyList<CmsInstance>> GetActiveAsync()
|
||||
=> await context.CmsInstances
|
||||
.Where(c => c.Status != CmsInstanceStatus.Inactive)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<CmsInstance?> GetByIdAsync(Guid id)
|
||||
=> await context.CmsInstances.FindAsync(id);
|
||||
|
||||
public async Task AddAsync(CmsInstance instance)
|
||||
=> await context.CmsInstances.AddAsync(instance);
|
||||
|
||||
public void Update(CmsInstance instance)
|
||||
=> context.CmsInstances.Update(instance);
|
||||
|
||||
public async Task SaveChangesAsync()
|
||||
=> await context.SaveChangesAsync();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using SlpModularCms.Modules.Master.Data.Entities;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Repositories;
|
||||
|
||||
public interface ICmsInstanceRepository
|
||||
{
|
||||
Task<IReadOnlyList<CmsInstance>> GetAllAsync();
|
||||
Task<IReadOnlyList<CmsInstance>> GetActiveAsync();
|
||||
Task<CmsInstance?> GetByIdAsync(Guid id);
|
||||
Task AddAsync(CmsInstance instance);
|
||||
void Update(CmsInstance instance);
|
||||
Task SaveChangesAsync();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Services;
|
||||
|
||||
public class ApiKeyProtector(IDataProtectionProvider provider) : IApiKeyProtector
|
||||
{
|
||||
private readonly IDataProtector _protector = provider.CreateProtector("SlpModularCms.Master.ApiKey");
|
||||
|
||||
public string Protect(string plainApiKey) => _protector.Protect(plainApiKey);
|
||||
public string Unprotect(string encryptedApiKey) => _protector.Unprotect(encryptedApiKey);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SlpModularCms.Modules.Master.Data.Entities;
|
||||
using SlpModularCms.Modules.Master.Models;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Services;
|
||||
|
||||
public class CmsInstanceService(MasterServiceDependencies deps) : ICmsInstanceService
|
||||
{
|
||||
public async Task<IReadOnlyList<CmsInstanceDto>> GetAllAsync()
|
||||
{
|
||||
var instances = await deps.Repository.GetAllAsync();
|
||||
return instances.Select(ToDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<CmsInstanceDto> AddAsync(CreateCmsInstanceRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
throw new ArgumentException("Name is required.", nameof(request));
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
throw new ArgumentException("Url is required.", nameof(request));
|
||||
if (string.IsNullOrWhiteSpace(request.ApiKey))
|
||||
throw new ArgumentException("ApiKey is required.", nameof(request));
|
||||
|
||||
var encryptedKey = deps.ApiKeyProtector.Protect(request.ApiKey);
|
||||
var instance = new CmsInstance
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = request.Name,
|
||||
Url = request.Url,
|
||||
ApiKey = encryptedKey,
|
||||
Status = CmsInstanceStatus.Available
|
||||
};
|
||||
|
||||
await deps.Repository.AddAsync(instance);
|
||||
await deps.Repository.SaveChangesAsync();
|
||||
|
||||
var masterUrl = ResolveMasterUrl();
|
||||
if (masterUrl is not null)
|
||||
{
|
||||
var registered = await deps.SlaveClient.RegisterMasterAsync(instance.Url, request.ApiKey, masterUrl);
|
||||
if (registered)
|
||||
{
|
||||
instance.LastContactedAt = DateTimeOffset.UtcNow;
|
||||
deps.Repository.Update(instance);
|
||||
await deps.Repository.SaveChangesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
deps.Logger.LogWarning("Master registration failed for slave {SlaveUrl}", instance.Url);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
deps.Logger.LogWarning("MasterUrl could not be determined; skipping slave registration for {SlaveUrl}", instance.Url);
|
||||
}
|
||||
|
||||
return ToDto(instance);
|
||||
}
|
||||
|
||||
public async Task<UpdateStatusResult> UpdateStatusAsync(Guid id, UpdateStatusRequest request)
|
||||
{
|
||||
var instance = await deps.Repository.GetByIdAsync(id)
|
||||
?? throw new KeyNotFoundException($"CMS instance {id} not found.");
|
||||
|
||||
if (request.Status == CmsInstanceStatus.NotAvailable && string.IsNullOrWhiteSpace(request.DisableMessage))
|
||||
throw new ArgumentException("DisableMessage is required when setting status to NotAvailable.", nameof(request));
|
||||
|
||||
instance.Status = request.Status;
|
||||
instance.DisableMessage = request.Status == CmsInstanceStatus.Available ? null : request.DisableMessage;
|
||||
deps.Repository.Update(instance);
|
||||
await deps.Repository.SaveChangesAsync();
|
||||
|
||||
if (request.Status == CmsInstanceStatus.Inactive)
|
||||
return new UpdateStatusResult(Success: true, SlaveContactSuccess: true);
|
||||
|
||||
var plainKey = deps.ApiKeyProtector.Unprotect(instance.ApiKey);
|
||||
var pushed = await deps.SlaveClient.PushStatusAsync(
|
||||
instance.Url,
|
||||
plainKey,
|
||||
isAvailable: request.Status == CmsInstanceStatus.Available,
|
||||
disableMessage: instance.DisableMessage);
|
||||
|
||||
if (pushed)
|
||||
{
|
||||
instance.LastStatusPushedAt = DateTimeOffset.UtcNow;
|
||||
deps.Repository.Update(instance);
|
||||
await deps.Repository.SaveChangesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
deps.Logger.LogError("Status push failed for slave {SlaveUrl}", instance.Url);
|
||||
}
|
||||
|
||||
return new UpdateStatusResult(Success: true, SlaveContactSuccess: pushed);
|
||||
}
|
||||
|
||||
public async Task VerifyIntegrityAsync()
|
||||
{
|
||||
var masterUrl = ResolveMasterUrl();
|
||||
if (masterUrl is null)
|
||||
{
|
||||
deps.Logger.LogWarning("MasterUrl not configured; skipping integrity check.");
|
||||
return;
|
||||
}
|
||||
|
||||
var activeInstances = await deps.Repository.GetActiveAsync();
|
||||
|
||||
foreach (var instance in activeInstances)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plainKey = deps.ApiKeyProtector.Unprotect(instance.ApiKey);
|
||||
var registeredUrl = await deps.SlaveClient.GetRegisteredMasterUrlAsync(instance.Url, plainKey);
|
||||
|
||||
if (registeredUrl is null)
|
||||
{
|
||||
deps.Logger.LogWarning("Slave {SlaveUrl} unreachable during integrity check.", instance.Url);
|
||||
instance.LastIntegrityCheckFailedAt = DateTimeOffset.UtcNow;
|
||||
deps.Repository.Update(instance);
|
||||
await deps.Repository.SaveChangesAsync();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(registeredUrl, masterUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
deps.Logger.LogWarning("Slave {SlaveUrl} has wrong master URL '{RegisteredUrl}'; re-registering.", instance.Url, registeredUrl);
|
||||
var reRegistered = await deps.SlaveClient.RegisterMasterAsync(instance.Url, plainKey, masterUrl);
|
||||
if (reRegistered)
|
||||
{
|
||||
instance.LastContactedAt = DateTimeOffset.UtcNow;
|
||||
instance.LastIntegrityCheckFailedAt = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
instance.LastIntegrityCheckFailedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
instance.LastIntegrityCheckFailedAt = null;
|
||||
}
|
||||
|
||||
deps.Repository.Update(instance);
|
||||
await deps.Repository.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
deps.Logger.LogError(ex, "Integrity check error for slave {SlaveUrl}", instance.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string? ResolveMasterUrl()
|
||||
{
|
||||
var httpContext = deps.HttpContextAccessor.HttpContext;
|
||||
if (httpContext is not null)
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
return $"{request.Scheme}://{request.Host}";
|
||||
}
|
||||
|
||||
return deps.Options.Value.MasterUrl;
|
||||
}
|
||||
|
||||
private static CmsInstanceDto ToDto(CmsInstance instance) => new(
|
||||
instance.Id,
|
||||
instance.Name,
|
||||
instance.Url,
|
||||
instance.Status,
|
||||
instance.DisableMessage,
|
||||
instance.LastContactedAt,
|
||||
instance.LastStatusPushedAt,
|
||||
instance.LastIntegrityCheckFailedAt
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SlpModularCms.Modules.Master.Services;
|
||||
|
||||
public interface IApiKeyProtector
|
||||
{
|
||||
string Protect(string plainApiKey);
|
||||
string Unprotect(string encryptedApiKey);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using SlpModularCms.Modules.Master.Models;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Services;
|
||||
|
||||
public interface ICmsInstanceService
|
||||
{
|
||||
Task<IReadOnlyList<CmsInstanceDto>> GetAllAsync();
|
||||
Task<CmsInstanceDto> AddAsync(CreateCmsInstanceRequest request);
|
||||
Task<UpdateStatusResult> UpdateStatusAsync(Guid id, UpdateStatusRequest request);
|
||||
Task VerifyIntegrityAsync();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SlpModularCms.Modules.Master.Services;
|
||||
|
||||
public interface ISlaveApiClient
|
||||
{
|
||||
Task<bool> RegisterMasterAsync(string slaveUrl, string plainApiKey, string masterUrl);
|
||||
Task<bool> PushStatusAsync(string slaveUrl, string plainApiKey, bool isAvailable, string? disableMessage);
|
||||
Task<string?> GetRegisteredMasterUrlAsync(string slaveUrl, string plainApiKey);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SlpModularCms.Modules.Master.Config;
|
||||
using SlpModularCms.Modules.Master.Repositories;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Services;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public record MasterServiceDependencies(
|
||||
ICmsInstanceRepository Repository,
|
||||
ISlaveApiClient SlaveClient,
|
||||
IApiKeyProtector ApiKeyProtector,
|
||||
IOptions<MasterModuleOptions> Options,
|
||||
IHttpContextAccessor HttpContextAccessor,
|
||||
ILogger<CmsInstanceService> Logger
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SlpModularCms.Modules.Master.Services;
|
||||
|
||||
public class SlaveApiClient(HttpClient httpClient) : ISlaveApiClient
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<bool> RegisterMasterAsync(string slaveUrl, string plainApiKey, string masterUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{slaveUrl.TrimEnd('/')}/api/v1/master/register");
|
||||
request.Headers.Add("X-Master-Api-Key", plainApiKey);
|
||||
request.Content = JsonContent.Create(new { MasterUrl = masterUrl }, options: JsonOptions);
|
||||
|
||||
var response = await httpClient.SendAsync(request);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PushStatusAsync(string slaveUrl, string plainApiKey, bool isAvailable, string? disableMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{slaveUrl.TrimEnd('/')}/api/v1/master/status");
|
||||
request.Headers.Add("X-Master-Api-Key", plainApiKey);
|
||||
request.Content = JsonContent.Create(new { IsAvailable = isAvailable, DisableMessage = disableMessage }, options: JsonOptions);
|
||||
|
||||
var response = await httpClient.SendAsync(request);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> GetRegisteredMasterUrlAsync(string slaveUrl, string plainApiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"{slaveUrl.TrimEnd('/')}/api/v1/master/registered-url");
|
||||
request.Headers.Add("X-Master-Api-Key", plainApiKey);
|
||||
|
||||
var response = await httpClient.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<RegisteredMasterUrlResponse>(JsonOptions);
|
||||
return result?.MasterUrl;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private record RegisteredMasterUrlResponse(string? MasterUrl);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>SlpModularCms.Modules.Master.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SlpModularCms.Core\SlpModularCms.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user