Resolve all high-severity architecture review findings
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Antiforgery;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
@@ -198,6 +200,7 @@ public static class AuthEndpoints
|
||||
// page. Allowing anonymous access and returning 200/401 ourselves gives
|
||||
// the client an unambiguous expiry signal.
|
||||
endpoints.MapGet("/auth/ping", HandlePing);
|
||||
endpoints.MapPost("/auth/activity", HandleActivity);
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
@@ -217,6 +220,29 @@ public static class AuthEndpoints
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Records real browser input only after authenticating and validating antiforgery.</summary>
|
||||
public static async Task<IResult> HandleActivity(HttpContext context, IAntiforgery antiforgery,
|
||||
SessionActivityTracker tracker, TimeProvider clock)
|
||||
{
|
||||
if (context.User.Identity?.IsAuthenticated != true) return Results.Unauthorized();
|
||||
try { await antiforgery.ValidateRequestAsync(context); }
|
||||
catch (AntiforgeryValidationException) { return Results.BadRequest(); }
|
||||
var now = clock.GetUtcNow();
|
||||
var identity = new ClaimsIdentity((ClaimsIdentity)context.User.Identity);
|
||||
var old = identity.FindFirst(JwtTokenService.LastActivityClaimType);
|
||||
if (old is not null) identity.RemoveClaim(old);
|
||||
identity.AddClaim(new Claim(JwtTokenService.LastActivityClaimType, now.ToString("o")));
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
// Preserve the login's persistence semantics; renew expiry from this actual interaction.
|
||||
var authenticated = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
var properties = authenticated.Properties ?? BuildSignInProperties();
|
||||
properties.IssuedUtc = now;
|
||||
properties.ExpiresUtc = null;
|
||||
await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, properties);
|
||||
tracker.Record(principal, now);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <see cref="AuthenticationProperties"/> for the login sign-in.
|
||||
/// Deliberately does <b>not</b> set <see cref="AuthenticationProperties.ExpiresUtc"/>.
|
||||
|
||||
@@ -12,6 +12,20 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
/// </summary>
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
/// <summary>Gets fresh mutation authority, failing closed on a mapping error.</summary>
|
||||
public static Task<AuthenticationState> GetAuthoritativeStateAsync(this AuthenticationStateProvider provider)
|
||||
=> provider is CookieAuthenticationStateProvider cookie
|
||||
? cookie.GetAuthoritativeStateAsync() : provider.GetAuthenticationStateAsync();
|
||||
|
||||
/// <summary>Checks the current grant before invoking a directly audited application service.</summary>
|
||||
public static async Task<string> RequireRoleAsync(this AuthenticationStateProvider provider, string role)
|
||||
{
|
||||
var principal = (await provider.GetAuthoritativeStateAsync()).User;
|
||||
if (principal.Identity?.IsAuthenticated != true || !principal.HasClaim(JwtTokenService.RoleClaimType, role))
|
||||
throw new UnauthorizedAccessException($"The current session requires the {role} role.");
|
||||
return principal.GetUsername();
|
||||
}
|
||||
|
||||
/// <summary>Fallback returned when no username claim is present.</summary>
|
||||
public const string UnknownUser = "unknown";
|
||||
|
||||
|
||||
@@ -1,48 +1,71 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.Security.Auth;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges ASP.NET Core cookie authentication with Blazor Server's auth state.
|
||||
/// <para>
|
||||
/// The cookie middleware validates and decrypts the cookie during the initial
|
||||
/// HTTP request that establishes the Blazor circuit. This provider is registered
|
||||
/// <c>Scoped</c>, so it is constructed within that request's DI scope while
|
||||
/// <see cref="IHttpContextAccessor.HttpContext"/> is still valid. We snapshot
|
||||
/// the authenticated principal <b>once</b> in the constructor and serve that
|
||||
/// snapshot for the lifetime of the circuit.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// We must NOT read <see cref="IHttpContextAccessor"/> on every
|
||||
/// <see cref="GetAuthenticationStateAsync"/> call: for the
|
||||
/// lifetime of a long-lived SignalR circuit <c>HttpContext</c> is <c>null</c>
|
||||
/// (or, worse, a stale/foreign context), so a later re-evaluation —
|
||||
/// e.g. <c><AuthorizeView></c> re-rendering — would otherwise see an
|
||||
/// unauthenticated principal and render the wrong UI.
|
||||
/// </para>
|
||||
/// Captures the circuit identity once, then revalidates its stored groups and session
|
||||
/// against current server state. Fresh scopes avoid sharing the circuit's DbContext.
|
||||
/// Never reads IHttpContextAccessor after construction.
|
||||
/// </summary>
|
||||
public class CookieAuthenticationStateProvider : ServerAuthenticationStateProvider
|
||||
{
|
||||
private readonly Task<AuthenticationState> _circuitAuthState;
|
||||
private AuthenticationState _state;
|
||||
private readonly IServiceScopeFactory? _scopes;
|
||||
private readonly bool _autoLogin;
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Snapshots the authenticated principal from the current HTTP context for use throughout the circuit lifetime.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">Accessor used to read the initial HTTP context principal.</param>
|
||||
public CookieAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor)
|
||||
/// <summary>Captures identity and the factory used for subsequent server-side revalidation.</summary>
|
||||
public CookieAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor, IServiceScopeFactory? scopes = null, IOptions<AuthDisableLoginOptions>? autoLogin = null)
|
||||
{
|
||||
// Snapshot the principal at circuit-construction time. HttpContext is
|
||||
// valid here (initial HTTP request) and will not be afterwards.
|
||||
var user = httpContextAccessor.HttpContext?.User
|
||||
?? new ClaimsPrincipal(new ClaimsIdentity());
|
||||
|
||||
_circuitAuthState = Task.FromResult(new AuthenticationState(user));
|
||||
_state = new AuthenticationState(httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
_scopes = scopes;
|
||||
// Trusted startup configuration, already checked by DisableLoginGuard. The
|
||||
// auto-login handler deliberately uses the Cookies scheme with no LDAP groups.
|
||||
_autoLogin = autoLogin?.Value.DisableLogin == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
=> _circuitAuthState;
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync() => RefreshAsync(failClosed: false);
|
||||
|
||||
/// <summary>Requires a current mapping before a mutation; mapping faults deny rather than reuse old grants.</summary>
|
||||
public Task<AuthenticationState> GetAuthoritativeStateAsync() => RefreshAsync(failClosed: true);
|
||||
|
||||
private async Task<AuthenticationState> RefreshAsync(bool failClosed)
|
||||
{
|
||||
if (_autoLogin || _scopes is null || _state.User.Identity?.AuthenticationType != CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
return _state;
|
||||
AuthenticationState state;
|
||||
bool changed;
|
||||
await _gate.WaitAsync();
|
||||
try
|
||||
{
|
||||
using var scope = _scopes.CreateScope();
|
||||
var validator = scope.ServiceProvider.GetRequiredService<CookieSessionValidator>();
|
||||
var result = await validator.ValidateAsync(_state.User, recordActivity: false, forceRefresh: true, failClosed: failClosed);
|
||||
var user = result.Action switch
|
||||
{
|
||||
SessionValidationAction.Replace => result.Principal!,
|
||||
SessionValidationAction.Reject => new ClaimsPrincipal(new ClaimsIdentity()),
|
||||
_ => _state.User,
|
||||
};
|
||||
changed = Authority(_state.User) != Authority(user);
|
||||
_state = state = new AuthenticationState(user);
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
if (changed) NotifyAuthenticationStateChanged(Task.FromResult(state));
|
||||
return state;
|
||||
}
|
||||
|
||||
private static string Authority(ClaimsPrincipal principal)
|
||||
=> principal.Identity?.IsAuthenticated + "|" + string.Join("|", principal.Claims
|
||||
.Where(c => c.Type == JwtTokenService.RoleClaimType || c.Type == JwtTokenService.SiteIdClaimType)
|
||||
.Select(c => c.Type + "=" + c.Value).OrderBy(x => x, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
public sealed class SiteScopeService
|
||||
{
|
||||
private readonly AuthenticationStateProvider _authStateProvider;
|
||||
private (bool IsSystemWide, IReadOnlySet<int> Sites)? _cached;
|
||||
|
||||
/// <summary>Initializes a new instance of <see cref="SiteScopeService"/>.</summary>
|
||||
/// <param name="authStateProvider">The Blazor authentication state provider used to read the current user's claims.</param>
|
||||
@@ -92,10 +91,7 @@ public sealed class SiteScopeService
|
||||
|
||||
private async Task<(bool IsSystemWide, IReadOnlySet<int> Sites)> ResolveAsync()
|
||||
{
|
||||
if (_cached is { } cached)
|
||||
return cached;
|
||||
|
||||
var state = await _authStateProvider.GetAuthenticationStateAsync();
|
||||
var state = await _authStateProvider.GetAuthoritativeStateAsync();
|
||||
|
||||
// Administrator => system-wide, whatever SiteId claims are present. Read
|
||||
// the role CLAIM rather than IsInRole: the principal's role claim type is
|
||||
@@ -103,6 +99,9 @@ public sealed class SiteScopeService
|
||||
// by SessionClaimBuilder, and the comparison is OrdinalIgnoreCase to match
|
||||
// ManagementActor's — IsInRole compares claim VALUES ordinally, which would
|
||||
// make the two sides disagree on a differently-cased role name.
|
||||
if (state.User.Identity?.IsAuthenticated != true)
|
||||
return (false, new HashSet<int>());
|
||||
|
||||
var isAdministrator = state.User
|
||||
.FindAll(JwtTokenService.RoleClaimType)
|
||||
.Any(c => string.Equals(c.Value, Roles.Administrator, StringComparison.OrdinalIgnoreCase));
|
||||
@@ -126,7 +125,6 @@ public sealed class SiteScopeService
|
||||
// unrestricted deployer (this service is the sole
|
||||
// site-scoping mechanism — there is no separate handler to mirror).
|
||||
var result = (IsSystemWide: isAdministrator || ids.Count == 0, Sites: (IReadOnlySet<int>)ids);
|
||||
_cached = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Auth
|
||||
@inject Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider MutationAuth
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@@ -92,7 +94,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
var actor = await ResolveActorAsync();
|
||||
var actor = await MutationAuth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Administrator);
|
||||
|
||||
if (IsSite)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Auth
|
||||
@inject Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider MutationAuth
|
||||
@page "/admin/api-keys/create"
|
||||
@page "/admin/api-keys/{KeyId}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@@ -187,6 +189,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await MutationAuth.RequireRoleAsync(Roles.Administrator);
|
||||
if (_editingKey != null)
|
||||
{
|
||||
// Edit: name is fixed; only the method-scope set is mutable.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Auth
|
||||
@inject Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider MutationAuth
|
||||
@page "/admin/api-keys"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Security
|
||||
@@ -151,6 +153,7 @@
|
||||
{
|
||||
var newEnabled = !key.Enabled;
|
||||
// The seam persists; there is no separate SaveChangesAsync.
|
||||
await MutationAuth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Administrator);
|
||||
var ok = await ApiKeyAdmin.SetEnabledAsync(key.KeyId, newEnabled);
|
||||
if (!ok)
|
||||
{
|
||||
@@ -177,6 +180,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await MutationAuth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Administrator);
|
||||
var ok = await ApiKeyAdmin.DeleteAsync(key.KeyId);
|
||||
if (!ok)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/admin/ldap-mappings/create"
|
||||
@page "/admin/ldap-mappings/{Id:int}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Security
|
||||
@@ -163,19 +166,9 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (_editingMapping != null)
|
||||
{
|
||||
_editingMapping.LdapGroupName = _formGroupName.Trim();
|
||||
_editingMapping.Role = _formRole;
|
||||
await SecurityRepository.UpdateMappingAsync(_editingMapping);
|
||||
}
|
||||
else
|
||||
{
|
||||
var mapping = new LdapGroupMapping(_formGroupName.Trim(), _formRole);
|
||||
await SecurityRepository.AddMappingAsync(mapping);
|
||||
}
|
||||
|
||||
await SecurityRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(_editingMapping is not null
|
||||
? new UpdateRoleMappingCommand(_editingMapping.Id, _formGroupName.Trim(), _formRole)
|
||||
: new CreateRoleMappingCommand(_formGroupName.Trim(), _formRole));
|
||||
NavigationManager.NavigateTo("/admin/ldap-mappings");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -196,9 +189,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
var rule = new SiteScopeRule { LdapGroupMappingId = Id!.Value, SiteId = _scopeRuleSiteId };
|
||||
await SecurityRepository.AddScopeRuleAsync(rule);
|
||||
await SecurityRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new AddScopeRuleCommand(Id!.Value, _scopeRuleSiteId));
|
||||
_scopeRules = (await SecurityRepository.GetScopeRulesForMappingAsync(Id.Value)).ToList();
|
||||
_scopeRuleSiteId = 0;
|
||||
}
|
||||
@@ -219,8 +210,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await SecurityRepository.DeleteScopeRuleAsync(rule.Id);
|
||||
await SecurityRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteScopeRuleCommand(rule.Id));
|
||||
_scopeRules = (await SecurityRepository.GetScopeRulesForMappingAsync(Id!.Value)).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/admin/ldap-mappings"
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireAdmin)]
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Security
|
||||
@@ -143,14 +146,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
// Also delete scope rules for this mapping
|
||||
var rules = await SecurityRepository.GetScopeRulesForMappingAsync(id);
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
await SecurityRepository.DeleteScopeRuleAsync(rule.Id);
|
||||
}
|
||||
await SecurityRepository.DeleteMappingAsync(id);
|
||||
await SecurityRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteRoleMappingCommand(id));
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/admin/sites/create"
|
||||
@page "/admin/sites/{Id:int}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@@ -158,35 +161,16 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (_editingSite != null)
|
||||
if (_editingSite is null && string.IsNullOrWhiteSpace(_formIdentifier))
|
||||
{
|
||||
_editingSite.Name = _formName.Trim();
|
||||
_editingSite.Description = _formDescription?.Trim();
|
||||
_editingSite.NodeAAddress = _formNodeAAddress?.Trim();
|
||||
_editingSite.NodeBAddress = _formNodeBAddress?.Trim();
|
||||
_editingSite.GrpcNodeAAddress = _formGrpcNodeAAddress?.Trim();
|
||||
_editingSite.GrpcNodeBAddress = _formGrpcNodeBAddress?.Trim();
|
||||
await SiteRepository.UpdateSiteAsync(_editingSite);
|
||||
_formError = "Identifier is required.";
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_formIdentifier))
|
||||
{
|
||||
_formError = "Identifier is required.";
|
||||
return;
|
||||
}
|
||||
var site = new Site(_formName.Trim(), _formIdentifier.Trim())
|
||||
{
|
||||
Description = _formDescription?.Trim(),
|
||||
NodeAAddress = _formNodeAAddress?.Trim(),
|
||||
NodeBAddress = _formNodeBAddress?.Trim(),
|
||||
GrpcNodeAAddress = _formGrpcNodeAAddress?.Trim(),
|
||||
GrpcNodeBAddress = _formGrpcNodeBAddress?.Trim()
|
||||
};
|
||||
await SiteRepository.AddSiteAsync(site);
|
||||
}
|
||||
|
||||
await SiteRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(_editingSite is not null
|
||||
? new UpdateSiteCommand(_editingSite.Id, _formName.Trim(), _formDescription?.Trim(),
|
||||
_formNodeAAddress?.Trim(), _formNodeBAddress?.Trim(), _formGrpcNodeAAddress?.Trim(), _formGrpcNodeBAddress?.Trim())
|
||||
: new CreateSiteCommand(_formName.Trim(), _formIdentifier.Trim(), _formDescription?.Trim(),
|
||||
_formNodeAAddress?.Trim(), _formNodeBAddress?.Trim(), _formGrpcNodeAAddress?.Trim(), _formGrpcNodeBAddress?.Trim()));
|
||||
CommunicationService.RefreshSiteAddresses();
|
||||
NavigationManager.NavigateTo("/admin/sites");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@* Reference pattern for list pages: card grid (col-lg-6) + flex header + search filter + kebab dropdown + Bootstrap collapse for noisy detail + @key on iterated cards + "No X match the filter." inline + empty-state CTA. Mirror this when building new list pages. *@
|
||||
@page "/admin/sites"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@@ -165,7 +168,7 @@
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
=> AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Administrator);
|
||||
|
||||
private List<Site> _sites = new();
|
||||
private Dictionary<int, List<DataConnection>> _siteConnections = new();
|
||||
@@ -221,8 +224,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await SiteRepository.DeleteSiteAsync(site.Id);
|
||||
await SiteRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteSiteCommand(site.Id));
|
||||
CommunicationService.RefreshSiteAddresses();
|
||||
_toast.ShowSuccess($"Site '{site.Name}' deleted.");
|
||||
await LoadDataAsync();
|
||||
|
||||
+27
-53
@@ -1,3 +1,7 @@
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/deployment/instances/{Id:int}/configure"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances
|
||||
@@ -5,14 +9,12 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
||||
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening
|
||||
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.DeploymentManager
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
|
||||
@inject ITemplateEngineRepository TemplateEngineRepository
|
||||
@@ -963,8 +965,14 @@
|
||||
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
private async Task<string> GetCurrentUserAsync()
|
||||
{
|
||||
var user = await AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var current = await TemplateEngineRepository.GetInstanceByIdAsync(Id);
|
||||
if (current is null || !await SiteScope.IsSiteAllowedAsync(current.SiteId))
|
||||
throw new UnauthorizedAccessException("The instance is outside your current site scope.");
|
||||
return user;
|
||||
}
|
||||
|
||||
// ── Bindings ────────────────────────────────────────────
|
||||
|
||||
@@ -1243,7 +1251,7 @@
|
||||
_bindingOverrides.GetValueOrDefault(kv.Key)))
|
||||
.ToList();
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await InstanceService.SetConnectionBindingsAsync(Id, bindings, user);
|
||||
var result = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(Id, null, (instanceService, currentUser) => instanceService.SetConnectionBindingsAsync(Id, bindings, currentUser));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
SnapshotSavedBindingState();
|
||||
@@ -1323,8 +1331,7 @@
|
||||
{
|
||||
if (_existingOverrides.TryGetValue(attrName, out var ovr))
|
||||
{
|
||||
await TemplateEngineRepository.DeleteInstanceAttributeOverrideAsync(ovr.Id);
|
||||
await TemplateEngineRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteInstanceAttributeOverrideCommand(Id, attrName));
|
||||
_existingOverrides.Remove(attrName);
|
||||
}
|
||||
_overrideValues.Remove(attrName);
|
||||
@@ -1377,7 +1384,7 @@
|
||||
var failures = new List<string>();
|
||||
foreach (var (attrName, value) in toSave)
|
||||
{
|
||||
var result = await InstanceService.SetAttributeOverrideAsync(Id, attrName, value, user);
|
||||
var result = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(Id, null, (instanceService, currentUser) => instanceService.SetAttributeOverrideAsync(Id, attrName, value, currentUser));
|
||||
if (result.IsSuccess)
|
||||
_existingOverrides[attrName] = result.Value!;
|
||||
else
|
||||
@@ -1454,7 +1461,7 @@
|
||||
var failures = new List<string>();
|
||||
foreach (var (attrName, value) in outcome.Overrides)
|
||||
{
|
||||
var result = await InstanceService.SetAttributeOverrideAsync(Id, attrName, value, user);
|
||||
var result = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(Id, null, (instanceService, currentUser) => instanceService.SetAttributeOverrideAsync(Id, attrName, value, currentUser));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_existingOverrides[attrName] = result.Value!;
|
||||
@@ -1622,7 +1629,7 @@
|
||||
{
|
||||
if (_existingAlarmOverrides.ContainsKey(alarmName))
|
||||
{
|
||||
var del = await InstanceService.DeleteAlarmOverrideAsync(Id, alarmName, user);
|
||||
var del = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(Id, null, (instanceService, currentUser) => instanceService.DeleteAlarmOverrideAsync(Id, alarmName, currentUser));
|
||||
if (!del.IsSuccess)
|
||||
{
|
||||
_editingError = del.Error;
|
||||
@@ -1639,8 +1646,8 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await InstanceService.SetAlarmOverrideAsync(
|
||||
Id, alarmName, overrideJson, priority, user);
|
||||
var result = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(Id, null, (instanceService, currentUser) => instanceService.SetAlarmOverrideAsync(
|
||||
Id, alarmName, overrideJson, priority, currentUser));
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
_editingError = result.Error;
|
||||
@@ -1675,7 +1682,7 @@
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await InstanceService.DeleteAlarmOverrideAsync(Id, alarmName, user);
|
||||
var result = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(Id, null, (instanceService, currentUser) => instanceService.DeleteAlarmOverrideAsync(Id, alarmName, currentUser));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_existingAlarmOverrides.Remove(alarmName);
|
||||
@@ -1718,7 +1725,6 @@
|
||||
}
|
||||
|
||||
await UpsertNativeOverrideCore(sourceName, conn, sref, filt);
|
||||
await TemplateEngineRepository.SaveChangesAsync();
|
||||
_toast.ShowSuccess($"Saved native alarm source override on '{sourceName}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1751,27 +1757,8 @@
|
||||
/// </summary>
|
||||
private async Task UpsertNativeOverrideCore(string sourceName, string? conn, string? sref, string? filt)
|
||||
{
|
||||
var existing = await TemplateEngineRepository.GetNativeAlarmSourceOverrideAsync(Id, sourceName);
|
||||
if (existing == null)
|
||||
{
|
||||
var ovr = new InstanceNativeAlarmSourceOverride(sourceName)
|
||||
{
|
||||
InstanceId = Id,
|
||||
ConnectionNameOverride = conn,
|
||||
SourceReferenceOverride = sref,
|
||||
ConditionFilterOverride = filt
|
||||
};
|
||||
await TemplateEngineRepository.AddInstanceNativeAlarmSourceOverrideAsync(ovr);
|
||||
_existingNativeOverrides[sourceName] = ovr;
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.ConnectionNameOverride = conn;
|
||||
existing.SourceReferenceOverride = sref;
|
||||
existing.ConditionFilterOverride = filt;
|
||||
await TemplateEngineRepository.UpdateInstanceNativeAlarmSourceOverrideAsync(existing);
|
||||
_existingNativeOverrides[sourceName] = existing;
|
||||
}
|
||||
var result = await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverrideCommand(Id, sourceName, conn, sref, filt));
|
||||
if (result is InstanceNativeAlarmSourceOverride saved) _existingNativeOverrides[sourceName] = saved;
|
||||
|
||||
// Keep the inline editor showing what was just persisted.
|
||||
_nasConnEdit[sourceName] = conn;
|
||||
@@ -1781,12 +1768,7 @@
|
||||
|
||||
private async Task ClearNativeOverrideCore(string sourceName)
|
||||
{
|
||||
var existing = await TemplateEngineRepository.GetNativeAlarmSourceOverrideAsync(Id, sourceName);
|
||||
if (existing != null)
|
||||
{
|
||||
await TemplateEngineRepository.DeleteInstanceNativeAlarmSourceOverrideAsync(existing.Id);
|
||||
await TemplateEngineRepository.SaveChangesAsync();
|
||||
}
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteInstanceNativeAlarmSourceOverrideCommand(Id, sourceName));
|
||||
_existingNativeOverrides.Remove(sourceName);
|
||||
_nasConnEdit[sourceName] = null;
|
||||
_nasRefEdit[sourceName] = null;
|
||||
@@ -1849,17 +1831,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Every row was validated up front, so the batch applies as a unit through
|
||||
// the same repository upsert/clear the inline editor uses.
|
||||
foreach (var row in outcome.Rows)
|
||||
{
|
||||
if (row.IsClear)
|
||||
await ClearNativeOverrideCore(row.SourceName);
|
||||
else
|
||||
await UpsertNativeOverrideCore(
|
||||
row.SourceName, row.Connection, row.SourceReference, row.Filter);
|
||||
}
|
||||
await TemplateEngineRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverridesCommand(Id,
|
||||
outcome.Rows.Select(row => new NativeAlarmSourceOverrideEntry(row.SourceName, row.Connection, row.SourceReference, row.Filter)).ToList()));
|
||||
await OnInitializedAsync();
|
||||
|
||||
_nasCsvImportSucceeded = true;
|
||||
_nasCsvImportResult = $"Imported {outcome.Rows.Count} native alarm source override(s).";
|
||||
@@ -1957,7 +1931,7 @@
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await InstanceService.AssignToAreaAsync(Id, _reassignAreaId == 0 ? null : _reassignAreaId, user);
|
||||
var result = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(Id, null, (instanceService, currentUser) => instanceService.AssignToAreaAsync(Id, _reassignAreaId == 0 ? null : _reassignAreaId, currentUser));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_areaName = _siteAreas.FirstOrDefault(a => a.Id == _reassignAreaId)?.Name ?? "";
|
||||
|
||||
+6
-4
@@ -1,3 +1,5 @@
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/deployment/instances/create"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances
|
||||
@@ -324,8 +326,8 @@
|
||||
try
|
||||
{
|
||||
user = await GetCurrentUserAsync();
|
||||
var result = await InstanceService.CreateInstanceAsync(
|
||||
_createName.Trim(), _createTemplateId, _createSiteId, _createAreaId == 0 ? null : _createAreaId, user);
|
||||
var result = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(null, _createSiteId, (instanceService, currentUser) => instanceService.CreateInstanceAsync(
|
||||
_createName.Trim(), _createTemplateId, _createSiteId, _createAreaId == 0 ? null : _createAreaId, currentUser));
|
||||
if (result.IsFailure)
|
||||
{
|
||||
_formError = result.Error;
|
||||
@@ -355,7 +357,7 @@
|
||||
string? bindError = null;
|
||||
try
|
||||
{
|
||||
var bind = await InstanceService.SetConnectionBindingsAsync(id, bindings, user);
|
||||
var bind = await MutationServices.GetRequiredService<ConfigurationCommandService>().ExecuteInstanceAsync(id, null, (instanceService, currentUser) => instanceService.SetConnectionBindingsAsync(id, bindings, currentUser));
|
||||
if (bind.IsFailure) bindError = bind.Error;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -378,7 +380,7 @@
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
=> AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the folder-tree template picker. A pick clears any dangling-id hint and
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/deployment/topology"
|
||||
@page "/deployment/instances"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@@ -705,8 +708,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await AreaService.UpdateAreaAsync(node.EntityId, _renameBuffer.Trim(), user);
|
||||
var result = await ExecuteConfigurationAsync<Area>(new UpdateAreaCommand(node.EntityId, _renameBuffer.Trim()));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
CancelRename();
|
||||
@@ -765,8 +767,7 @@
|
||||
if (req.SiteId == 0) { _createAreaError = "Select a site."; return; }
|
||||
if (string.IsNullOrWhiteSpace(req.Name)) { _createAreaError = "Area name is required."; return; }
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await AreaService.CreateAreaAsync(req.Name, req.SiteId, req.ParentAreaId, user);
|
||||
var result = await ExecuteConfigurationAsync<Area>(new CreateAreaCommand(req.SiteId, req.Name, req.ParentAreaId));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showCreateAreaDialog = false;
|
||||
@@ -800,8 +801,7 @@
|
||||
private async Task SubmitMoveArea((int AreaId, int? NewParentId) req)
|
||||
{
|
||||
_moveAreaError = null;
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await AreaService.MoveAreaAsync(req.AreaId, req.NewParentId, user);
|
||||
var result = await ExecuteConfigurationAsync<Area>(new MoveAreaCommand(req.AreaId, req.NewParentId));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showMoveAreaDialog = false;
|
||||
@@ -836,8 +836,7 @@
|
||||
private async Task SubmitMoveInstance((int InstanceId, int? NewAreaId) req)
|
||||
{
|
||||
_moveInstanceError = null;
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await InstanceService.AssignToAreaAsync(req.InstanceId, req.NewAreaId, user);
|
||||
var result = await ExecuteConfigurationAsync<Instance>(new SetInstanceAreaCommand(req.InstanceId, req.NewAreaId));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_showMoveInstanceDialog = false;
|
||||
@@ -860,8 +859,7 @@
|
||||
if (!confirmed) return;
|
||||
|
||||
_actionInProgress = true;
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await AreaService.DeleteAreaAsync(node.EntityId, user);
|
||||
var result = await ExecuteConfigurationAsync<bool>(new DeleteAreaCommand(node.EntityId));
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_toast.ShowSuccess($"Area '{node.Label}' deleted.");
|
||||
@@ -885,7 +883,8 @@
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var user = await GetCurrentUserAsync(inst.Id);
|
||||
if (user is null) return;
|
||||
var result = await DeploymentService.DeleteInstanceAsync(inst.Id, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
@@ -910,7 +909,8 @@
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var user = await GetCurrentUserAsync(inst.Id);
|
||||
if (user is null) return;
|
||||
var result = await DeploymentService.EnableInstanceAsync(inst.Id, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
@@ -941,7 +941,8 @@
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var user = await GetCurrentUserAsync(inst.Id);
|
||||
if (user is null) return;
|
||||
var result = await DeploymentService.DisableInstanceAsync(inst.Id, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
@@ -965,7 +966,8 @@
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var user = await GetCurrentUserAsync(inst.Id);
|
||||
if (user is null) return;
|
||||
var result = await DeploymentService.DeployInstanceAsync(inst.Id, user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
@@ -1239,6 +1241,35 @@
|
||||
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
private async Task<string?> GetCurrentUserAsync(int instanceId)
|
||||
{
|
||||
var user = await AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
// Re-fetch outside the circuit tracker: the instance may have moved since rendering.
|
||||
using var scope = ScopeFactory.CreateScope();
|
||||
var repository = scope.ServiceProvider.GetRequiredService<ITemplateEngineRepository>();
|
||||
var instance = await repository.GetInstanceByIdAsync(instanceId);
|
||||
if (instance is null || !await SiteScope.IsSiteAllowedAsync(instance.SiteId))
|
||||
{
|
||||
_toast.ShowError("This instance is no longer available within your permitted sites.");
|
||||
_actionInProgress = false;
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
private async Task<ZB.MOM.WW.ScadaBridge.Commons.Types.Result<T>> ExecuteConfigurationAsync<T>(object command)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = await MutationServices.ExecuteConfigurationAsync(command);
|
||||
return ZB.MOM.WW.ScadaBridge.Commons.Types.Result<T>.Success((T)value!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = ex is UnauthorizedAccessException
|
||||
or ZB.MOM.WW.ScadaBridge.ManagementService.ManagementCommandException
|
||||
? ex.Message : "The configuration change failed. Refresh the page and try again.";
|
||||
return ZB.MOM.WW.ScadaBridge.Commons.Types.Result<T>.Failure(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Auth
|
||||
@inject Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider MutationAuth
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/design/api-methods/create"
|
||||
@page "/design/api-methods/{Id:int}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ScriptAnalysis = ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject IInboundApiRepository InboundApiRepository
|
||||
@@ -297,33 +301,10 @@
|
||||
|
||||
try
|
||||
{
|
||||
// Save the ApiMethod entity FIRST so the method Name exists before we reconcile
|
||||
// key scopes against it. The method entity stays in SQL Server; we leave the
|
||||
// (now-legacy) ApprovedApiKeyIds column untouched — it is dropped in C5.
|
||||
string methodName;
|
||||
if (_existing != null)
|
||||
{
|
||||
_existing.Script = _script;
|
||||
_existing.TimeoutSeconds = _timeoutSeconds;
|
||||
_existing.TraceAllCalls = _traceAllCalls;
|
||||
_existing.ParameterDefinitions = _params?.Trim();
|
||||
_existing.ReturnDefinition = _returns?.Trim();
|
||||
await InboundApiRepository.UpdateApiMethodAsync(_existing);
|
||||
methodName = _existing.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
var m = new ApiMethod(_name.Trim(), _script)
|
||||
{
|
||||
TimeoutSeconds = _timeoutSeconds,
|
||||
TraceAllCalls = _traceAllCalls,
|
||||
ParameterDefinitions = _params?.Trim(),
|
||||
ReturnDefinition = _returns?.Trim()
|
||||
};
|
||||
await InboundApiRepository.AddApiMethodAsync(m);
|
||||
methodName = m.Name;
|
||||
}
|
||||
await InboundApiRepository.SaveChangesAsync();
|
||||
var methodName = _existing?.Name ?? _name.Trim();
|
||||
await MutationServices.ExecuteConfigurationAsync(_existing is not null
|
||||
? new UpdateApiMethodCommand(_existing.Id, _script, _timeoutSeconds, _params?.Trim(), _returns?.Trim(), _traceAllCalls)
|
||||
: new CreateApiMethodCommand(methodName, _script, _timeoutSeconds, _params?.Trim(), _returns?.Trim(), _traceAllCalls));
|
||||
|
||||
// Reconcile per-key method-scopes for the affected keys (added/removed vs. load time).
|
||||
if (!await ReconcileKeyScopesAsync(methodName)) return;
|
||||
@@ -371,6 +352,7 @@
|
||||
{
|
||||
foreach (var update in plan.Updates)
|
||||
{
|
||||
await MutationAuth.RequireRoleAsync(Roles.Designer);
|
||||
var ok = await ApiKeyAdmin.SetMethodsAsync(update.KeyId, update.NewMethods);
|
||||
if (!ok)
|
||||
throw new InvalidOperationException(
|
||||
|
||||
+6
-20
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/design/connections/create"
|
||||
@page "/design/connections/{Id:int}/edit"
|
||||
@page "/design/data-connections/create"
|
||||
@@ -327,26 +330,9 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (_editingConnection != null)
|
||||
{
|
||||
_editingConnection.Name = _formName.Trim();
|
||||
_editingConnection.Protocol = _protocol;
|
||||
_editingConnection.PrimaryConfiguration = primaryJson;
|
||||
_editingConnection.BackupConfiguration = backupJson;
|
||||
_editingConnection.FailoverRetryCount = _showBackup ? _formFailoverRetryCount : 3;
|
||||
await SiteRepository.UpdateDataConnectionAsync(_editingConnection);
|
||||
}
|
||||
else
|
||||
{
|
||||
var conn = new DataConnection(_formName.Trim(), _protocol, _formSiteId)
|
||||
{
|
||||
PrimaryConfiguration = primaryJson,
|
||||
BackupConfiguration = backupJson,
|
||||
FailoverRetryCount = _showBackup ? _formFailoverRetryCount : 3
|
||||
};
|
||||
await SiteRepository.AddDataConnectionAsync(conn);
|
||||
}
|
||||
await SiteRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(_editingConnection is not null
|
||||
? new UpdateDataConnectionCommand(_editingConnection.Id, _formName.Trim(), _protocol, primaryJson, backupJson, _showBackup ? _formFailoverRetryCount : 3)
|
||||
: new CreateDataConnectionCommand(_formSiteId, _formName.Trim(), _protocol, primaryJson, backupJson, _showBackup ? _formFailoverRetryCount : 3));
|
||||
NavigationManager.NavigateTo("/design/connections");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/design/connections"
|
||||
@page "/design/data-connections"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
||||
@@ -417,8 +419,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await SiteRepository.DeleteDataConnectionAsync(conn.Id);
|
||||
await SiteRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteDataConnectionCommand(conn.Id));
|
||||
_toast.ShowSuccess($"Connection '{conn.Name}' deleted.");
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/design/db-connections/create"
|
||||
@page "/design/db-connections/{Id:int}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@@ -94,24 +97,9 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (_existing != null)
|
||||
{
|
||||
_existing.Name = _name.Trim();
|
||||
_existing.ConnectionString = _connectionString.Trim();
|
||||
_existing.MaxRetries = _maxRetries;
|
||||
_existing.RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds);
|
||||
await ExternalSystemRepository.UpdateDatabaseConnectionAsync(_existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dc = new DatabaseConnectionDefinition(_name.Trim(), _connectionString.Trim())
|
||||
{
|
||||
MaxRetries = _maxRetries,
|
||||
RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds)
|
||||
};
|
||||
await ExternalSystemRepository.AddDatabaseConnectionAsync(dc);
|
||||
}
|
||||
await ExternalSystemRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(_existing is not null
|
||||
? new UpdateDatabaseConnectionDefCommand(_existing.Id, _name.Trim(), _connectionString.Trim(), _maxRetries, TimeSpan.FromSeconds(_retryDelaySeconds))
|
||||
: new CreateDatabaseConnectionDefCommand(_name.Trim(), _connectionString.Trim(), _maxRetries, TimeSpan.FromSeconds(_retryDelaySeconds)));
|
||||
NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
|
||||
+6
-23
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/design/external-systems/create"
|
||||
@page "/design/external-systems/{Id:int}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@@ -137,29 +140,9 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (_existing != null)
|
||||
{
|
||||
_existing.Name = _name.Trim();
|
||||
_existing.EndpointUrl = _endpointUrl.Trim();
|
||||
_existing.AuthType = _authType;
|
||||
_existing.AuthConfiguration = _authConfig?.Trim();
|
||||
_existing.MaxRetries = _maxRetries;
|
||||
_existing.RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds);
|
||||
_existing.TimeoutSeconds = _timeoutSeconds;
|
||||
await ExternalSystemRepository.UpdateExternalSystemAsync(_existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var es = new ExternalSystemDefinition(_name.Trim(), _endpointUrl.Trim(), _authType)
|
||||
{
|
||||
AuthConfiguration = _authConfig?.Trim(),
|
||||
MaxRetries = _maxRetries,
|
||||
RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds),
|
||||
TimeoutSeconds = _timeoutSeconds
|
||||
};
|
||||
await ExternalSystemRepository.AddExternalSystemAsync(es);
|
||||
}
|
||||
await ExternalSystemRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(_existing is not null
|
||||
? new UpdateExternalSystemCommand(_existing.Id, _name.Trim(), _endpointUrl.Trim(), _authType, _authConfig?.Trim() ?? "", _timeoutSeconds, _maxRetries, TimeSpan.FromSeconds(_retryDelaySeconds))
|
||||
: new CreateExternalSystemCommand(_name.Trim(), _endpointUrl.Trim(), _authType, _authConfig?.Trim(), _timeoutSeconds, _maxRetries, TimeSpan.FromSeconds(_retryDelaySeconds)));
|
||||
NavigationManager.NavigateTo("/design/external-systems");
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/design/external-systems"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems
|
||||
@@ -189,7 +192,7 @@
|
||||
private async Task DeleteExtSys(ExternalSystemDefinition es)
|
||||
{
|
||||
if (!await Dialog.ConfirmAsync("Delete External System", $"Delete '{es.Name}'?", danger: true)) return;
|
||||
try { await ExternalSystemRepository.DeleteExternalSystemAsync(es.Id); await ExternalSystemRepository.SaveChangesAsync(); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
||||
try { await MutationServices.ExecuteConfigurationAsync(new DeleteExternalSystemCommand(es.Id)); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
||||
catch (Exception ex) { _toast.ShowError(ex.Message); }
|
||||
}
|
||||
|
||||
@@ -262,7 +265,7 @@
|
||||
private async Task DeleteDbConn(DatabaseConnectionDefinition dc)
|
||||
{
|
||||
if (!await Dialog.ConfirmAsync("Delete DB Connection", $"Delete '{dc.Name}'?", danger: true)) return;
|
||||
try { await ExternalSystemRepository.DeleteDatabaseConnectionAsync(dc.Id); await ExternalSystemRepository.SaveChangesAsync(); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
||||
try { await MutationServices.ExecuteConfigurationAsync(new DeleteDatabaseConnectionDefCommand(dc.Id)); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
||||
catch (Exception ex) { _toast.ShowError(ex.Message); }
|
||||
}
|
||||
|
||||
@@ -337,7 +340,7 @@
|
||||
private async Task DeleteApiMethod(ApiMethod m)
|
||||
{
|
||||
if (!await Dialog.ConfirmAsync("Delete", $"Delete API method '{m.Name}'?", danger: true)) return;
|
||||
try { await InboundApiRepository.DeleteApiMethodAsync(m.Id); await InboundApiRepository.SaveChangesAsync(); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
||||
try { await MutationServices.ExecuteConfigurationAsync(new DeleteApiMethodCommand(m.Id)); _toast.ShowSuccess("Deleted."); await LoadAllAsync(); }
|
||||
catch (Exception ex) { _toast.ShowError(ex.Message); }
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
=> AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Designer);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
=> AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Designer);
|
||||
|
||||
private List<SharedScript> _scripts = new();
|
||||
private bool _loading = true;
|
||||
|
||||
@@ -122,5 +122,5 @@
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
=> AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Designer);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/design/templates/{Id:int}"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances
|
||||
@@ -217,7 +220,7 @@
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
=> AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Designer);
|
||||
|
||||
private RenderFragment RenderTemplateDetail() => __builder =>
|
||||
{
|
||||
@@ -1080,13 +1083,10 @@
|
||||
{
|
||||
var existing = await TemplateEngineRepository.GetTemplateNativeAlarmSourceByIdAsync(id);
|
||||
if (existing == null) return "Source no longer exists.";
|
||||
existing.ConnectionName = draft.ConnectionName.Trim();
|
||||
existing.SourceReference = draft.SourceReference.Trim();
|
||||
existing.ConditionFilter = string.IsNullOrWhiteSpace(draft.ConditionFilter) ? null : draft.ConditionFilter.Trim();
|
||||
existing.Description = string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim();
|
||||
existing.IsLocked = draft.IsLocked;
|
||||
await TemplateEngineRepository.UpdateTemplateNativeAlarmSourceAsync(existing);
|
||||
await TemplateEngineRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new UpdateTemplateNativeAlarmSourceCommand(id, existing.Name,
|
||||
draft.ConnectionName.Trim(), draft.SourceReference.Trim(),
|
||||
string.IsNullOrWhiteSpace(draft.ConditionFilter) ? null : draft.ConditionFilter.Trim(),
|
||||
string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim(), draft.IsLocked));
|
||||
_toast.ShowSuccess($"Native alarm source '{existing.Name}' updated.");
|
||||
return null;
|
||||
}
|
||||
@@ -1105,8 +1105,8 @@
|
||||
Description = string.IsNullOrWhiteSpace(draft.Description) ? null : draft.Description.Trim(),
|
||||
IsLocked = draft.IsLocked
|
||||
};
|
||||
await TemplateEngineRepository.AddTemplateNativeAlarmSourceAsync(source);
|
||||
await TemplateEngineRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new AddTemplateNativeAlarmSourceCommand(source.TemplateId, source.Name,
|
||||
source.ConnectionName, source.SourceReference, source.ConditionFilter, source.Description, source.IsLocked));
|
||||
_toast.ShowSuccess($"Native alarm source '{source.Name}' added.");
|
||||
return null;
|
||||
}
|
||||
@@ -1115,8 +1115,7 @@
|
||||
{
|
||||
var confirmed = await Dialog.ConfirmAsync("Delete Native Alarm Source", $"Delete native alarm source '{src.Name}'?", danger: true);
|
||||
if (!confirmed) return;
|
||||
await TemplateEngineRepository.DeleteTemplateNativeAlarmSourceAsync(src.Id);
|
||||
await TemplateEngineRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteTemplateNativeAlarmSourceCommand(src.Id));
|
||||
_toast.ShowSuccess($"Native alarm source '{src.Name}' deleted.");
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
=> AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Designer);
|
||||
|
||||
private List<Template> _templates = new();
|
||||
private List<TemplateFolder> _folders = new();
|
||||
|
||||
+1
-1
@@ -382,7 +382,7 @@ public partial class TransportExport : ComponentBase
|
||||
_downloadError = null;
|
||||
try
|
||||
{
|
||||
var user = await Auth.GetCurrentUsernameAsync();
|
||||
var user = await Auth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Designer);
|
||||
var sourceEnv = TransportOptions.Value.SourceEnvironment;
|
||||
if (string.IsNullOrWhiteSpace(sourceEnv))
|
||||
{
|
||||
|
||||
+2
-2
@@ -465,7 +465,7 @@ public partial class TransportImport : ComponentBase, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await Auth.GetCurrentUsernameAsync();
|
||||
var user = await Auth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Administrator);
|
||||
var entityName = _session?.Manifest.SourceEnvironment ?? "<unknown>";
|
||||
await AuditService.LogAsync(
|
||||
user: user,
|
||||
@@ -857,7 +857,7 @@ public partial class TransportImport : ComponentBase, IDisposable
|
||||
_result = null;
|
||||
try
|
||||
{
|
||||
var user = await Auth.GetCurrentUsernameAsync();
|
||||
var user = await Auth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Administrator);
|
||||
// Fold the operator's Map-step choices into the name map. For
|
||||
// central-config-only bundles (no required mappings) this is
|
||||
// BundleNameMap.Empty, which the importer normalises away.
|
||||
|
||||
+16
-2
@@ -1,3 +1,5 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Auth
|
||||
@inject Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider MutationAuth
|
||||
@page "/monitoring/parked-messages"
|
||||
@attribute [Authorize(Policy = ZB.MOM.WW.ScadaBridge.Security.AuthorizationPolicies.RequireDeployment)]
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
|
||||
@@ -647,7 +649,10 @@
|
||||
try
|
||||
{
|
||||
var req = new ParkedMessageRetryRequest(Guid.NewGuid().ToString("N"), _selectedSiteId, id, DateTimeOffset.UtcNow);
|
||||
var resp = await CommunicationService.RetryParkedMessageAsync(_selectedSiteId, req);
|
||||
await MutationAuth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var mutationSite = (await SiteRepository.GetAllSitesAsync()).FirstOrDefault(s => s.SiteIdentifier == _selectedSiteId);
|
||||
if (mutationSite is null || !await SiteScope.IsSiteAllowedAsync(mutationSite.Id)) throw new UnauthorizedAccessException("Site access was revoked.");
|
||||
var resp = await CommunicationService.RetryParkedMessageAsync(_selectedSiteId, req);
|
||||
if (resp.Success) success++; else failed++;
|
||||
}
|
||||
catch { failed++; }
|
||||
@@ -680,7 +685,10 @@
|
||||
try
|
||||
{
|
||||
var req = new ParkedMessageDiscardRequest(Guid.NewGuid().ToString("N"), _selectedSiteId, id, DateTimeOffset.UtcNow);
|
||||
var resp = await CommunicationService.DiscardParkedMessageAsync(_selectedSiteId, req);
|
||||
await MutationAuth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var mutationSite = (await SiteRepository.GetAllSitesAsync()).FirstOrDefault(s => s.SiteIdentifier == _selectedSiteId);
|
||||
if (mutationSite is null || !await SiteScope.IsSiteAllowedAsync(mutationSite.Id)) throw new UnauthorizedAccessException("Site access was revoked.");
|
||||
var resp = await CommunicationService.DiscardParkedMessageAsync(_selectedSiteId, req);
|
||||
if (resp.Success) success++; else failed++;
|
||||
}
|
||||
catch { failed++; }
|
||||
@@ -702,6 +710,9 @@
|
||||
try
|
||||
{
|
||||
var req = new ParkedMessageRetryRequest(Guid.NewGuid().ToString("N"), _selectedSiteId, msg.MessageId, DateTimeOffset.UtcNow);
|
||||
await MutationAuth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var mutationSite = (await SiteRepository.GetAllSitesAsync()).FirstOrDefault(s => s.SiteIdentifier == _selectedSiteId);
|
||||
if (mutationSite is null || !await SiteScope.IsSiteAllowedAsync(mutationSite.Id)) throw new UnauthorizedAccessException("Site access was revoked.");
|
||||
var resp = await CommunicationService.RetryParkedMessageAsync(_selectedSiteId, req);
|
||||
if (resp.Success)
|
||||
{
|
||||
@@ -731,6 +742,9 @@
|
||||
try
|
||||
{
|
||||
var req = new ParkedMessageDiscardRequest(Guid.NewGuid().ToString("N"), _selectedSiteId, msg.MessageId, DateTimeOffset.UtcNow);
|
||||
await MutationAuth.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var mutationSite = (await SiteRepository.GetAllSitesAsync()).FirstOrDefault(s => s.SiteIdentifier == _selectedSiteId);
|
||||
if (mutationSite is null || !await SiteScope.IsSiteAllowedAsync(mutationSite.Id)) throw new UnauthorizedAccessException("Site access was revoked.");
|
||||
var resp = await CommunicationService.DiscardParkedMessageAsync(_selectedSiteId, req);
|
||||
if (resp.Success)
|
||||
{
|
||||
|
||||
+8
-19
@@ -1,7 +1,9 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/notifications/lists/create"
|
||||
@page "/notifications/lists/{Id:int}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
||||
@@ -185,20 +187,9 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (_existing != null)
|
||||
{
|
||||
_existing.Name = _name.Trim();
|
||||
await NotificationRepository.UpdateNotificationListAsync(_existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Type is set at create time and never changes thereafter (the selector
|
||||
// is disabled on edit) so a list's recipients always carry the matching
|
||||
// contact field.
|
||||
var nl = new NotificationList(_name.Trim()) { Type = _type };
|
||||
await NotificationRepository.AddNotificationListAsync(nl);
|
||||
}
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(_existing is not null
|
||||
? new RenameNotificationListCommand(_existing.Id, _name.Trim())
|
||||
: new CreateNotificationListCommand(_name.Trim(), [], _type));
|
||||
NavigationManager.NavigateTo("/notifications/lists");
|
||||
}
|
||||
catch (Exception ex) { _formError = ex.Message; }
|
||||
@@ -238,8 +229,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
await NotificationRepository.AddRecipientAsync(r);
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new AddNotificationRecipientCommand(r.NotificationListId, r.Name, r.EmailAddress, r.PhoneNumber));
|
||||
_recipientName = _recipientEmail = _recipientPhone = string.Empty;
|
||||
_recipients = (await NotificationRepository.GetRecipientsByListIdAsync(Id.Value)).ToList();
|
||||
}
|
||||
@@ -264,8 +254,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
await NotificationRepository.DeleteRecipientAsync(r.Id);
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteNotificationRecipientCommand(r.Id));
|
||||
_recipients = (await NotificationRepository.GetRecipientsByListIdAsync(Id!.Value)).ToList();
|
||||
}
|
||||
catch (Exception ex) { _recipientFormError = ex.Message; }
|
||||
|
||||
+4
-2
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/notifications/lists"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications
|
||||
@@ -165,8 +168,7 @@
|
||||
}
|
||||
try
|
||||
{
|
||||
await NotificationRepository.DeleteNotificationListAsync(list.Id);
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(new DeleteNotificationListCommand(list.Id));
|
||||
_toast.ShowSuccess("Deleted.");
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
+9
-4
@@ -604,7 +604,8 @@
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var requestedBy = await AuthStateProvider.GetCurrentUsernameAsync();
|
||||
if (!await IsRowSiteAllowedAsync(n.SourceSiteId)) throw new UnauthorizedAccessException("Site access was revoked.");
|
||||
var requestedBy = await AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var response = await CommunicationService.RetryNotificationAsync(
|
||||
new RetryNotificationRequest(Guid.NewGuid().ToString("N"), n.NotificationId, requestedBy));
|
||||
if (response.Success)
|
||||
@@ -642,7 +643,8 @@
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var requestedBy = await AuthStateProvider.GetCurrentUsernameAsync();
|
||||
if (!await IsRowSiteAllowedAsync(n.SourceSiteId)) throw new UnauthorizedAccessException("Site access was revoked.");
|
||||
var requestedBy = await AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var response = await CommunicationService.DiscardNotificationAsync(
|
||||
new DiscardNotificationRequest(Guid.NewGuid().ToString("N"), n.NotificationId, requestedBy));
|
||||
if (response.Success)
|
||||
@@ -848,6 +850,9 @@
|
||||
return _permittedSiteIds.Contains(resolved.Id);
|
||||
}
|
||||
|
||||
private Task<bool> IsRowSiteAllowedAsync(string sourceSiteId)
|
||||
=> Task.FromResult(IsRowSiteAllowedSync(sourceSiteId));
|
||||
private async Task<bool> IsRowSiteAllowedAsync(string sourceSiteId)
|
||||
{
|
||||
var resolved = (await SiteRepository.GetAllSitesAsync()).FirstOrDefault(s => s.SiteIdentifier == sourceSiteId);
|
||||
return resolved is not null && await SiteScope.IsSiteAllowedAsync(resolved.Id);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-40
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/notifications/sms"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@@ -245,48 +248,18 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (_editingSms != null)
|
||||
if (_editingSms is null && typedAuthToken is null)
|
||||
{
|
||||
_editingSms.AccountSid = _accountSid.Trim();
|
||||
_editingSms.FromNumber = string.IsNullOrWhiteSpace(_fromNumber) ? null : _fromNumber.Trim();
|
||||
_editingSms.MessagingServiceSid = string.IsNullOrWhiteSpace(_messagingServiceSid)
|
||||
? null
|
||||
: _messagingServiceSid.Trim();
|
||||
_editingSms.ApiBaseUrl = string.IsNullOrWhiteSpace(_apiBaseUrl) ? null : _apiBaseUrl.Trim();
|
||||
// Preserve-if-blank: only overwrite the stored token when a new value was typed.
|
||||
if (typedAuthToken != null)
|
||||
{
|
||||
_editingSms.AuthToken = typedAuthToken;
|
||||
}
|
||||
_editingSms.ConnectionTimeoutSeconds = _connectionTimeoutSeconds;
|
||||
_editingSms.MaxRetries = _maxRetries;
|
||||
_editingSms.RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds);
|
||||
await NotificationRepository.UpdateSmsConfigurationAsync(_editingSms);
|
||||
_formError = "Auth Token is required.";
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (typedAuthToken == null)
|
||||
{
|
||||
_formError = "Auth Token is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
var sms = new SmsConfigurationEntity(
|
||||
_accountSid.Trim(),
|
||||
string.IsNullOrWhiteSpace(_fromNumber) ? null : _fromNumber.Trim())
|
||||
{
|
||||
MessagingServiceSid = string.IsNullOrWhiteSpace(_messagingServiceSid)
|
||||
? null
|
||||
: _messagingServiceSid.Trim(),
|
||||
ApiBaseUrl = string.IsNullOrWhiteSpace(_apiBaseUrl) ? null : _apiBaseUrl.Trim(),
|
||||
AuthToken = typedAuthToken,
|
||||
ConnectionTimeoutSeconds = _connectionTimeoutSeconds,
|
||||
MaxRetries = _maxRetries,
|
||||
RetryDelay = TimeSpan.FromSeconds(_retryDelaySeconds),
|
||||
};
|
||||
await NotificationRepository.AddSmsConfigurationAsync(sms);
|
||||
}
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
var from = string.IsNullOrWhiteSpace(_fromNumber) ? null : _fromNumber.Trim();
|
||||
var messaging = string.IsNullOrWhiteSpace(_messagingServiceSid) ? null : _messagingServiceSid.Trim();
|
||||
await MutationServices.ExecuteConfigurationAsync(_editingSms is not null
|
||||
? new UpdateSmsConfigCommand(_editingSms.Id, _accountSid.Trim(), from, messaging, _apiBaseUrl?.Trim() ?? "", typedAuthToken,
|
||||
_connectionTimeoutSeconds, _maxRetries, TimeSpan.FromSeconds(_retryDelaySeconds))
|
||||
: new CreateSmsConfigCommand(_accountSid.Trim(), from, messaging, _apiBaseUrl?.Trim(), typedAuthToken,
|
||||
_connectionTimeoutSeconds, _maxRetries, TimeSpan.FromSeconds(_retryDelaySeconds)));
|
||||
_showForm = false;
|
||||
_toast.ShowSuccess("SMS configuration saved.");
|
||||
await LoadAsync();
|
||||
|
||||
+8
-27
@@ -1,3 +1,6 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@inject IServiceProvider MutationServices
|
||||
@page "/notifications/smtp"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@@ -345,33 +348,11 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (_editingSmtp != null)
|
||||
{
|
||||
_editingSmtp.Host = _host.Trim();
|
||||
_editingSmtp.Port = _port;
|
||||
_editingSmtp.Transport = _transport;
|
||||
_editingSmtp.AuthType = _authType;
|
||||
_editingSmtp.TlsMode = _tlsMode;
|
||||
_editingSmtp.Credentials = _credentials?.Trim();
|
||||
_editingSmtp.FromAddress = _fromAddress.Trim();
|
||||
_editingSmtp.OAuth2Authority = NormalizeOptional(_oauth2Authority);
|
||||
_editingSmtp.OAuth2Scope = NormalizeOptional(_oauth2Scope);
|
||||
await NotificationRepository.UpdateSmtpConfigurationAsync(_editingSmtp);
|
||||
}
|
||||
else
|
||||
{
|
||||
var smtp = new SmtpConfigurationEntity(_host.Trim(), _authType, _fromAddress.Trim())
|
||||
{
|
||||
Port = _port,
|
||||
Transport = _transport,
|
||||
TlsMode = _tlsMode,
|
||||
Credentials = _credentials?.Trim(),
|
||||
OAuth2Authority = NormalizeOptional(_oauth2Authority),
|
||||
OAuth2Scope = NormalizeOptional(_oauth2Scope)
|
||||
};
|
||||
await NotificationRepository.AddSmtpConfigurationAsync(smtp);
|
||||
}
|
||||
await NotificationRepository.SaveChangesAsync();
|
||||
await MutationServices.ExecuteConfigurationAsync(_editingSmtp is not null
|
||||
? new UpdateSmtpConfigCommand(_editingSmtp.Id, _host.Trim(), _port, _authType, _fromAddress.Trim(), _tlsMode,
|
||||
_credentials?.Trim() ?? "", NormalizeOptional(_oauth2Authority) ?? "", NormalizeOptional(_oauth2Scope) ?? "", _transport)
|
||||
: new CreateSmtpConfigCommand(_host.Trim(), _port, _authType, _fromAddress.Trim(), _tlsMode,
|
||||
_credentials?.Trim(), NormalizeOptional(_oauth2Authority), NormalizeOptional(_oauth2Scope), _transport));
|
||||
_showForm = false;
|
||||
_toast.ShowSuccess("SMTP configuration saved.");
|
||||
await LoadAsync();
|
||||
|
||||
+7
-11
@@ -338,7 +338,8 @@ public partial class SiteCallsReport
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var requestedBy = await AuthStateProvider.GetCurrentUsernameAsync();
|
||||
if (!await IsRowSiteAllowedAsync(c.SourceSite)) throw new UnauthorizedAccessException("Site access was revoked.");
|
||||
var requestedBy = await AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var response = await CommunicationService.RetrySiteCallAsync(
|
||||
new RetrySiteCallRequest(Guid.NewGuid().ToString("N"), c.TrackedOperationId, c.SourceSite, requestedBy));
|
||||
ShowRelayOutcome(response.Outcome, response.SiteReachable, response.ErrorMessage,
|
||||
@@ -374,7 +375,8 @@ public partial class SiteCallsReport
|
||||
_actionInProgress = true;
|
||||
try
|
||||
{
|
||||
var requestedBy = await AuthStateProvider.GetCurrentUsernameAsync();
|
||||
if (!await IsRowSiteAllowedAsync(c.SourceSite)) throw new UnauthorizedAccessException("Site access was revoked.");
|
||||
var requestedBy = await AuthStateProvider.RequireRoleAsync(ZB.MOM.WW.ScadaBridge.Security.Roles.Deployer);
|
||||
var response = await CommunicationService.DiscardSiteCallAsync(
|
||||
new DiscardSiteCallRequest(Guid.NewGuid().ToString("N"), c.TrackedOperationId, c.SourceSite, requestedBy));
|
||||
ShowRelayOutcome(response.Outcome, response.SiteReachable, response.ErrorMessage,
|
||||
@@ -589,16 +591,10 @@ public partial class SiteCallsReport
|
||||
/// Server-side re-check for the Retry/Discard relay. True for a system-wide
|
||||
/// user, or when the row's source site maps to a Site.Id in the permitted set.
|
||||
/// </summary>
|
||||
private Task<bool> IsRowSiteAllowedAsync(string sourceSite)
|
||||
private async Task<bool> IsRowSiteAllowedAsync(string sourceSite)
|
||||
{
|
||||
if (_siteScopeSystemWide)
|
||||
return Task.FromResult(true);
|
||||
|
||||
var resolved = _allSites.FirstOrDefault(s => s.SiteIdentifier == sourceSite);
|
||||
if (resolved is null)
|
||||
return Task.FromResult(true);
|
||||
|
||||
return Task.FromResult(_permittedSiteIds.Contains(resolved.Id));
|
||||
var resolved = (await SiteRepository.GetAllSitesAsync()).FirstOrDefault(s => s.SiteIdentifier == sourceSite);
|
||||
return resolved is not null && await SiteScope.IsSiteAllowedAsync(resolved.Id);
|
||||
}
|
||||
|
||||
// ── Trends ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2,28 +2,11 @@
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<AntiforgeryToken />
|
||||
|
||||
@code {
|
||||
// CentralUI-005 / CentralUI-020: session expiry is a sliding window owned by
|
||||
// the cookie authentication middleware (ZB.MOM.WW.ScadaBridge.Security AddCookie:
|
||||
// ExpireTimeSpan = idle timeout, SlidingExpiration = true). An active user's
|
||||
// cookie is continually renewed; an idle user's cookie lapses after the idle
|
||||
// timeout. There is no fixed login-time deadline to redirect at.
|
||||
//
|
||||
// This component must NOT poll the Blazor AuthenticationStateProvider:
|
||||
// CookieAuthenticationStateProvider serves a frozen constructor-time
|
||||
// principal for the whole circuit (CentralUI-004), so the polled auth state
|
||||
// can never transition to "expired" and the redirect would never fire
|
||||
// (CentralUI-020).
|
||||
//
|
||||
// Instead it polls the server endpoint GET /auth/ping via fetch(). Being a
|
||||
// normal HTTP request, the cookie middleware re-validates — and slides — the
|
||||
// cookie on every hit, and answers 200 while the session is live or 401 once
|
||||
// it has lapsed. A genuine idle user's circuit produces no other HTTP
|
||||
// traffic, so once the cookie lapses the next ping returns 401 and this
|
||||
// component redirects to /login. (The ping itself slides the cookie, but the
|
||||
// poll interval is well under the idle timeout, so an idle session still
|
||||
// lapses on schedule once the poll catches the lapsed state — the ping only
|
||||
// ever observes expiry, it does not keep a dead session alive.)
|
||||
// Passive HTTP probes observe expiry but never advance activity. The JS module
|
||||
// separately reports trusted browser input through an antiforgery-protected POST.
|
||||
|
||||
/// <summary>Server endpoint that reports live session validity.</summary>
|
||||
internal const string PingUrl = "/auth/ping";
|
||||
@@ -49,6 +32,14 @@
|
||||
_ = PollSessionAsync(_cts.Token);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender || IsOnLoginPage) return;
|
||||
try { _module ??= await JS.InvokeAsync<IJSObjectReference>("import", ModulePath); }
|
||||
catch (JSDisconnectedException) { }
|
||||
catch (JSException) { }
|
||||
}
|
||||
|
||||
private bool IsOnLoginPage =>
|
||||
Navigation.ToBaseRelativePath(Navigation.Uri)
|
||||
.StartsWith("login", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -37,6 +37,7 @@ public static class ServiceCollectionExtensions
|
||||
// Resolves the current user's permitted site set from their SiteId claims
|
||||
// so Deployment/Monitoring pages can enforce site scoping.
|
||||
services.AddScoped<SiteScopeService>();
|
||||
services.AddScoped<ConfigurationCommandService>();
|
||||
|
||||
// Centralised dialog service: pages inject IDialogService and a single
|
||||
// <DialogHost /> in MainLayout renders the active dialog. See
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
@@ -39,7 +40,7 @@ public sealed class BindingTester : IBindingTester
|
||||
// roles, so the Designer check must happen here before any cross-cluster
|
||||
// traffic. Use HasClaim against JwtTokenService.RoleClaimType (not
|
||||
// IsInRole, per c1e16cf).
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
if (!state.User.HasClaim(JwtTokenService.RoleClaimType, Roles.Designer))
|
||||
{
|
||||
return new ReadTagValuesResult(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
@@ -45,7 +46,7 @@ public sealed class BrowseService : IBrowseService
|
||||
{
|
||||
// CentralUI-side role guard — sites don't enforce envelope-level roles,
|
||||
// so the Designer check must happen here before any cross-cluster traffic.
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
if (!state.User.HasClaim(JwtTokenService.RoleClaimType, Roles.Designer))
|
||||
{
|
||||
return new BrowseNodeResult(
|
||||
@@ -100,7 +101,7 @@ public sealed class BrowseService : IBrowseService
|
||||
// Same CentralUI-side role guard as BrowseChildrenAsync — sites don't
|
||||
// enforce envelope-level roles, so the Designer check must happen here
|
||||
// before any cross-cluster traffic.
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
if (!state.User.HasClaim(JwtTokenService.RoleClaimType, Roles.Designer))
|
||||
{
|
||||
return new SearchAddressSpaceResult(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
@@ -142,7 +143,7 @@ public sealed class CertManagementService : ICertManagementService
|
||||
|
||||
private async Task<bool> HasRoleAsync(string role)
|
||||
{
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
return state.User.HasClaim(JwtTokenService.RoleClaimType, role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
using ZB.MOM.WW.ScadaBridge.ManagementService;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
|
||||
/// <summary>Fresh caller authorization and audited configuration commands for page event handlers.</summary>
|
||||
public sealed class ConfigurationCommandService(IServiceProvider services, AuthenticationStateProvider auth)
|
||||
{
|
||||
/// <summary>Runs an existing audited instance operation with fresh scope and an atomic audit commit.</summary>
|
||||
public async Task<T> ExecuteInstanceAsync<T>(int? instanceId, int? createSiteId,
|
||||
Func<ZB.MOM.WW.ScadaBridge.TemplateEngine.Services.InstanceService, string, Task<T>> operation)
|
||||
{
|
||||
var username = await auth.RequireRoleAsync(Roles.Deployer);
|
||||
var sites = new SiteScopeService(auth);
|
||||
try
|
||||
{
|
||||
return (T)(await services.GetRequiredService<IConfigurationCommandTransaction>().ExecuteAsync(async sp =>
|
||||
{
|
||||
var siteId = createSiteId;
|
||||
if (instanceId.HasValue)
|
||||
{
|
||||
var instance = await sp.GetRequiredService<ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.ITemplateEngineRepository>().GetInstanceByIdAsync(instanceId.Value);
|
||||
siteId = instance?.SiteId;
|
||||
}
|
||||
if (!siteId.HasValue || !await sites.IsSiteAllowedAsync(siteId.Value))
|
||||
throw new UnauthorizedAccessException("Instance site access was revoked.");
|
||||
return await operation(sp.GetRequiredService<ZB.MOM.WW.ScadaBridge.TemplateEngine.Services.InstanceService>(), username);
|
||||
}))!;
|
||||
}
|
||||
finally { services.GetService<IConfigurationReadSession>()?.Reset(); }
|
||||
}
|
||||
|
||||
/// <summary>Executes one command through the same guarded boundary as the management actor.</summary>
|
||||
public async Task<object?> ExecuteAsync(object command)
|
||||
{
|
||||
var principal = (await auth.GetAuthoritativeStateAsync()).User;
|
||||
if (principal.Identity?.IsAuthenticated != true) throw new UnauthorizedAccessException("Session expired.");
|
||||
var user = new AuthenticatedUser(principal.GetUsername(), principal.GetDisplayName() ?? principal.GetUsername(),
|
||||
principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value).ToArray(),
|
||||
principal.FindAll(JwtTokenService.SiteIdClaimType).Select(c => c.Value).ToArray());
|
||||
try { return await ManagementActor.ExecuteConfigurationCommandAsync(services, command, user, services.GetService<CircuitClientAddressProvider>()?.ClientAddress); }
|
||||
finally { services.GetService<IConfigurationReadSession>()?.Reset(); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resolves the mutation facade only when a page acts, keeping read-only rendering independent.</summary>
|
||||
public static class ConfigurationCommandExtensions
|
||||
{
|
||||
/// <summary>Submits a database-only configuration mutation with current server-side authority.</summary>
|
||||
public static Task<object?> ExecuteConfigurationAsync(this IServiceProvider services, object command)
|
||||
=> services.GetRequiredService<ConfigurationCommandService>().ExecuteAsync(command);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using System.Security.Claims;
|
||||
using Akka.Actor;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@@ -106,7 +107,7 @@ public sealed class DataConnectionMoveService : IDataConnectionMoveService
|
||||
/// </summary>
|
||||
private async Task<AuthenticatedUser> BuildAuthenticatedUserAsync()
|
||||
{
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
var principal = state.User;
|
||||
|
||||
var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? "unknown";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Serialization;
|
||||
@@ -47,7 +48,7 @@ public sealed class EndpointVerificationService : IEndpointVerificationService
|
||||
{
|
||||
// CentralUI-side role guard — sites don't enforce envelope-level roles, so the
|
||||
// Designer check must happen here before any cross-cluster traffic.
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
if (!state.User.HasClaim(JwtTokenService.RoleClaimType, Roles.Designer))
|
||||
{
|
||||
return new VerifyEndpointResult(
|
||||
|
||||
@@ -299,7 +299,7 @@ public sealed class FlowDebugService : IFlowDebugService
|
||||
var shortId = c.ToString("N")[..12];
|
||||
try
|
||||
{
|
||||
var requestedBy = await _auth.GetCurrentUsernameAsync();
|
||||
var requestedBy = await _auth.RequireRoleAsync(Roles.Deployer);
|
||||
var corr = Guid.NewGuid().ToString("N");
|
||||
|
||||
if (isNotify)
|
||||
@@ -580,7 +580,7 @@ public sealed class FlowDebugService : IFlowDebugService
|
||||
|
||||
private async Task<AuthenticatedUser> BuildAuthenticatedUserAsync()
|
||||
{
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
var principal = state.User;
|
||||
|
||||
var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? "unknown";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Akka.Actor;
|
||||
@@ -144,7 +145,7 @@ public sealed class SchemaLibraryService : ISchemaLibraryService
|
||||
/// </summary>
|
||||
private async Task<AuthenticatedUser> BuildAuthenticatedUserAsync()
|
||||
{
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
var principal = state.User;
|
||||
|
||||
var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? "unknown";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Akka.Actor;
|
||||
@@ -165,7 +166,7 @@ public sealed class SecuredWriteService : ISecuredWriteService
|
||||
/// </summary>
|
||||
private async Task<AuthenticatedUser> BuildAuthenticatedUserAsync()
|
||||
{
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
var principal = state.User;
|
||||
|
||||
var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? "unknown";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Akka.Actor;
|
||||
@@ -112,7 +113,7 @@ public sealed class TemplateInheritanceQueryService : ITemplateInheritanceQueryS
|
||||
/// </summary>
|
||||
private async Task<AuthenticatedUser> BuildAuthenticatedUserAsync()
|
||||
{
|
||||
var state = await _auth.GetAuthenticationStateAsync();
|
||||
var state = await _auth.GetAuthoritativeStateAsync();
|
||||
var principal = state.User;
|
||||
|
||||
var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? "unknown";
|
||||
|
||||
@@ -21,3 +21,24 @@ export async function ping(url) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// SignalR event traffic and automatic pings are not user activity. Only trusted
|
||||
// browser input can schedule this antiforgery-protected request.
|
||||
let lastReported = 0;
|
||||
let reporting = false;
|
||||
async function recordInput(event) {
|
||||
if (!event.isTrusted || reporting || Date.now() - lastReported < 15000) return;
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||||
if (!token) return;
|
||||
reporting = true;
|
||||
try {
|
||||
const response = await fetch('/auth/activity', {
|
||||
method: 'POST', credentials: 'same-origin', cache: 'no-store',
|
||||
headers: { 'RequestVerificationToken': token }
|
||||
});
|
||||
if (response.ok) lastReported = Date.now();
|
||||
} catch { /* A failed request never advances the server's idle clock. */ }
|
||||
finally { reporting = false; }
|
||||
}
|
||||
for (const name of ['pointerdown', 'keydown', 'wheel', 'touchstart'])
|
||||
document.addEventListener(name, recordInput, { passive: true });
|
||||
|
||||
Reference in New Issue
Block a user