Verify mailer credentials against ProcessWeb API before showing E-Mail-Dienst as healthy
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
The Admin status pill previously turned green whenever the mailer was enabled in config, without proving the account/token actually work. Add IComService.VerifyCredentialsAsync (GET /mailer/fc?fn=verify, also passing serverid when configured so the API confirms this server belongs to the account) and wire it into a new "email" system-status probe, so the pill only goes green once the Mailer API itself confirms the credentials. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f36f65789e
commit
107b7f54f1
@@ -476,6 +476,139 @@ public class ProcessWebComServiceTests
|
|||||||
Assert.Equal(0, handler.CallCount);
|
Assert.Equal(0, handler.CallCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Credential verification (GET /mailer/fc?fn=verify) ─────────────────────
|
||||||
|
private sealed class CapturingRequestHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
private readonly HttpStatusCode _status;
|
||||||
|
private readonly string _body;
|
||||||
|
public HttpRequestMessage? LastRequest { get; private set; }
|
||||||
|
|
||||||
|
public CapturingRequestHandler(HttpStatusCode status, string body) { _status = status; _body = body; }
|
||||||
|
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
LastRequest = request;
|
||||||
|
return Task.FromResult(new HttpResponseMessage(_status) { Content = new StringContent(_body) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyCredentialsAsync_MailerConfirms_ReturnsOkWithAccount()
|
||||||
|
{
|
||||||
|
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":true,\"account\":\"acc-123\"}");
|
||||||
|
var svc = new ProcessWebComService(
|
||||||
|
NullLogger<ProcessWebComService>.Instance, intranet: null!,
|
||||||
|
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok" }),
|
||||||
|
Options.Create(new FuchsEmailSettings()),
|
||||||
|
new StubHttpClientFactory(handler));
|
||||||
|
|
||||||
|
var result = await svc.VerifyCredentialsAsync();
|
||||||
|
|
||||||
|
Assert.True(result.Ok);
|
||||||
|
Assert.Equal("acc-123", result.Account);
|
||||||
|
Assert.NotNull(handler.LastRequest);
|
||||||
|
Assert.Equal(HttpMethod.Get, handler.LastRequest!.Method);
|
||||||
|
Assert.Equal("https://mailer.test/mailer/fc?fn=verify", handler.LastRequest.RequestUri!.ToString());
|
||||||
|
Assert.Equal("Basic", handler.LastRequest.Headers.Authorization!.Scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyCredentialsAsync_ServerIdConfigured_IsSentAndEchoedOnSuccess()
|
||||||
|
{
|
||||||
|
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":true,\"account\":\"acc-123\",\"serverid\":\"srv-1\"}");
|
||||||
|
var svc = new ProcessWebComService(
|
||||||
|
NullLogger<ProcessWebComService>.Instance, intranet: null!,
|
||||||
|
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok", ServerId = "srv-1" }),
|
||||||
|
Options.Create(new FuchsEmailSettings()),
|
||||||
|
new StubHttpClientFactory(handler));
|
||||||
|
|
||||||
|
var result = await svc.VerifyCredentialsAsync();
|
||||||
|
|
||||||
|
Assert.True(result.Ok);
|
||||||
|
Assert.Equal("srv-1", result.ServerId);
|
||||||
|
Assert.Equal("https://mailer.test/mailer/fc?fn=verify&serverid=srv-1", handler.LastRequest!.RequestUri!.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyCredentialsAsync_ServerIdNotOwnedByAccount_ReturnsNotOkWithServerIdDetail()
|
||||||
|
{
|
||||||
|
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":false,\"account\":\"acc-123\",\"serverid\":\"srv-wrong\"}");
|
||||||
|
var svc = new ProcessWebComService(
|
||||||
|
NullLogger<ProcessWebComService>.Instance, intranet: null!,
|
||||||
|
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok", ServerId = "srv-wrong" }),
|
||||||
|
Options.Create(new FuchsEmailSettings()),
|
||||||
|
new StubHttpClientFactory(handler));
|
||||||
|
|
||||||
|
var result = await svc.VerifyCredentialsAsync();
|
||||||
|
|
||||||
|
Assert.False(result.Ok);
|
||||||
|
Assert.Contains("srv-wrong", result.Detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyCredentialsAsync_MailerReturnsOkFalse_ReturnsNotOk()
|
||||||
|
{
|
||||||
|
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":false}");
|
||||||
|
var svc = new ProcessWebComService(
|
||||||
|
NullLogger<ProcessWebComService>.Instance, intranet: null!,
|
||||||
|
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok" }),
|
||||||
|
Options.Create(new FuchsEmailSettings()),
|
||||||
|
new StubHttpClientFactory(handler));
|
||||||
|
|
||||||
|
var result = await svc.VerifyCredentialsAsync();
|
||||||
|
|
||||||
|
Assert.False(result.Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyCredentialsAsync_Http401_ReturnsNotOkWithDetail()
|
||||||
|
{
|
||||||
|
var handler = new CapturingRequestHandler(HttpStatusCode.Unauthorized, "unauthorized");
|
||||||
|
var svc = new ProcessWebComService(
|
||||||
|
NullLogger<ProcessWebComService>.Instance, intranet: null!,
|
||||||
|
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "wrong" }),
|
||||||
|
Options.Create(new FuchsEmailSettings()),
|
||||||
|
new StubHttpClientFactory(handler));
|
||||||
|
|
||||||
|
var result = await svc.VerifyCredentialsAsync();
|
||||||
|
|
||||||
|
Assert.False(result.Ok);
|
||||||
|
Assert.Contains("401", result.Detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task VerifyCredentialsAsync_TransportException_ReturnsNotOkWithExceptionDetail()
|
||||||
|
{
|
||||||
|
var svc = new ProcessWebComService(
|
||||||
|
NullLogger<ProcessWebComService>.Instance, intranet: null!,
|
||||||
|
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok" }),
|
||||||
|
Options.Create(new FuchsEmailSettings()),
|
||||||
|
new StubHttpClientFactory(new ThrowingHandler()));
|
||||||
|
|
||||||
|
var result = await svc.VerifyCredentialsAsync();
|
||||||
|
|
||||||
|
Assert.False(result.Ok);
|
||||||
|
Assert.Contains("simulated transport failure", result.Detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "tok")]
|
||||||
|
[InlineData("acct", "")]
|
||||||
|
public async Task VerifyCredentialsAsync_CredentialsNotConfigured_ReturnsNotOkWithoutCallingApi(string accountId, string token)
|
||||||
|
{
|
||||||
|
var handler = new CapturingRequestHandler(HttpStatusCode.OK, "{\"ok\":true}");
|
||||||
|
var svc = new ProcessWebComService(
|
||||||
|
NullLogger<ProcessWebComService>.Instance, intranet: null!,
|
||||||
|
Options.Create(new ProcessWebComSettings { Enabled = true, BaseUrl = "https://mailer.test", AccountId = accountId, Token = token }),
|
||||||
|
Options.Create(new FuchsEmailSettings()),
|
||||||
|
new StubHttpClientFactory(handler));
|
||||||
|
|
||||||
|
var result = await svc.VerifyCredentialsAsync();
|
||||||
|
|
||||||
|
Assert.False(result.Ok);
|
||||||
|
Assert.Null(handler.LastRequest);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Metric emission (performance indicator) ────────────────────────────────
|
// ── Metric emission (performance indicator) ────────────────────────────────
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendEmailAsync_Success_IncrementsEmailsSentCounter()
|
public async Task SendEmailAsync_Success_IncrementsEmailsSentCounter()
|
||||||
|
|||||||
@@ -60,13 +60,20 @@ public class SystemStatusServiceTests
|
|||||||
var env = new Mock<IHostEnvironment>();
|
var env = new Mock<IHostEnvironment>();
|
||||||
env.SetupGet(e => e.EnvironmentName).Returns("Development");
|
env.SetupGet(e => e.EnvironmentName).Returns("Development");
|
||||||
|
|
||||||
|
var comMock = com ?? new Mock<IComService>();
|
||||||
|
if (com is null)
|
||||||
|
{
|
||||||
|
comMock.Setup(c => c.VerifyCredentialsAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MailerCredentialsCheckResult { Ok = true, Account = "acc" });
|
||||||
|
}
|
||||||
|
|
||||||
return new SystemStatusService(
|
return new SystemStatusService(
|
||||||
config ?? Config(),
|
config ?? Config(),
|
||||||
env.Object,
|
env.Object,
|
||||||
provider ?? new ServiceCollection().BuildServiceProvider(),
|
provider ?? new ServiceCollection().BuildServiceProvider(),
|
||||||
(blob ?? new Mock<IBlobStorageService>()).Object,
|
(blob ?? new Mock<IBlobStorageService>()).Object,
|
||||||
(mfr ?? new Mock<IMfrClientFactory>()).Object,
|
(mfr ?? new Mock<IMfrClientFactory>()).Object,
|
||||||
(com ?? new Mock<IComService>()).Object,
|
comMock.Object,
|
||||||
Options.Create(mailer ?? new ProcessWebComSettings { Enabled = true, Token = "tok", BaseUrl = "https://api.example", AccountId = "acc", ServerId = "srv" }),
|
Options.Create(mailer ?? new ProcessWebComSettings { Enabled = true, Token = "tok", BaseUrl = "https://api.example", AccountId = "acc", ServerId = "srv" }),
|
||||||
Options.Create(email ?? new FuchsEmailSettings()),
|
Options.Create(email ?? new FuchsEmailSettings()),
|
||||||
Options.Create(blobSettings ?? new AzureBlobStorageSettings { Enabled = true }),
|
Options.Create(blobSettings ?? new AzureBlobStorageSettings { Enabled = true }),
|
||||||
@@ -277,6 +284,63 @@ public class SystemStatusServiceTests
|
|||||||
Assert.False(r.Ok);
|
Assert.False(r.Ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Email probe ────────────────────────────────────────────────────────────
|
||||||
|
[Fact]
|
||||||
|
public async Task ProbeEmail_MailerDisabled_ReturnsDisabled()
|
||||||
|
{
|
||||||
|
var svc = Build(mailer: new ProcessWebComSettings { Enabled = false, AccountId = "acc", Token = "tok" });
|
||||||
|
|
||||||
|
var r = await svc.ProbeAsync("email");
|
||||||
|
|
||||||
|
Assert.Equal("email", r.Component);
|
||||||
|
Assert.Equal("disabled", r.Status);
|
||||||
|
Assert.False(r.Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "tok")]
|
||||||
|
[InlineData("acc", "")]
|
||||||
|
public async Task ProbeEmail_CredentialsMissing_ReturnsUnconfigured(string accountId, string token)
|
||||||
|
{
|
||||||
|
var svc = Build(mailer: new ProcessWebComSettings { Enabled = true, AccountId = accountId, Token = token });
|
||||||
|
|
||||||
|
var r = await svc.ProbeAsync("email");
|
||||||
|
|
||||||
|
Assert.Equal("unconfigured", r.Status);
|
||||||
|
Assert.False(r.Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProbeEmail_MailerConfirms_ReturnsOkWithAccount()
|
||||||
|
{
|
||||||
|
var com = new Mock<IComService>();
|
||||||
|
com.Setup(c => c.VerifyCredentialsAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MailerCredentialsCheckResult { Ok = true, Account = "acc-123" });
|
||||||
|
var svc = Build(com: com);
|
||||||
|
|
||||||
|
var r = await svc.ProbeAsync("email");
|
||||||
|
|
||||||
|
Assert.Equal("email", r.Component);
|
||||||
|
Assert.Equal("ok", r.Status);
|
||||||
|
Assert.True(r.Ok);
|
||||||
|
Assert.Equal("acc-123", r.Detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProbeEmail_MailerRejectsCredentials_ReturnsError()
|
||||||
|
{
|
||||||
|
var com = new Mock<IComService>();
|
||||||
|
com.Setup(c => c.VerifyCredentialsAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MailerCredentialsCheckResult { Ok = false, Detail = "API-Fehler 401 (Unauthorized): {}" });
|
||||||
|
var svc = Build(com: com);
|
||||||
|
|
||||||
|
var r = await svc.ProbeAsync("email");
|
||||||
|
|
||||||
|
Assert.Equal("error", r.Status);
|
||||||
|
Assert.False(r.Ok);
|
||||||
|
Assert.Equal("API-Fehler 401 (Unauthorized): {}", r.Detail);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Key Vault probe ────────────────────────────────────────────────────────
|
// ── Key Vault probe ────────────────────────────────────────────────────────
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ProbeKeyVault_ClientNotRegistered_ReturnsUnconfigured()
|
public async Task ProbeKeyVault_ClientNotRegistered_ReturnsUnconfigured()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: Active
|
status: Active
|
||||||
lastUpdated: 2026-09-09
|
lastUpdated: 2026-09-15
|
||||||
applyTo:
|
applyTo:
|
||||||
- "Fuchs/Controllers/IntranetController.Admin.cs"
|
- "Fuchs/Controllers/IntranetController.Admin.cs"
|
||||||
- "Fuchs/Services/SystemStatusService.cs"
|
- "Fuchs/Services/SystemStatusService.cs"
|
||||||
@@ -56,15 +56,24 @@ click "Administration" → $ocms.init('admin')
|
|||||||
**never the password**), email (mailer enabled/base-url/account/server-id/token-present +
|
**never the password**), email (mailer enabled/base-url/account/server-id/token-present +
|
||||||
OverrideRecipient), blob (enabled/configured/containers), MFR (host/creds-present/sync),
|
OverrideRecipient), blob (enabled/configured/containers), MFR (host/creds-present/sync),
|
||||||
Key Vault (vault-uri/app-prefix/managed-secret-count/client-registered).
|
Key Vault (vault-uri/app-prefix/managed-secret-count/client-registered).
|
||||||
- Probes (`database`, `keyvault`, `blob`, `mfr`) each return a `SystemProbeResult`
|
- Probes (`database`, `email`, `keyvault`, `blob`, `mfr`) each return a `SystemProbeResult`
|
||||||
(`status` = `ok`/`error`/`disabled`/`unconfigured`, `ok`, `message`, `detail`, `durationMs`,
|
(`status` = `ok`/`error`/`disabled`/`unconfigured`, `ok`, `message`, `detail`, `durationMs`,
|
||||||
optional `metrics`). They never throw — failures are captured in the result. `database` runs
|
optional `metrics`). They never throw — failures are captured in the result. `database` runs
|
||||||
`SELECT @@SERVERNAME, DB_NAME(), SUSER_SNAME()`; `keyvault` reads the first managed secret via
|
`SELECT @@SERVERNAME, DB_NAME(), SUSER_SNAME()`; `email` calls
|
||||||
the DI `SecretClient`; `blob` calls `IBlobStorageService.CheckConnectivityAsync` (account-info
|
`IComService.VerifyCredentialsAsync` (GET `{BaseUrl}/mailer/fc?fn=verify` with the configured
|
||||||
request **plus a per-container blob count** for the invoice/reminder containers, surfaced as
|
Basic-auth account/token — see the Mailer API contract) and only turns `ok` when the Mailer API
|
||||||
`metrics` and totalled in the message); `mfr` calls `IMfrClientFactory.Create().GetEntities()`.
|
itself confirms the credentials (`ok:true`); when `ProcessWebComSettings.ServerId` is configured
|
||||||
The generic `metrics` (label/value pairs) is how a probe reports extra facts for display — the
|
it is sent along as `&serverid=...` so the API also confirms this server belongs to the account
|
||||||
blob probe uses it for the file counts.
|
(an account/token that is valid but for a server the account doesn't own comes back `ok:false`
|
||||||
|
with the mismatched `serverid` echoed, surfaced in `detail`); a configured-but-wrong token
|
||||||
|
surfaces as `error`, never as a false-positive `ok` — the `mailerEnabled`/`tokenConfigured` flags
|
||||||
|
in `GetInfo()` describe configuration only and were never proof the credentials actually work;
|
||||||
|
`keyvault` reads
|
||||||
|
the first managed secret via the DI `SecretClient`; `blob` calls
|
||||||
|
`IBlobStorageService.CheckConnectivityAsync` (account-info request **plus a per-container blob
|
||||||
|
count** for the invoice/reminder containers, surfaced as `metrics` and totalled in the message);
|
||||||
|
`mfr` calls `IMfrClientFactory.Create().GetEntities()`. The generic `metrics` (label/value pairs)
|
||||||
|
is how a probe reports extra facts for display — the blob probe uses it for the file counts.
|
||||||
- `SendTestEmailAsync(to, subject, body)` HTML-encodes the body and sends via `IComService`, so
|
- `SendTestEmailAsync(to, subject, body)` HTML-encodes the body and sends via `IComService`, so
|
||||||
the `Fuchs:Email:OverrideRecipient` redirect applies exactly as for any other mail; the result
|
the `Fuchs:Email:OverrideRecipient` redirect applies exactly as for any other mail; the result
|
||||||
reports the requested recipient and, when active, the override target.
|
reports the requested recipient and, when active, the override target.
|
||||||
|
|||||||
@@ -27,4 +27,30 @@ public interface IComService
|
|||||||
/// <param name="message">Text message body.</param>
|
/// <param name="message">Text message body.</param>
|
||||||
/// <returns><c>true</c> when the SMS was accepted successfully.</returns>
|
/// <returns><c>true</c> when the SMS was accepted successfully.</returns>
|
||||||
Task<bool> SendSmsAsync(string mobile, string message);
|
Task<bool> SendSmsAsync(string mobile, string message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the configured account/token against the Mailer API's credential-check
|
||||||
|
/// endpoint (GET {BaseUrl}/mailer/fc?fn=verify) without sending anything.
|
||||||
|
/// </summary>
|
||||||
|
Task<MailerCredentialsCheckResult> VerifyCredentialsAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Outcome of a Mailer API credential verification (see <see cref="IComService.VerifyCredentialsAsync"/>).</summary>
|
||||||
|
public sealed class MailerCredentialsCheckResult
|
||||||
|
{
|
||||||
|
/// <summary><c>true</c> only when the Mailer API confirmed the credentials (HTTP 200, <c>ok:true</c>).</summary>
|
||||||
|
public bool Ok { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Account name the Mailer API echoed back, when verified.</summary>
|
||||||
|
public string? Account { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server ID the Mailer API echoed back. Only present when a <c>serverid</c> was sent with
|
||||||
|
/// the request (i.e. <see cref="ProcessWebComSettings.ServerId"/> is configured); in that
|
||||||
|
/// case <see cref="Ok"/> is only <c>true</c> when this server belongs to the account too.
|
||||||
|
/// </summary>
|
||||||
|
public string? ServerId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Human-readable failure detail (status code / body / exception message), never the token.</summary>
|
||||||
|
public string? Detail { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,64 @@ public class ProcessWebComService : IComService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<MailerCredentialsCheckResult> VerifyCredentialsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_settings.AccountId) || string.IsNullOrWhiteSpace(_settings.Token))
|
||||||
|
return new MailerCredentialsCheckResult { Ok = false, Detail = "Konto-ID oder Token nicht konfiguriert." };
|
||||||
|
|
||||||
|
var client = _httpClientFactory.CreateClient("ProcessWebMailer");
|
||||||
|
string credentials = Convert.ToBase64String(
|
||||||
|
Encoding.UTF8.GetBytes($"{_settings.AccountId}:{_settings.Token}"));
|
||||||
|
|
||||||
|
// Passing serverid makes the Mailer API also confirm that THIS server is one of the
|
||||||
|
// account's usable servers, not just that the account/token pair is valid.
|
||||||
|
string url = $"{_settings.BaseUrl}/mailer/fc?fn=verify";
|
||||||
|
if (!string.IsNullOrWhiteSpace(_settings.ServerId))
|
||||||
|
url += $"&serverid={Uri.EscapeDataString(_settings.ServerId)}";
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await client.SendAsync(request, cancellationToken);
|
||||||
|
string body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return new MailerCredentialsCheckResult
|
||||||
|
{
|
||||||
|
Ok = false,
|
||||||
|
Detail = $"API-Fehler {(int)response.StatusCode} ({response.StatusCode}): {body}",
|
||||||
|
};
|
||||||
|
|
||||||
|
var payload = JsonConvert.DeserializeObject<MailerVerifyResponse>(body);
|
||||||
|
bool ok = payload?.Ok == true;
|
||||||
|
string? detail = ok ? null
|
||||||
|
: (!string.IsNullOrWhiteSpace(payload?.ServerId)
|
||||||
|
? $"Server-ID '{payload!.ServerId}' gehört nicht zu diesem Mailer-Konto."
|
||||||
|
: "Mailer API meldete ungültige Zugangsdaten.");
|
||||||
|
return new MailerCredentialsCheckResult
|
||||||
|
{
|
||||||
|
Ok = ok,
|
||||||
|
Account = payload?.Account,
|
||||||
|
ServerId = payload?.ServerId,
|
||||||
|
Detail = detail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "VerifyCredentialsAsync failed");
|
||||||
|
return new MailerCredentialsCheckResult { Ok = false, Detail = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class MailerVerifyResponse
|
||||||
|
{
|
||||||
|
public bool Ok { get; set; }
|
||||||
|
public string? Account { get; set; }
|
||||||
|
public string? ServerId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Private helpers ────────────────────────────────────────────────────────
|
// ── Private helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task<(bool ok, System.Net.HttpStatusCode status, string body)> PostToApiAsync(string reference, string comtype, object communication)
|
private async Task<(bool ok, System.Net.HttpStatusCode status, string body)> PostToApiAsync(string reference, string comtype, object communication)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Fuchs.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SystemProbeResult
|
public sealed class SystemProbeResult
|
||||||
{
|
{
|
||||||
/// <summary>Probe identifier: <c>database</c>, <c>keyvault</c>, <c>blob</c> or <c>mfr</c>.</summary>
|
/// <summary>Probe identifier: <c>database</c>, <c>email</c>, <c>keyvault</c>, <c>blob</c> or <c>mfr</c>.</summary>
|
||||||
public string Component { get; init; } = "";
|
public string Component { get; init; } = "";
|
||||||
|
|
||||||
/// <summary><c>ok</c> | <c>error</c> | <c>disabled</c> | <c>unconfigured</c>.</summary>
|
/// <summary><c>ok</c> | <c>error</c> | <c>disabled</c> | <c>unconfigured</c>.</summary>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace Fuchs.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SystemStatusService : ISystemStatusService
|
public sealed class SystemStatusService : ISystemStatusService
|
||||||
{
|
{
|
||||||
private static readonly string[] Components = { "database", "keyvault", "blob", "mfr" };
|
private static readonly string[] Components = { "database", "email", "keyvault", "blob", "mfr" };
|
||||||
|
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IHostEnvironment _environment;
|
private readonly IHostEnvironment _environment;
|
||||||
@@ -177,6 +177,7 @@ public sealed class SystemStatusService : ISystemStatusService
|
|||||||
result = component switch
|
result = component switch
|
||||||
{
|
{
|
||||||
"database" => await ProbeDatabaseAsync(cancellationToken),
|
"database" => await ProbeDatabaseAsync(cancellationToken),
|
||||||
|
"email" => await ProbeEmailAsync(cancellationToken),
|
||||||
"keyvault" => await ProbeKeyVaultAsync(cancellationToken),
|
"keyvault" => await ProbeKeyVaultAsync(cancellationToken),
|
||||||
"blob" => await ProbeBlobAsync(cancellationToken),
|
"blob" => await ProbeBlobAsync(cancellationToken),
|
||||||
"mfr" => await ProbeMfrAsync(cancellationToken),
|
"mfr" => await ProbeMfrAsync(cancellationToken),
|
||||||
@@ -268,6 +269,32 @@ public sealed class SystemStatusService : ISystemStatusService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<SystemProbeResult> ProbeEmailAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!_mailer.Enabled)
|
||||||
|
return SystemProbeResult.Disabled("email", "Mailer ist deaktiviert (Fuchs:Mailer:Enabled=false).");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_mailer.AccountId) || string.IsNullOrWhiteSpace(_mailer.Token))
|
||||||
|
return SystemProbeResult.Unconfigured("email", "Konto-ID oder Token nicht konfiguriert.");
|
||||||
|
|
||||||
|
MailerCredentialsCheckResult check = await _comService.VerifyCredentialsAsync(ct);
|
||||||
|
bool checkedServerId = !string.IsNullOrWhiteSpace(_mailer.ServerId);
|
||||||
|
string okMessage = checkedServerId
|
||||||
|
? "Zugangsdaten und Server-ID beim Mailer verifiziert."
|
||||||
|
: "Zugangsdaten beim Mailer verifiziert.";
|
||||||
|
string? okDetail = checkedServerId && !string.IsNullOrWhiteSpace(check.ServerId)
|
||||||
|
? $"{check.Account} (Server: {check.ServerId})"
|
||||||
|
: check.Account;
|
||||||
|
return new SystemProbeResult
|
||||||
|
{
|
||||||
|
Component = "email",
|
||||||
|
Status = check.Ok ? "ok" : "error",
|
||||||
|
Ok = check.Ok,
|
||||||
|
Message = check.Ok ? okMessage : "Zugangsdaten konnten nicht verifiziert werden.",
|
||||||
|
Detail = check.Ok ? okDetail : check.Detail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<SystemProbeResult> ProbeKeyVaultAsync(CancellationToken ct)
|
private async Task<SystemProbeResult> ProbeKeyVaultAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
string appName = _configuration["SecretManagement:AppName"] ?? "";
|
string appName = _configuration["SecretManagement:AppName"] ?? "";
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ let $adm = {
|
|||||||
$$.dc('adm_sec', frm).text($adt.sub_status);
|
$$.dc('adm_sec', frm).text($adt.sub_status);
|
||||||
let grid = $$.dc('adm_grid', frm);
|
let grid = $$.dc('adm_grid', frm);
|
||||||
$adm.dbCard(grid, info.database || {}, pmap.database);
|
$adm.dbCard(grid, info.database || {}, pmap.database);
|
||||||
$adm.mailCard(grid, info.email || {});
|
$adm.mailCard(grid, info.email || {}, pmap.email);
|
||||||
$adm.blobCard(grid, info.blob || {}, pmap.blob);
|
$adm.blobCard(grid, info.blob || {}, pmap.blob);
|
||||||
$adm.mfrCard(grid, info.mfr || {}, pmap.mfr);
|
$adm.mfrCard(grid, info.mfr || {}, pmap.mfr);
|
||||||
$adm.kvCard(grid, info.keyVault || {}, pmap.keyvault);
|
$adm.kvCard(grid, info.keyVault || {}, pmap.keyvault);
|
||||||
@@ -176,12 +176,7 @@ let $adm = {
|
|||||||
[$adt.login, d.userId]
|
[$adt.login, d.userId]
|
||||||
], probe);
|
], probe);
|
||||||
},
|
},
|
||||||
mailCard: function (grid, e) {
|
mailCard: function (grid, e, probe) {
|
||||||
let card = $$.dc('adm_card', grid);
|
|
||||||
let hd = $$.dc('adm_card_hd', card);
|
|
||||||
$$.s($adt.mail).appendTo(hd);
|
|
||||||
$adm.setPill($$.sc('adm_pill').appendTo(hd), { status: e.mailerEnabled === true ? 'ok' : 'disabled' });
|
|
||||||
let bd = $$.dc('adm_card_bd', card);
|
|
||||||
let rows = [
|
let rows = [
|
||||||
[$adt.mailer, $adm.yesNo(e.mailerEnabled === true)],
|
[$adt.mailer, $adm.yesNo(e.mailerEnabled === true)],
|
||||||
[$adt.baseurl, e.baseUrl],
|
[$adt.baseurl, e.baseUrl],
|
||||||
@@ -191,7 +186,9 @@ let $adm = {
|
|||||||
[$adt.overrideActive, $adm.yesNo(e.overrideActive === true)]
|
[$adt.overrideActive, $adm.yesNo(e.overrideActive === true)]
|
||||||
];
|
];
|
||||||
if (e.overrideActive === true) { rows.push([$adt.override, e.overrideRecipient]); }
|
if (e.overrideActive === true) { rows.push([$adt.override, e.overrideRecipient]); }
|
||||||
$adm.kvTable(bd, rows);
|
// The pill only turns green once the credentials are verified against the Mailer API
|
||||||
|
// (probe 'email', GET /mailer/fc?fn=verify) — mailerEnabled alone is not proof they work.
|
||||||
|
let card = $adm.probeCard(grid, $adt.mail, 'email', rows, probe);
|
||||||
let ft = $$.dc('adm_card_ft', card);
|
let ft = $$.dc('adm_card_ft', card);
|
||||||
$$.bbtn($adt.testmail_btn, 'adm_btn adm_btn_primary').appendTo(ft).click($adm.testMailDlg);
|
$$.bbtn($adt.testmail_btn, 'adm_btn adm_btn_primary').appendTo(ft).click($adm.testMailDlg);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ let $adm = {
|
|||||||
$$.dc('adm_sec', frm).text($adt.sub_status);
|
$$.dc('adm_sec', frm).text($adt.sub_status);
|
||||||
let grid = $$.dc('adm_grid', frm);
|
let grid = $$.dc('adm_grid', frm);
|
||||||
$adm.dbCard(grid, info.database || {}, pmap.database);
|
$adm.dbCard(grid, info.database || {}, pmap.database);
|
||||||
$adm.mailCard(grid, info.email || {});
|
$adm.mailCard(grid, info.email || {}, pmap.email);
|
||||||
$adm.blobCard(grid, info.blob || {}, pmap.blob);
|
$adm.blobCard(grid, info.blob || {}, pmap.blob);
|
||||||
$adm.mfrCard(grid, info.mfr || {}, pmap.mfr);
|
$adm.mfrCard(grid, info.mfr || {}, pmap.mfr);
|
||||||
$adm.kvCard(grid, info.keyVault || {}, pmap.keyvault);
|
$adm.kvCard(grid, info.keyVault || {}, pmap.keyvault);
|
||||||
@@ -261,12 +261,7 @@ let $adm = {
|
|||||||
[$adt.login, d.userId]
|
[$adt.login, d.userId]
|
||||||
], probe);
|
], probe);
|
||||||
},
|
},
|
||||||
mailCard: function (grid, e) {
|
mailCard: function (grid, e, probe) {
|
||||||
let card = $$.dc('adm_card', grid);
|
|
||||||
let hd = $$.dc('adm_card_hd', card);
|
|
||||||
$$.s($adt.mail).appendTo(hd);
|
|
||||||
$adm.setPill($$.sc('adm_pill').appendTo(hd), { status: e.mailerEnabled === true ? 'ok' : 'disabled' });
|
|
||||||
let bd = $$.dc('adm_card_bd', card);
|
|
||||||
let rows = [
|
let rows = [
|
||||||
[$adt.mailer, $adm.yesNo(e.mailerEnabled === true)],
|
[$adt.mailer, $adm.yesNo(e.mailerEnabled === true)],
|
||||||
[$adt.baseurl, e.baseUrl],
|
[$adt.baseurl, e.baseUrl],
|
||||||
@@ -276,7 +271,9 @@ let $adm = {
|
|||||||
[$adt.overrideActive, $adm.yesNo(e.overrideActive === true)]
|
[$adt.overrideActive, $adm.yesNo(e.overrideActive === true)]
|
||||||
];
|
];
|
||||||
if (e.overrideActive === true) { rows.push([$adt.override, e.overrideRecipient]); }
|
if (e.overrideActive === true) { rows.push([$adt.override, e.overrideRecipient]); }
|
||||||
$adm.kvTable(bd, rows);
|
// The pill only turns green once the credentials are verified against the Mailer API
|
||||||
|
// (probe 'email', GET /mailer/fc?fn=verify) — mailerEnabled alone is not proof they work.
|
||||||
|
let card = $adm.probeCard(grid, $adt.mail, 'email', rows, probe);
|
||||||
let ft = $$.dc('adm_card_ft', card);
|
let ft = $$.dc('adm_card_ft', card);
|
||||||
$$.bbtn($adt.testmail_btn, 'adm_btn adm_btn_primary').appendTo(ft).click($adm.testMailDlg);
|
$$.bbtn($adt.testmail_btn, 'adm_btn adm_btn_primary').appendTo(ft).click($adm.testMailDlg);
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user