124 lines
7.5 KiB
C#
124 lines
7.5 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Browser;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser;
|
|
using ZB.MOM.WW.Secrets.Ui;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI;
|
|
|
|
public static class EndpointRouteBuilderExtensions
|
|
{
|
|
/// <summary>
|
|
/// Mounts the AdminUI Razor components and the AdminUI static asset pipeline at the root.
|
|
/// Call from the fused Host's Program.cs alongside <c>app.MapOtOpcUaAuth()</c>.
|
|
/// </summary>
|
|
/// <typeparam name="TApp">The root component type for Razor pages.</typeparam>
|
|
/// <param name="app">The endpoint route builder.</param>
|
|
/// <returns>The same endpoint route builder, for chaining.</returns>
|
|
public static IEndpointRouteBuilder MapAdminUI<TApp>(this IEndpointRouteBuilder app)
|
|
where TApp : IComponent
|
|
{
|
|
// Razor class library static assets (_content/ZB.MOM.WW.OtOpcUa.AdminUI/**) are
|
|
// served via the Host's app.UseStaticFiles() middleware which must run BEFORE
|
|
// UseAuthentication() — see Program.cs.
|
|
// The Secrets.Ui RCL's routable page is deliberately NOT registered here (lmxopcua#483):
|
|
// this host's router is static with per-page @rendermode opt-in, so routing straight to
|
|
// the RCL page rendered it without a circuit — it displayed but every @onclick was dead.
|
|
// Pages/Secrets.razor in THIS assembly owns /admin/secrets instead (wrapping the RCL
|
|
// page with the render mode); registering the RCL routes too would make the route
|
|
// ambiguous.
|
|
app.MapRazorComponents<TApp>()
|
|
.AddInteractiveServerRenderMode();
|
|
return app;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds AdminUI services to the dependency injection container.
|
|
/// </summary>
|
|
/// <param name="services">The service collection.</param>
|
|
/// <returns>The same service collection, for chaining.</returns>
|
|
public static IServiceCollection AddAdminUI(this IServiceCollection services)
|
|
{
|
|
services.AddRazorComponents().AddInteractiveServerComponents();
|
|
services.AddOtOpcUaDriverStatusServices();
|
|
|
|
// Secrets-management UI (ZB.MOM.WW.Secrets.Ui RCL, mounted at /admin/secrets). Register its
|
|
// "secrets:manage"/"secrets:reveal" authorization policies additively onto the AuthorizationOptions
|
|
// that AddOtOpcUaAuth (called just before AddAdminUI on admin nodes) sets up — Configure<T> stacks,
|
|
// so this ADDS the two policies without disturbing FleetAdmin/DriverOperator/ConfigEditor. The
|
|
// policies' AdminRole = "Administrator" reads the same ClaimTypes.Role claim as FleetAdmin, so an
|
|
// existing Administrator satisfies them with no new group→role mapping. Registered here (the AdminUI
|
|
// composition layer that already references the RCL) rather than in the core Security lib.
|
|
services.Configure<AuthorizationOptions>(o => o.AddSecretsAuthorization());
|
|
|
|
// Browse pipeline — see docs/plans/2026-05-28-driver-browsers-design.md
|
|
services.AddSingleton<Browsing.BrowseSessionRegistry>();
|
|
services.AddHostedService<Browsing.BrowseSessionReaper>();
|
|
services.AddScoped<Browsing.IBrowserSessionService, Browsing.BrowserSessionService>();
|
|
services.AddScoped<IUnsTreeService, UnsTreeService>();
|
|
services.AddScoped<IRawTreeService, RawTreeService>();
|
|
// v3 Batch 3 (WP2): authoring-time UNS effective-name uniqueness guard. Consumed by
|
|
// UnsTreeService's reference/VirtualTag/ScriptedAlarm mutations (WP1); mirrors the
|
|
// deploy-time DraftValidator UnsEffectiveNameCollision rule.
|
|
services.AddScoped<IEffectiveNameGuard, EffectiveNameGuard>();
|
|
// #520 stage 7: the ACL scope picker's two trees + the authoring-time scope/principal checks that
|
|
// mirror the deploy gate's NodeAclScope* rules. Scoped, because it composes the two scoped tree
|
|
// services above.
|
|
services.AddScoped<Acls.IAclScopeService, Acls.AclScopeService>();
|
|
// WP5 CSV export enumeration (read-only; does not extend the closed IRawTreeService prelude).
|
|
services.AddScoped<RawTagCsvExportReader>();
|
|
services.AddSingleton<IDriverBrowser, OpcUaClientDriverBrowser>();
|
|
services.AddSingleton<IDriverBrowser, GalaxyDriverBrowser>();
|
|
// Bespoke Sql schema browser (server → schema → table → column). Constructible bare — every
|
|
// ctor param has a production default. The universal-browser DI line is deliberately NOT added
|
|
// for Sql: SqlDriver.CanBrowse is false, and this bespoke IDriverBrowser wins by construction
|
|
// (BrowserSessionService indexes bespoke browsers by DriverType before falling back to universal).
|
|
services.AddSingleton<IDriverBrowser, SqlDriverBrowser>();
|
|
services.AddSingleton<IDriverBrowser, MqttDriverBrowser>();
|
|
// Universal Discover-backed fallback browser (Wave 0). IDriverFactory is bound by the
|
|
// fused Host's DriverFactoryBootstrap; a standalone AdminUI has none → NullDriverFactory
|
|
// → CanBrowse false → pickers gracefully stay manual-entry (design §6).
|
|
services.AddSingleton<IUniversalDriverBrowser>(sp => new DiscoveryDriverBrowser(
|
|
sp.GetService<IDriverFactory>() ?? NullDriverFactory.Instance,
|
|
sp.GetRequiredService<ILogger<DiscoveryDriverBrowser>>()));
|
|
|
|
// Roslyn-backed Monaco script-editor analysis (diagnostics/completions/hover/...).
|
|
services.AddScoped<ScriptAnalysis.ScriptAnalysisService>();
|
|
services.AddScoped<ScriptAnalysis.IScriptTagCatalog, ScriptAnalysis.ScriptTagCatalog>();
|
|
// The catalog stays Scoped (per circuit); its six-table fleet-wide projection is shared across
|
|
// circuits behind a 5 s TTL snapshot — Monaco fires a completion request per keystroke.
|
|
services.AddSingleton<ScriptAnalysis.ScriptTagCatalogSnapshotCache>();
|
|
// TryAdd so a host that already bound a clock (test harness, future fused-host registration) wins.
|
|
services.TryAddSingleton<TimeProvider>(TimeProvider.System);
|
|
|
|
// /deployments drift chip. A singleton so its ConfigEdit-watermark cache is shared across
|
|
// circuits — otherwise every page load would recompose the whole fleet configuration.
|
|
// Constructed explicitly rather than by ctor scan: the compose seam is an optional parameter
|
|
// that only tests supply, and production must always get the real ConfigComposer default.
|
|
services.AddSingleton(sp => new Deployments.DeploymentDriftProbe(
|
|
sp.GetRequiredService<IDbContextFactory<OtOpcUaConfigDbContext>>()));
|
|
|
|
// Certificate-store actions (trust/untrust/delete) for the /certificates page.
|
|
services.AddSingleton<Certificates.CertificateStoreManager>();
|
|
|
|
// Structured audit-event sink: forwards AuditEvents to the ControlPlane AuditWriter singleton.
|
|
services.AddSingleton<ZB.MOM.WW.Audit.IAuditWriter, Audit.ActorAuditWriter>();
|
|
|
|
return services;
|
|
}
|
|
}
|