feat(centralui): cert-management UI + Trust action + site relay (T17)
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
@page "/design/connections/{Id:int}/certificates"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireAdmin)]
|
||||
@inject ISiteRepository SiteRepository
|
||||
@inject ICertManagementService CertManagementService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3" data-test="connection-certificates">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm me-3" @onclick="GoBack">← Back</button>
|
||||
<h4 class="mb-0">Server Certificates@(string.IsNullOrEmpty(_connectionName) ? "" : $" — {_connectionName}")</h4>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info py-2 small">
|
||||
The trusted-peer certificate store is <strong>node-wide for the site</strong>
|
||||
(shared by every site node), not per data connection. Trusting or removing a
|
||||
certificate affects all OPC UA connections at this site.
|
||||
</div>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
}
|
||||
else if (_loadError is { } loadError)
|
||||
{
|
||||
<div class="text-danger small" data-test="cert-load-error">@loadError</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (_actionError is { } actionError)
|
||||
{
|
||||
<div class="text-danger small mb-2" data-test="cert-action-error">@actionError</div>
|
||||
}
|
||||
|
||||
@if (_certs.Count == 0)
|
||||
{
|
||||
<div class="text-muted small" data-test="cert-empty">
|
||||
No certificates are present in this site's trusted-peer or rejected stores.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subject</th>
|
||||
<th>Issuer</th>
|
||||
<th>Thumbprint</th>
|
||||
<th>Not before</th>
|
||||
<th>Not after</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var cert in _certs)
|
||||
{
|
||||
<tr data-test="cert-row">
|
||||
<td class="small"><code>@cert.Subject</code></td>
|
||||
<td class="small"><code>@cert.Issuer</code></td>
|
||||
<td class="small"><code>@cert.Thumbprint</code></td>
|
||||
<td class="small">@cert.NotBeforeUtc.ToString("u")</td>
|
||||
<td class="small">@cert.NotAfterUtc.ToString("u")</td>
|
||||
<td class="small">
|
||||
@if (cert.Rejected)
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Rejected</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-success">Trusted</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
data-test="cert-remove-btn"
|
||||
disabled="@_removing"
|
||||
@onclick="() => RemoveCert(cert.Thumbprint)">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public int Id { get; set; }
|
||||
|
||||
private bool _loading = true;
|
||||
private bool _removing;
|
||||
private string? _loadError;
|
||||
private string? _actionError;
|
||||
private string _connectionName = string.Empty;
|
||||
private string _siteIdentifier = string.Empty;
|
||||
private IReadOnlyList<TrustedCertInfo> _certs = Array.Empty<TrustedCertInfo>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Resolve the connection's owning site so the cert relay targets the
|
||||
// right site (the trusted-peer store is node-wide PER SITE node).
|
||||
var connection = await SiteRepository.GetDataConnectionByIdAsync(Id);
|
||||
if (connection is null)
|
||||
{
|
||||
_loadError = $"Data connection {Id} not found.";
|
||||
return;
|
||||
}
|
||||
|
||||
_connectionName = connection.Name;
|
||||
var site = await SiteRepository.GetSiteByIdAsync(connection.SiteId);
|
||||
if (site is null)
|
||||
{
|
||||
_loadError = $"Site {connection.SiteId} not found.";
|
||||
return;
|
||||
}
|
||||
|
||||
_siteIdentifier = site.SiteIdentifier;
|
||||
await LoadCertsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_loadError = $"Failed to load: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCertsAsync()
|
||||
{
|
||||
var result = await CertManagementService.ListAsync(_siteIdentifier);
|
||||
if (result.Success)
|
||||
{
|
||||
_certs = result.Certs ?? Array.Empty<TrustedCertInfo>();
|
||||
_loadError = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_loadError = result.Error ?? "Failed to list certificates.";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveCert(string thumbprint)
|
||||
{
|
||||
_removing = true;
|
||||
_actionError = null;
|
||||
try
|
||||
{
|
||||
var result = await CertManagementService.RemoveAsync(_siteIdentifier, thumbprint);
|
||||
if (result.Success)
|
||||
{
|
||||
// Re-list so the table reflects the removal (the store is node-wide,
|
||||
// so the authoritative list comes back from the site, not local state).
|
||||
await LoadCertsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_actionError = result.Error ?? "Failed to remove certificate.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_actionError = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_removing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack() =>
|
||||
NavigationManager.NavigateTo($"/design/connections/{Id}/edit");
|
||||
}
|
||||
@@ -72,6 +72,21 @@
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formName" />
|
||||
</div>
|
||||
|
||||
@if (Id.HasValue && _protocol == "OpcUa")
|
||||
{
|
||||
<div class="mb-2">
|
||||
<a class="btn btn-outline-secondary btn-sm"
|
||||
data-test="manage-certificates-link"
|
||||
href="@($"/design/connections/{Id}/certificates")">
|
||||
Manage certificates
|
||||
</a>
|
||||
<div class="form-text">
|
||||
View, trust, and remove OPC UA server certificates in the
|
||||
site's node-wide trusted-peer store.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h6 class="text-muted mt-3">Primary endpoint</h6>
|
||||
@if (_protocol == "MxGateway")
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user