From 107b7f54f192563649265a624e2970a70a9599d9 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 15 Sep 2026 23:51:32 +0200 Subject: [PATCH] Verify mailer credentials against ProcessWeb API before showing E-Mail-Dienst as healthy 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 --- Fuchs.Tests/ProcessWebComServiceTests.cs | 133 +++++++++++++++++++++ Fuchs.Tests/SystemStatusServiceTests.cs | 66 +++++++++- Fuchs/Docs/Concepts/admin-system-status.md | 25 ++-- Fuchs/Services/IComService.cs | 26 ++++ Fuchs/Services/ProcessWebComService.cs | 58 +++++++++ Fuchs/Services/SystemStatusModels.cs | 2 +- Fuchs/Services/SystemStatusService.cs | 29 ++++- Fuchs/js/intranet/modules/fis.admin.js | 13 +- Fuchs/wwwroot/web/fis.admin.de.js | 13 +- Fuchs/wwwroot/web/fis.admin.de.min.js | 2 +- 10 files changed, 339 insertions(+), 28 deletions(-) diff --git a/Fuchs.Tests/ProcessWebComServiceTests.cs b/Fuchs.Tests/ProcessWebComServiceTests.cs index 0792a5f..047bb7d 100644 --- a/Fuchs.Tests/ProcessWebComServiceTests.cs +++ b/Fuchs.Tests/ProcessWebComServiceTests.cs @@ -476,6 +476,139 @@ public class ProcessWebComServiceTests 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 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.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.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.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.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.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.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.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) ──────────────────────────────── [Fact] public async Task SendEmailAsync_Success_IncrementsEmailsSentCounter() diff --git a/Fuchs.Tests/SystemStatusServiceTests.cs b/Fuchs.Tests/SystemStatusServiceTests.cs index 741866e..628d240 100644 --- a/Fuchs.Tests/SystemStatusServiceTests.cs +++ b/Fuchs.Tests/SystemStatusServiceTests.cs @@ -60,13 +60,20 @@ public class SystemStatusServiceTests var env = new Mock(); env.SetupGet(e => e.EnvironmentName).Returns("Development"); + var comMock = com ?? new Mock(); + if (com is null) + { + comMock.Setup(c => c.VerifyCredentialsAsync(It.IsAny())) + .ReturnsAsync(new MailerCredentialsCheckResult { Ok = true, Account = "acc" }); + } + return new SystemStatusService( config ?? Config(), env.Object, provider ?? new ServiceCollection().BuildServiceProvider(), (blob ?? new Mock()).Object, (mfr ?? new Mock()).Object, - (com ?? new Mock()).Object, + comMock.Object, Options.Create(mailer ?? new ProcessWebComSettings { Enabled = true, Token = "tok", BaseUrl = "https://api.example", AccountId = "acc", ServerId = "srv" }), Options.Create(email ?? new FuchsEmailSettings()), Options.Create(blobSettings ?? new AzureBlobStorageSettings { Enabled = true }), @@ -277,6 +284,63 @@ public class SystemStatusServiceTests 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(); + com.Setup(c => c.VerifyCredentialsAsync(It.IsAny())) + .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(); + com.Setup(c => c.VerifyCredentialsAsync(It.IsAny())) + .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 ──────────────────────────────────────────────────────── [Fact] public async Task ProbeKeyVault_ClientNotRegistered_ReturnsUnconfigured() diff --git a/Fuchs/Docs/Concepts/admin-system-status.md b/Fuchs/Docs/Concepts/admin-system-status.md index cf051f2..11adfe3 100644 --- a/Fuchs/Docs/Concepts/admin-system-status.md +++ b/Fuchs/Docs/Concepts/admin-system-status.md @@ -1,6 +1,6 @@ --- status: Active -lastUpdated: 2026-09-09 +lastUpdated: 2026-09-15 applyTo: - "Fuchs/Controllers/IntranetController.Admin.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 + OverrideRecipient), blob (enabled/configured/containers), MFR (host/creds-present/sync), 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`, 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 - 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. + `SELECT @@SERVERNAME, DB_NAME(), SUSER_SNAME()`; `email` calls + `IComService.VerifyCredentialsAsync` (GET `{BaseUrl}/mailer/fc?fn=verify` with the configured + Basic-auth account/token — see the Mailer API contract) and only turns `ok` when the Mailer API + itself confirms the credentials (`ok:true`); when `ProcessWebComSettings.ServerId` is configured + it is sent along as `&serverid=...` so the API also confirms this server belongs to the account + (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 the `Fuchs:Email:OverrideRecipient` redirect applies exactly as for any other mail; the result reports the requested recipient and, when active, the override target. diff --git a/Fuchs/Services/IComService.cs b/Fuchs/Services/IComService.cs index 5b419cc..037458a 100644 --- a/Fuchs/Services/IComService.cs +++ b/Fuchs/Services/IComService.cs @@ -27,4 +27,30 @@ public interface IComService /// Text message body. /// true when the SMS was accepted successfully. Task SendSmsAsync(string mobile, string message); + + /// + /// Verifies the configured account/token against the Mailer API's credential-check + /// endpoint (GET {BaseUrl}/mailer/fc?fn=verify) without sending anything. + /// + Task VerifyCredentialsAsync(CancellationToken cancellationToken = default); +} + +/// Outcome of a Mailer API credential verification (see ). +public sealed class MailerCredentialsCheckResult +{ + /// true only when the Mailer API confirmed the credentials (HTTP 200, ok:true). + public bool Ok { get; init; } + + /// Account name the Mailer API echoed back, when verified. + public string? Account { get; init; } + + /// + /// Server ID the Mailer API echoed back. Only present when a serverid was sent with + /// the request (i.e. is configured); in that + /// case is only true when this server belongs to the account too. + /// + public string? ServerId { get; init; } + + /// Human-readable failure detail (status code / body / exception message), never the token. + public string? Detail { get; init; } } diff --git a/Fuchs/Services/ProcessWebComService.cs b/Fuchs/Services/ProcessWebComService.cs index 648d145..6f35e2e 100644 --- a/Fuchs/Services/ProcessWebComService.cs +++ b/Fuchs/Services/ProcessWebComService.cs @@ -199,6 +199,64 @@ public class ProcessWebComService : IComService } } + public async Task 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(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 async Task<(bool ok, System.Net.HttpStatusCode status, string body)> PostToApiAsync(string reference, string comtype, object communication) diff --git a/Fuchs/Services/SystemStatusModels.cs b/Fuchs/Services/SystemStatusModels.cs index 92dcb36..4ec2ca1 100644 --- a/Fuchs/Services/SystemStatusModels.cs +++ b/Fuchs/Services/SystemStatusModels.cs @@ -6,7 +6,7 @@ namespace Fuchs.Services; /// public sealed class SystemProbeResult { - /// Probe identifier: database, keyvault, blob or mfr. + /// Probe identifier: database, email, keyvault, blob or mfr. public string Component { get; init; } = ""; /// ok | error | disabled | unconfigured. diff --git a/Fuchs/Services/SystemStatusService.cs b/Fuchs/Services/SystemStatusService.cs index 7530bfe..b236032 100644 --- a/Fuchs/Services/SystemStatusService.cs +++ b/Fuchs/Services/SystemStatusService.cs @@ -20,7 +20,7 @@ namespace Fuchs.Services; /// 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 IHostEnvironment _environment; @@ -177,6 +177,7 @@ public sealed class SystemStatusService : ISystemStatusService result = component switch { "database" => await ProbeDatabaseAsync(cancellationToken), + "email" => await ProbeEmailAsync(cancellationToken), "keyvault" => await ProbeKeyVaultAsync(cancellationToken), "blob" => await ProbeBlobAsync(cancellationToken), "mfr" => await ProbeMfrAsync(cancellationToken), @@ -268,6 +269,32 @@ public sealed class SystemStatusService : ISystemStatusService } } + private async Task 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 ProbeKeyVaultAsync(CancellationToken ct) { string appName = _configuration["SecretManagement:AppName"] ?? ""; diff --git a/Fuchs/js/intranet/modules/fis.admin.js b/Fuchs/js/intranet/modules/fis.admin.js index 5690598..e5b2b6e 100644 --- a/Fuchs/js/intranet/modules/fis.admin.js +++ b/Fuchs/js/intranet/modules/fis.admin.js @@ -38,7 +38,7 @@ let $adm = { $$.dc('adm_sec', frm).text($adt.sub_status); let grid = $$.dc('adm_grid', frm); $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.mfrCard(grid, info.mfr || {}, pmap.mfr); $adm.kvCard(grid, info.keyVault || {}, pmap.keyvault); @@ -176,12 +176,7 @@ let $adm = { [$adt.login, d.userId] ], probe); }, - mailCard: function (grid, e) { - 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); + mailCard: function (grid, e, probe) { let rows = [ [$adt.mailer, $adm.yesNo(e.mailerEnabled === true)], [$adt.baseurl, e.baseUrl], @@ -191,7 +186,9 @@ let $adm = { [$adt.overrideActive, $adm.yesNo(e.overrideActive === true)] ]; 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); $$.bbtn($adt.testmail_btn, 'adm_btn adm_btn_primary').appendTo(ft).click($adm.testMailDlg); }, diff --git a/Fuchs/wwwroot/web/fis.admin.de.js b/Fuchs/wwwroot/web/fis.admin.de.js index 3084ebd..789f7bb 100644 --- a/Fuchs/wwwroot/web/fis.admin.de.js +++ b/Fuchs/wwwroot/web/fis.admin.de.js @@ -123,7 +123,7 @@ let $adm = { $$.dc('adm_sec', frm).text($adt.sub_status); let grid = $$.dc('adm_grid', frm); $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.mfrCard(grid, info.mfr || {}, pmap.mfr); $adm.kvCard(grid, info.keyVault || {}, pmap.keyvault); @@ -261,12 +261,7 @@ let $adm = { [$adt.login, d.userId] ], probe); }, - mailCard: function (grid, e) { - 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); + mailCard: function (grid, e, probe) { let rows = [ [$adt.mailer, $adm.yesNo(e.mailerEnabled === true)], [$adt.baseurl, e.baseUrl], @@ -276,7 +271,9 @@ let $adm = { [$adt.overrideActive, $adm.yesNo(e.overrideActive === true)] ]; 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); $$.bbtn($adt.testmail_btn, 'adm_btn adm_btn_primary').appendTo(ft).click($adm.testMailDlg); }, diff --git a/Fuchs/wwwroot/web/fis.admin.de.min.js b/Fuchs/wwwroot/web/fis.admin.de.min.js index ee63e9c..0a7baa5 100644 --- a/Fuchs/wwwroot/web/fis.admin.de.min.js +++ b/Fuchs/wwwroot/web/fis.admin.de.min.js @@ -1 +1 @@ -let $adt={mdl:"Systemüberwachung",sub_info:"System",sub_status:"Status & Konnektivität",sub_tests:"Tests",startup:"Startup-Prüfungen",startup_notrun:"Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).",startup_on:"Host",startup_at:"Ausgeführt (UTC)",chk_ok:"OK",chk_fail:"Fehler",chk_skipped:"übersprungen",chk_KeyVault:"Azure Key Vault",chk_Database:"SQL-Datenbank",chk_MFR:"MFR (ERP)",chk_PdfLicense:"Spire.PDF-Lizenz",chk_Mailer:"E-Mail (Startup-Probe)",host:"Host / Server",machine:"Hostname",os:"Betriebssystem",framework:"Framework",env:"Umgebung",testdep:"Test-Deployment",pid:"Prozess-ID",uptime:"Laufzeit",since:"Start (UTC)",hours:"Std.",db:"SQL-Datenbank",mail:"E-Mail-Dienst",blob:"Azure Blob Storage",mfr:"MFR (ERP)",kv:"Azure Key Vault",server:"Server",catalog:"Datenbank",login:"Login",mailer:"Mailer aktiv",baseurl:"Base-URL",account:"Account",serverid:"Server-ID",token:"Token hinterlegt",override:"OverrideRecipient",overrideActive:"Umleitung aktiv",enabled:"Aktiviert",configured:"Konfiguriert",invContainer:"Rechnungs-Container",remContainer:"Mahnungs-Container",host_l:"Host",creds:"Zugangsdaten hinterlegt",sync:"Sync aktiv",vault:"Vault-URI",appname:"App-Präfix",seccount:"Verwaltete Secrets",clientreg:"Client registriert",duration:"Dauer",checked:"Geprüft",detail:"Detail",st_ok:"OK",st_error:"Fehler",st_disabled:"Deaktiviert",st_unconfigured:"Nicht konfiguriert",st_checking:"Prüfe…",refresh:"Aktualisieren",refreshAll:"Alle prüfen",yes:"Ja",no:"Nein",testmail:"Test-E-Mail",testmail_btn:"Test-E-Mail senden",tm_to:"Empfänger",tm_subject:"Betreff",tm_body:"Nachricht",tm_hint:"Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.",tm_sent:"Test-E-Mail übergeben.",tm_failed:"Test-E-Mail konnte nicht gesendet werden.",tm_default_subject:"Fuchs Intranet – Test-E-Mail",tm_default_body:"Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.",loadfail:"Der Systemstatus konnte nicht geladen werden."},$adm={init2:function(){$ocms.getScript([],(function(){$adm.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($adt.mdl),$adm.topbar();let e=$$.dc("admfrm",$("#contentframe")).ldng(1);$adm.load(e)},topbar:function(){return $("#topbar").ocmsmenu([{lbl:$adt.refreshAll,id:"adm_refresh_all",ico:"glyphicon glyphicon-refresh",fnc:function(){$adm.init3()}}])},load:function(e){$ocms.postXT({url:$ocms.url("admin/status"),success:function(t){e.empty(),$adm.render(e,t.info||{},t.probes||[])},error:function(){e.empty(),$$.dc("adm_err",e).text($adt.loadfail)},complete:function(){e.ldng(0)}})},render:function(e,t,a){let d={};$.each(a,(function(e,t){d[t.component]=t})),$$.dc("adm_sec",e).text($adt.sub_info),$adm.sysCard(e,t),$adm.startupCard(e,t.startupChecks),$$.dc("adm_sec",e).text($adt.sub_status);let r=$$.dc("adm_grid",e);$adm.dbCard(r,t.database||{},d.database),$adm.mailCard(r,t.email||{}),$adm.blobCard(r,t.blob||{},d.blob),$adm.mfrCard(r,t.mfr||{},d.mfr),$adm.kvCard(r,t.keyVault||{},d.keyvault)},kvTable:function(e,t){let a=$$.tbl().addClass("adm_kv");return $.each(t||[],(function(e,t){if(!t)return!0;let d=$$.tr(a);$$.td(d).addClass("k").text(null==t[0]?"":String(t[0]));let r=$$.td(d).addClass("v");t[1]instanceof jQuery?r.append(t[1]):r.text(null==t[1]?"":String(t[1]))})),e instanceof jQuery&&e.append(a),a},yesNo:function(e){return $$.sc("adm_bool adm_bool_"+(e?"y":"n"),e?$adt.yes:$adt.no)},setPill:function(e,t){let a=(t=t||{}).status||"unconfigured",d={ok:$adt.st_ok,error:$adt.st_error,disabled:$adt.st_disabled,unconfigured:$adt.st_unconfigured,checking:$adt.st_checking}[a]||a;return e.attr("class","adm_pill adm_pill_"+a).text(d),e},probeCard:function(e,t,a,d,r){let n=$$.dc("adm_card",e),i=$$.dc("adm_card_hd",n);$$.s(t).appendTo(i);let s=$$.sc("adm_pill").appendTo(i),c=$$.bbtn($adt.refresh,"adm_btn").appendTo(i),o=$$.dc("adm_card_bd",n);$adm.kvTable(o,d);let l=$$.dc("adm_probe",o),m=$$.dc("adm_probe_msg",l),u=$$.dc("adm_probe_metrics",l),_=$$.dc("adm_probe_det",l),p=$$.dc("adm_probe_dur",l),b=function(e){e=e||{},$adm.setPill(s,e),m.text(e.message||""),u.empty(),$.each(e.metrics||[],(function(e,t){let a=$$.dc("adm_metric",u);$$.sc("adm_metric_k").text(t.label||"").appendTo(a),$$.sc("adm_metric_v").text(null==t.value?"":String(t.value)).appendTo(a)})),_.text(e.detail||""),p.text(null!=e.durationMs?e.durationMs+" ms":"")};return b(r),c.click((function(){$adm.setPill(s,{status:"checking"}),m.text(""),u.empty(),_.text(""),p.text(""),$ocms.postXT({url:$ocms.url("admin/probe/"+a),success:function(e){b(e.probe)},error:function(){$adm.setPill(s,{status:"error"}),m.text($adt.st_error)}})})),n},sysCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.host).appendTo(d);let r=$$.sc("adm_env").text(t.environment||"");!0===t.isTestDeployment&&r.aC("adm_env_test"),r.appendTo(d);let n=$$.dc("adm_card_bd",a);$adm.kvTable(n,[[$adt.machine,t.machineName],[$adt.os,t.osDescription],[$adt.framework,t.frameworkDescription],[$adt.env,t.environment],[$adt.testdep,$adm.yesNo(!0===t.isTestDeployment)],[$adt.pid,t.processId],[$adt.since,t.processStartUtc?fdt(t.processStartUtc):""],[$adt.uptime,null!=t.uptimeHours?t.uptimeHours+" "+$adt.hours:""]])},startupCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.startup).appendTo(d);let r=$$.dc("adm_card_bd",a);if(!t||!0!==t.ran)return $adm.setPill($$.sc("adm_pill").appendTo(d),{status:"disabled"}),void $$.dc("adm_note",r).text($adt.startup_notrun);let n=t.items||[],i=!1;$.each(n,(function(e,t){!0===t.enabled&&!0!==t.ok&&(i=!0)})),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:i?"error":"ok"}),$adm.kvTable(r,[[$adt.startup_on,t.machineName],[$adt.startup_at,t.completedUtc?fdt(t.completedUtc):""]]);let s=$$.dc("adm_checks",r);$.each(n,(function(e,t){let a=$$.dc("adm_check",s);$$.sc("adm_check_k").text($adt["chk_"+t.name]||t.name).appendTo(a);let d=!0!==t.enabled?"skipped":!0===t.ok?"ok":"fail",r={ok:$adt.chk_ok,fail:$adt.chk_fail,skipped:$adt.chk_skipped}[d];$$.sc("adm_check_v adm_check_"+d).text(r).appendTo(a)}))},dbCard:function(e,t,a){$adm.probeCard(e,$adt.db,"database",[[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.server,t.server],[$adt.catalog,t.catalog],[$adt.login,t.userId]],a)},mailCard:function(e,t){let a=$$.dc("adm_card",e),d=$$.dc("adm_card_hd",a);$$.s($adt.mail).appendTo(d),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:!0===t.mailerEnabled?"ok":"disabled"});let r=$$.dc("adm_card_bd",a),n=[[$adt.mailer,$adm.yesNo(!0===t.mailerEnabled)],[$adt.baseurl,t.baseUrl],[$adt.account,t.accountId],[$adt.serverid,t.serverId],[$adt.token,$adm.yesNo(!0===t.tokenConfigured)],[$adt.overrideActive,$adm.yesNo(!0===t.overrideActive)]];!0===t.overrideActive&&n.push([$adt.override,t.overrideRecipient]),$adm.kvTable(r,n);let i=$$.dc("adm_card_ft",a);$$.bbtn($adt.testmail_btn,"adm_btn adm_btn_primary").appendTo(i).click($adm.testMailDlg)},blobCard:function(e,t,a){$adm.probeCard(e,$adt.blob,"blob",[[$adt.enabled,$adm.yesNo(!0===t.enabled)],[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.invContainer,t.invoiceContainer],[$adt.remContainer,t.reminderContainer]],a)},mfrCard:function(e,t,a){$adm.probeCard(e,$adt.mfr,"mfr",[[$adt.host_l,t.host],[$adt.creds,$adm.yesNo(!0===t.credentialsConfigured)],[$adt.sync,$adm.yesNo(!0===t.syncEnabled)]],a)},kvCard:function(e,t,a){$adm.probeCard(e,$adt.kv,"keyvault",[[$adt.vault,t.vaultUri],[$adt.appname,t.appName],[$adt.seccount,t.managedSecretCount],[$adt.clientreg,$adm.yesNo(!0===t.clientRegistered)]],a)},testMailDlg:function(){$ocms.dlgform([{name:"to",label:$adt.tm_to,type:"email",required:!0,value:$ocms.auth.email||""},{name:"subject",label:$adt.tm_subject,type:"string",required:!0,value:$adt.tm_default_subject},{name:"body",label:$adt.tm_body,type:"text",required:!0,value:$adt.tm_default_body}],{title:$adt.testmail,button:$adt.testmail_btn,size:[420,560],addcontent:$$.dc("adm_note").text($adt.tm_hint),url:$ocms.url("admin/testmail"),typedvalues:!0,success:function(e){let t=e&&e.result||{};alert(t.message||(!0===t.sent?$adt.tm_sent:$adt.tm_failed))}})}},$$adm={init2:$adm.init2,auth:{}};export default $$adm; \ No newline at end of file +let $adt={mdl:"Systemüberwachung",sub_info:"System",sub_status:"Status & Konnektivität",sub_tests:"Tests",startup:"Startup-Prüfungen",startup_notrun:"Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).",startup_on:"Host",startup_at:"Ausgeführt (UTC)",chk_ok:"OK",chk_fail:"Fehler",chk_skipped:"übersprungen",chk_KeyVault:"Azure Key Vault",chk_Database:"SQL-Datenbank",chk_MFR:"MFR (ERP)",chk_PdfLicense:"Spire.PDF-Lizenz",chk_Mailer:"E-Mail (Startup-Probe)",host:"Host / Server",machine:"Hostname",os:"Betriebssystem",framework:"Framework",env:"Umgebung",testdep:"Test-Deployment",pid:"Prozess-ID",uptime:"Laufzeit",since:"Start (UTC)",hours:"Std.",db:"SQL-Datenbank",mail:"E-Mail-Dienst",blob:"Azure Blob Storage",mfr:"MFR (ERP)",kv:"Azure Key Vault",server:"Server",catalog:"Datenbank",login:"Login",mailer:"Mailer aktiv",baseurl:"Base-URL",account:"Account",serverid:"Server-ID",token:"Token hinterlegt",override:"OverrideRecipient",overrideActive:"Umleitung aktiv",enabled:"Aktiviert",configured:"Konfiguriert",invContainer:"Rechnungs-Container",remContainer:"Mahnungs-Container",host_l:"Host",creds:"Zugangsdaten hinterlegt",sync:"Sync aktiv",vault:"Vault-URI",appname:"App-Präfix",seccount:"Verwaltete Secrets",clientreg:"Client registriert",duration:"Dauer",checked:"Geprüft",detail:"Detail",st_ok:"OK",st_error:"Fehler",st_disabled:"Deaktiviert",st_unconfigured:"Nicht konfiguriert",st_checking:"Prüfe…",refresh:"Aktualisieren",refreshAll:"Alle prüfen",yes:"Ja",no:"Nein",testmail:"Test-E-Mail",testmail_btn:"Test-E-Mail senden",tm_to:"Empfänger",tm_subject:"Betreff",tm_body:"Nachricht",tm_hint:"Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.",tm_sent:"Test-E-Mail übergeben.",tm_failed:"Test-E-Mail konnte nicht gesendet werden.",tm_default_subject:"Fuchs Intranet – Test-E-Mail",tm_default_body:"Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.",loadfail:"Der Systemstatus konnte nicht geladen werden."},$adm={init2:function(){$ocms.getScript([],(function(){$adm.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($adt.mdl),$adm.topbar();let e=$$.dc("admfrm",$("#contentframe")).ldng(1);$adm.load(e)},topbar:function(){return $("#topbar").ocmsmenu([{lbl:$adt.refreshAll,id:"adm_refresh_all",ico:"glyphicon glyphicon-refresh",fnc:function(){$adm.init3()}}])},load:function(e){$ocms.postXT({url:$ocms.url("admin/status"),success:function(t){e.empty(),$adm.render(e,t.info||{},t.probes||[])},error:function(){e.empty(),$$.dc("adm_err",e).text($adt.loadfail)},complete:function(){e.ldng(0)}})},render:function(e,t,a){let d={};$.each(a,(function(e,t){d[t.component]=t})),$$.dc("adm_sec",e).text($adt.sub_info),$adm.sysCard(e,t),$adm.startupCard(e,t.startupChecks),$$.dc("adm_sec",e).text($adt.sub_status);let r=$$.dc("adm_grid",e);$adm.dbCard(r,t.database||{},d.database),$adm.mailCard(r,t.email||{},d.email),$adm.blobCard(r,t.blob||{},d.blob),$adm.mfrCard(r,t.mfr||{},d.mfr),$adm.kvCard(r,t.keyVault||{},d.keyvault)},kvTable:function(e,t){let a=$$.tbl().addClass("adm_kv");return $.each(t||[],(function(e,t){if(!t)return!0;let d=$$.tr(a);$$.td(d).addClass("k").text(null==t[0]?"":String(t[0]));let r=$$.td(d).addClass("v");t[1]instanceof jQuery?r.append(t[1]):r.text(null==t[1]?"":String(t[1]))})),e instanceof jQuery&&e.append(a),a},yesNo:function(e){return $$.sc("adm_bool adm_bool_"+(e?"y":"n"),e?$adt.yes:$adt.no)},setPill:function(e,t){let a=(t=t||{}).status||"unconfigured",d={ok:$adt.st_ok,error:$adt.st_error,disabled:$adt.st_disabled,unconfigured:$adt.st_unconfigured,checking:$adt.st_checking}[a]||a;return e.attr("class","adm_pill adm_pill_"+a).text(d),e},probeCard:function(e,t,a,d,r){let n=$$.dc("adm_card",e),i=$$.dc("adm_card_hd",n);$$.s(t).appendTo(i);let s=$$.sc("adm_pill").appendTo(i),o=$$.bbtn($adt.refresh,"adm_btn").appendTo(i),c=$$.dc("adm_card_bd",n);$adm.kvTable(c,d);let l=$$.dc("adm_probe",c),m=$$.dc("adm_probe_msg",l),u=$$.dc("adm_probe_metrics",l),p=$$.dc("adm_probe_det",l),_=$$.dc("adm_probe_dur",l),b=function(e){e=e||{},$adm.setPill(s,e),m.text(e.message||""),u.empty(),$.each(e.metrics||[],(function(e,t){let a=$$.dc("adm_metric",u);$$.sc("adm_metric_k").text(t.label||"").appendTo(a),$$.sc("adm_metric_v").text(null==t.value?"":String(t.value)).appendTo(a)})),p.text(e.detail||""),_.text(null!=e.durationMs?e.durationMs+" ms":"")};return b(r),o.click((function(){$adm.setPill(s,{status:"checking"}),m.text(""),u.empty(),p.text(""),_.text(""),$ocms.postXT({url:$ocms.url("admin/probe/"+a),success:function(e){b(e.probe)},error:function(){$adm.setPill(s,{status:"error"}),m.text($adt.st_error)}})})),n},sysCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.host).appendTo(d);let r=$$.sc("adm_env").text(t.environment||"");!0===t.isTestDeployment&&r.aC("adm_env_test"),r.appendTo(d);let n=$$.dc("adm_card_bd",a);$adm.kvTable(n,[[$adt.machine,t.machineName],[$adt.os,t.osDescription],[$adt.framework,t.frameworkDescription],[$adt.env,t.environment],[$adt.testdep,$adm.yesNo(!0===t.isTestDeployment)],[$adt.pid,t.processId],[$adt.since,t.processStartUtc?fdt(t.processStartUtc):""],[$adt.uptime,null!=t.uptimeHours?t.uptimeHours+" "+$adt.hours:""]])},startupCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.startup).appendTo(d);let r=$$.dc("adm_card_bd",a);if(!t||!0!==t.ran)return $adm.setPill($$.sc("adm_pill").appendTo(d),{status:"disabled"}),void $$.dc("adm_note",r).text($adt.startup_notrun);let n=t.items||[],i=!1;$.each(n,(function(e,t){!0===t.enabled&&!0!==t.ok&&(i=!0)})),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:i?"error":"ok"}),$adm.kvTable(r,[[$adt.startup_on,t.machineName],[$adt.startup_at,t.completedUtc?fdt(t.completedUtc):""]]);let s=$$.dc("adm_checks",r);$.each(n,(function(e,t){let a=$$.dc("adm_check",s);$$.sc("adm_check_k").text($adt["chk_"+t.name]||t.name).appendTo(a);let d=!0!==t.enabled?"skipped":!0===t.ok?"ok":"fail",r={ok:$adt.chk_ok,fail:$adt.chk_fail,skipped:$adt.chk_skipped}[d];$$.sc("adm_check_v adm_check_"+d).text(r).appendTo(a)}))},dbCard:function(e,t,a){$adm.probeCard(e,$adt.db,"database",[[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.server,t.server],[$adt.catalog,t.catalog],[$adt.login,t.userId]],a)},mailCard:function(e,t,a){let d=[[$adt.mailer,$adm.yesNo(!0===t.mailerEnabled)],[$adt.baseurl,t.baseUrl],[$adt.account,t.accountId],[$adt.serverid,t.serverId],[$adt.token,$adm.yesNo(!0===t.tokenConfigured)],[$adt.overrideActive,$adm.yesNo(!0===t.overrideActive)]];!0===t.overrideActive&&d.push([$adt.override,t.overrideRecipient]);let r=$adm.probeCard(e,$adt.mail,"email",d,a),n=$$.dc("adm_card_ft",r);$$.bbtn($adt.testmail_btn,"adm_btn adm_btn_primary").appendTo(n).click($adm.testMailDlg)},blobCard:function(e,t,a){$adm.probeCard(e,$adt.blob,"blob",[[$adt.enabled,$adm.yesNo(!0===t.enabled)],[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.invContainer,t.invoiceContainer],[$adt.remContainer,t.reminderContainer]],a)},mfrCard:function(e,t,a){$adm.probeCard(e,$adt.mfr,"mfr",[[$adt.host_l,t.host],[$adt.creds,$adm.yesNo(!0===t.credentialsConfigured)],[$adt.sync,$adm.yesNo(!0===t.syncEnabled)]],a)},kvCard:function(e,t,a){$adm.probeCard(e,$adt.kv,"keyvault",[[$adt.vault,t.vaultUri],[$adt.appname,t.appName],[$adt.seccount,t.managedSecretCount],[$adt.clientreg,$adm.yesNo(!0===t.clientRegistered)]],a)},testMailDlg:function(){$ocms.dlgform([{name:"to",label:$adt.tm_to,type:"email",required:!0,value:$ocms.auth.email||""},{name:"subject",label:$adt.tm_subject,type:"string",required:!0,value:$adt.tm_default_subject},{name:"body",label:$adt.tm_body,type:"text",required:!0,value:$adt.tm_default_body}],{title:$adt.testmail,button:$adt.testmail_btn,size:[420,560],addcontent:$$.dc("adm_note").text($adt.tm_hint),url:$ocms.url("admin/testmail"),typedvalues:!0,success:function(e){let t=e&&e.result||{};alert(t.message||(!0===t.sent?$adt.tm_sent:$adt.tm_failed))}})}},$$adm={init2:$adm.init2,auth:{}};export default $$adm; \ No newline at end of file