-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
154 lines (131 loc) · 7.85 KB
/
Copy pathProgram.cs
File metadata and controls
154 lines (131 loc) · 7.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
using System.Globalization;
using GridSim.Core.Licensing;
using GridSim.Web.Api;
using GridSim.Web.Components;
using GridSim.Web.Model;
using GridSim.Web.Realtime;
using GridSim.Web.Sessions;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
// GridSim.Web - Blazor Server dashboard + REST/WebSocket API over GridSim.Core.
// Pin InvariantCulture up front for the same reason the CLI does: every number we parse or emit
// (JSON, query strings) must be locale-independent so a comma-decimal host can't mis-read "0.5".
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Public replay-only instance (GridSim:ReplayOnly=true): offer ONLY the raw public-data replay. The solve REST
// API + the sim WebSocket are not mapped, the realtime estimator stays off, and the Blazor UI hides every
// solver/physics surface (Home redirects straight to the raw replay; Dashboard shows only the raw-safe tabs).
bool replayOnly = builder.Configuration.GetValue<bool>("GridSim:ReplayOnly");
// Software licence, resolved once, offline. Precedence: the Licence:Key config value (or the LICENCE__KEY
// / GRIDSIM_LICENCE environment variable), then %LOCALAPPDATA%\GridSim\licence.gridsim, then the free
// Personal tier. Registered as a singleton so components (footer) and the catalog can read it.
LicenceGate licence = LicenceGate.Resolve(builder.Configuration["Licence:Key"]);
builder.Services.AddSingleton(licence);
// One catalog for the process (filesystem discovery runs once); the session factory is stateless.
builder.Services.AddSingleton(new CaseCatalog(AppContext.BaseDirectory, licence));
builder.Services.AddSingleton<SessionManager>();
// Anonymised presence (who's online + the replay time they're viewing) and server CPU/RAM, shared by every
// Blazor circuit and shown in the LIVE tab.
builder.Services.AddSingleton<PresenceService>();
// Non-finite doubles (inf Q-limits, +inf purchased time) must serialize as null on EVERY
// HTTP JSON path, not just /ws/sim - /api/solve 500'd on any model carrying them (GS-H7).
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.Converters.Add(new NonFiniteDoubleConverter()));
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// ---- Shared estate single-sign-on: lightweight CONSUMER (recognise only) ------------------------
// This is a public raw-replay demo with no user store of its own. It joins the estate SSO purely to
// RECOGNISE a visitor who is already signed in on the parent domain — nothing is forced, no login/
// register UI is added, no Identity DB is touched. The estate auth cookie is a self-contained,
// DataProtection-encrypted cookie: to DECRYPT it we only need (1) the SAME key ring, (2) the SAME
// application name and (3) a cookie authentication scheme bound to the SAME cookie name/domain.
// All of this is ADDITIVE — every page stays fully accessible anonymously.
//
// KeysDirectory MUST point at the shared estate key ring on the host (the same folder tsgb-website
// persists) for decryption to succeed; if it is unset we fall back to a local dir, which simply means
// the estate cookie won't decrypt and every visitor is treated as anonymous — the app still runs.
var authAppName = builder.Configuration["Auth:ApplicationName"];
if (string.IsNullOrWhiteSpace(authAppName)) authAppName = "TSGBWebsite";
var authKeysDir = builder.Configuration["Auth:KeysDirectory"];
if (string.IsNullOrWhiteSpace(authKeysDir))
authKeysDir = Path.Combine(builder.Environment.ContentRootPath, "App_Data", "keys");
Directory.CreateDirectory(authKeysDir);
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(authKeysDir))
.SetApplicationName(authAppName);
builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
.AddCookie(IdentityConstants.ApplicationScheme, options =>
{
var cookieName = builder.Configuration["Auth:CookieName"];
options.Cookie.Name = string.IsNullOrWhiteSpace(cookieName)
? ".AspNetCore.Identity.Application"
: cookieName;
var cookieDomain = builder.Configuration["Auth:CookieDomain"];
if (!string.IsNullOrWhiteSpace(cookieDomain)) options.Cookie.Domain = cookieDomain;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.SlidingExpiration = true;
// Demo app: never bounce anyone to a login form — an unauthenticated visitor is simply anonymous.
});
builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
// The resident real-time estimator runs only when explicitly enabled (Realtime:Enabled=true) with a case
// and a measurement-window directory - it streams replayed/permitted-age history through the guarded engine.
// Default off keeps the app's behaviour unchanged.
if (!replayOnly && builder.Configuration.GetValue<bool>("Realtime:Enabled"))
{
string caseName = builder.Configuration["Realtime:Case"] ?? "case9";
string windowsDir = builder.Configuration["Realtime:WindowsDir"] ?? "";
builder.Services.AddSingleton<IRealtimeWindowFeed>(sp =>
new ReplayDirectoryWindowFeed(sp.GetRequiredService<CaseCatalog>().LoadModel(caseName), windowsDir));
builder.Services.AddHostedService<RealtimeEstimatorHostedService>();
}
WebApplication app = builder.Build();
// Behind nginx (loopback) terminating TLS — honour X-Forwarded-Proto/For so the request is seen as
// https and the SSO cookie's Secure policy (SameAsRequest) is satisfied. The proxy is trusted loopback,
// so clear the default known-proxy/network lists. ADDITIVE: harmless for direct/local requests too.
var fwd = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
};
fwd.KnownIPNetworks.Clear();
fwd.KnownProxies.Clear();
app.UseForwardedHeaders(fwd);
if (!app.Environment.IsDevelopment())
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseStaticFiles();
// Recognise the shared estate login cookie (decrypt-only; no forced auth). Placed after static files
// and before antiforgery/endpoints so User is populated for every component that reads it.
app.UseAuthentication();
app.UseAuthorization();
// Serve the git-ignored geography snapshot (DNO/GSP/asset GeoJSON) under /geo when present, so the
// network map's admin-boundary overlays light up. Optional & graceful: if the folder is absent the
// map-geo.js loaders just get 404s and return [] (overlays unavailable), exactly like the WPF app.
string geoDir = Path.GetFullPath(Path.Combine(app.Environment.ContentRootPath, "..", "..", "gda-out", "geo"));
if (Directory.Exists(geoDir))
{
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(geoDir),
RequestPath = "/geo",
ServeUnknownFileTypes = true, // .geojson has no default MIME mapping
DefaultContentType = "application/geo+json",
});
}
app.UseAntiforgery();
// The solve REST API (incl. POST /api/solve) and the raw sim WebSocket are external-consumer surfaces that drive
// the in-process solver - not offered on a replay-only instance. The Blazor UI drives its own SimSession, so the
// dashboard is unaffected. (Blazor's own SignalR circuit does not need app.UseWebSockets().)
if (!replayOnly)
{
app.UseWebSockets();
app.MapGroup("/api").MapGridSimApi();
// Streaming endpoint for external consumers (the Blazor UI drives its own SimSession in-process).
app.Map("/ws/sim", (HttpContext ctx, SessionManager manager) => SimWebSocket.HandleAsync(ctx, manager));
}
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();