From cd650c43658cfaa9b90d07244b5e1c503fab8ce0 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Tue, 19 May 2026 15:25:25 -0400 Subject: [PATCH] feat(manager): stand up DeepDrftManager CMS host (Phase 1 of two-app split) InteractiveServer only, full AuthBlocks, no WASM. Controllers scaffolded for future CMS controller migration. CmsStealthRoutingHandler omitted by design (subdomain topology). --- DeepDrftHome.sln | 14 ++ DeepDrftManager/Components/App.razor | 22 +++ DeepDrftManager/Components/Routes.razor | 19 +++ DeepDrftManager/Components/_Imports.razor | 8 + DeepDrftManager/DeepDrftManager.csproj | 25 +++ DeepDrftManager/Program.cs | 189 ++++++++++++++++++++++ DeepDrftManager/apikey.example.json | 5 + DeepDrftManager/appsettings.json | 15 ++ DeepDrftManager/authblocks.example.json | 19 +++ DeepDrftManager/connections.example.json | 6 + 10 files changed, 322 insertions(+) create mode 100644 DeepDrftManager/Components/App.razor create mode 100644 DeepDrftManager/Components/Routes.razor create mode 100644 DeepDrftManager/Components/_Imports.razor create mode 100644 DeepDrftManager/DeepDrftManager.csproj create mode 100644 DeepDrftManager/Program.cs create mode 100644 DeepDrftManager/apikey.example.json create mode 100644 DeepDrftManager/appsettings.json create mode 100644 DeepDrftManager/authblocks.example.json create mode 100644 DeepDrftManager/connections.example.json diff --git a/DeepDrftHome.sln b/DeepDrftHome.sln index b214d54..74b85ff 100644 --- a/DeepDrftHome.sln +++ b/DeepDrftHome.sln @@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Data", "Dee EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftCms", "DeepDrftCms\DeepDrftCms.csproj", "{81F1D47F-F892-45FB-9E35-D7775805FFD3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftManager", "DeepDrftManager\DeepDrftManager.csproj", "{E50071B2-A59F-4FB7-A435-5D966C538DDD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -150,6 +152,18 @@ Global {81F1D47F-F892-45FB-9E35-D7775805FFD3}.Release|x64.Build.0 = Release|Any CPU {81F1D47F-F892-45FB-9E35-D7775805FFD3}.Release|x86.ActiveCfg = Release|Any CPU {81F1D47F-F892-45FB-9E35-D7775805FFD3}.Release|x86.Build.0 = Release|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|x64.ActiveCfg = Debug|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|x64.Build.0 = Debug|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|x86.ActiveCfg = Debug|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Debug|x86.Build.0 = Debug|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|Any CPU.Build.0 = Release|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|x64.ActiveCfg = Release|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|x64.Build.0 = Release|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|x86.ActiveCfg = Release|Any CPU + {E50071B2-A59F-4FB7-A435-5D966C538DDD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DeepDrftManager/Components/App.razor b/DeepDrftManager/Components/App.razor new file mode 100644 index 0000000..3f117cd --- /dev/null +++ b/DeepDrftManager/Components/App.razor @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/DeepDrftManager/Components/Routes.razor b/DeepDrftManager/Components/Routes.razor new file mode 100644 index 0000000..0102731 --- /dev/null +++ b/DeepDrftManager/Components/Routes.razor @@ -0,0 +1,19 @@ + + + + + @{ + NavigationManager.NavigateTo($"account/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); + } + + + + + + Not found +

Sorry, there's nothing at this address.

+
+
+ +@inject NavigationManager NavigationManager diff --git a/DeepDrftManager/Components/_Imports.razor b/DeepDrftManager/Components/_Imports.razor new file mode 100644 index 0000000..d2fb54e --- /dev/null +++ b/DeepDrftManager/Components/_Imports.razor @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using MudBlazor +@using DeepDrftManager +@using DeepDrftManager.Components diff --git a/DeepDrftManager/DeepDrftManager.csproj b/DeepDrftManager/DeepDrftManager.csproj new file mode 100644 index 0000000..78c0b25 --- /dev/null +++ b/DeepDrftManager/DeepDrftManager.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/DeepDrftManager/Program.cs b/DeepDrftManager/Program.cs new file mode 100644 index 0000000..e72ab3f --- /dev/null +++ b/DeepDrftManager/Program.cs @@ -0,0 +1,189 @@ +using AuthBlocksLib; +using AuthBlocksLib.Options; +using DeepDrftCms; +using DeepDrftData; +using DeepDrftData.Data; +using DeepDrftData.Repositories; +using DeepDrftManager.Components; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.EntityFrameworkCore; +using MudBlazor.Services; +using NetBlocks.Utilities.Environment; + +var builder = WebApplication.CreateBuilder(args); + +// Required credential files — must exist before the app will start. +// Production secrets stay gitignored; the *.example.json templates at the project root show the shape. +// - environment/apikey.json: { "DeepDrftContent": { "ApiKey": "..." } } +// - environment/connections.json: { "ConnectionStrings": { "DefaultConnection": "...", "Auth": "..." } } +// - environment/authblocks.json: { "AuthBlocks": { "Jwt": {...}, "Email": {...}, "Admin": {...} } } +// Content API key — not consumed by this host in Phase 1. Required by the CredentialTools +// pattern (the file must exist); will be used by CmsUploadController when it migrates here. +var apiKeyPath = CredentialTools.ResolvePathOrThrow("apikey", "environment/apikey.json"); +builder.Configuration.AddJsonFile(apiKeyPath, optional: false, reloadOnChange: false); + +var connectionsPath = CredentialTools.ResolvePathOrThrow("connections", "environment/connections.json"); +builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false); + +var authBlocksPath = CredentialTools.ResolvePathOrThrow("authblocks", "environment/authblocks.json"); +builder.Configuration.AddJsonFile(authBlocksPath, optional: false, reloadOnChange: false); + +// MudBlazor. +builder.Services.AddMudServices(); + +// CMS-specific services (currently a no-op placeholder; reserved for future RCL additions). +builder.Services.AddCmsServices(); + +// SQL metadata domain — DbContext + repository + manager. The CMS pages inject ITrackService +// and resolve the same scoped TrackManager instance, so the DTO and entity surfaces share state. +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); + +builder.Services + .AddScoped() + .AddScoped() + .AddScoped(sp => sp.GetRequiredService()); + +// AuthBlocks: JWT Bearer auth, Identity, EF schema, admin seeding. +// Auth schema runs in its own database (separate from DefaultConnection by design). +builder.Services.AddAuthBlocks(options => +{ + options.ConnectionString = builder.Configuration.GetConnectionString("Auth") + ?? throw new InvalidOperationException("ConnectionStrings:Auth is required"); + options.ApplicationName = "DeepDrft"; + options.SupportEmail = builder.Configuration["AuthBlocks:SupportEmail"] ?? "admin@deepdrft.com"; + + options.JwtSettings.Secret = builder.Configuration["AuthBlocks:Jwt:Secret"] + ?? throw new InvalidOperationException("AuthBlocks:Jwt:Secret is required"); + options.JwtSettings.Issuer = builder.Configuration["AuthBlocks:Jwt:Issuer"] + ?? throw new InvalidOperationException("AuthBlocks:Jwt:Issuer is required"); + options.JwtSettings.Audience = builder.Configuration["AuthBlocks:Jwt:Audience"] + ?? throw new InvalidOperationException("AuthBlocks:Jwt:Audience is required"); + + options.EmailConnection.Host = builder.Configuration["AuthBlocks:Email:Host"] + ?? throw new InvalidOperationException("AuthBlocks:Email:Host is required"); + options.EmailConnection.Token = builder.Configuration["AuthBlocks:Email:Token"] + ?? throw new InvalidOperationException("AuthBlocks:Email:Token is required"); + + options.AdminUserSettings = new AdminUserSettings + { + UserName = builder.Configuration["AuthBlocks:Admin:UserName"] + ?? throw new InvalidOperationException("AuthBlocks:Admin:UserName is required"), + Email = builder.Configuration["AuthBlocks:Admin:Email"] + ?? throw new InvalidOperationException("AuthBlocks:Admin:Email is required"), + Password = builder.Configuration["AuthBlocks:Admin:Password"] + ?? throw new InvalidOperationException("AuthBlocks:Admin:Password is required") + }; +}); + +// AuthBlocksWeb: server-side cascading auth state plus the JWT client services used by the +// /account/login + /account/logout Razor pages that ship in the AuthBlocksWeb RCL. +var baseUrl = GetKestrelUrl(builder); +AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl); + +// Named HttpClient used by CMS pages for delete/upload calls. +// Phase 1: points at DeepDrftWeb (https://localhost:5001) where the CMS mutation controllers +// (CmsUploadController, CmsEditController, CmsDeleteController) currently live. +// When those controllers migrate to DeepDrftManager, update ApiUrls:ApiHost to this host's URL. +var apiHostUrl = builder.Configuration["ApiUrls:ApiHost"] + ?? throw new InvalidOperationException("ApiUrls:ApiHost is required"); +builder.Services.AddHttpClient("DeepDrft.API", client => +{ + client.BaseAddress = new Uri(apiHostUrl); +}); + +// Reverse-proxy support (nginx in production). +builder.Services.Configure(options => +{ + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +// Controllers: no-op until CMS mutation controllers migrate from DeepDrftWeb, but registered +// now so they are discovered automatically when they arrive. Matches DeepDrftWeb precedent. +builder.Services.AddControllers(); + +// InteractiveServer only — no WASM render mode on the CMS host. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +// SignalR tuning for InteractiveServer circuits. Long-running CMS operations (upload progress, +// large table loads) benefit from tighter keepalive and detailed errors in dev. +builder.Services.AddSignalR(options => +{ + if (builder.Environment.IsDevelopment()) + { + options.EnableDetailedErrors = true; + options.KeepAliveInterval = TimeSpan.FromSeconds(10); + options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); + } +}); + +var app = builder.Build(); + +// Apply AuthBlocks EF migrations, seed system roles, seed admin user on first boot. +await app.Services.UseAuthBlocksStartupAsync(); + +app.UseForwardedHeaders(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); + + var disableHttpsRedirection = app.Configuration.GetValue("ForwardedHeaders:DisableHttpsRedirection"); + if (!disableHttpsRedirection) + { + app.UseHttpsRedirection(); + } +} + +app.UseAuthentication(); +app.UseAuthorization(); +app.UseAntiforgery(); + +app.MapStaticAssets(); + +// Mount AuthBlocks API surface (/api/auth/*, /api/users/*, etc.) and the AuthBlocksWeb +// Razor pages (/account/login, /account/logout). +app.MapAuthBlocks(); + +// No-op today; picks up CMS mutation controllers when they migrate from DeepDrftWeb. +app.MapControllers(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddAdditionalAssemblies( + typeof(DeepDrftCms._Imports).Assembly, + typeof(AuthBlocksWeb._Imports).Assembly); + +app.Run(); + +// Local helper — mirrors DeepDrftWeb.Startup.GetKestrelUrl. Kept inline because this host's +// only consumer is right here; promoting to a shared library would be premature. +static string GetKestrelUrl(WebApplicationBuilder builder) +{ + var urls = builder.Configuration["ASPNETCORE_URLS"] + ?? builder.Configuration["urls"]; + + if (!string.IsNullOrEmpty(urls)) + { + return urls.Split(';')[0].Trim(); + } + + var firstEndpoint = builder.Configuration.GetSection("Kestrel:Endpoints").GetChildren().FirstOrDefault(); + var endpointUrl = firstEndpoint?["Url"]; + + if (!string.IsNullOrEmpty(endpointUrl)) + { + return endpointUrl; + } + + return builder.Environment.IsDevelopment() + ? "https://localhost:5004" + : "http://localhost:5000"; +} diff --git a/DeepDrftManager/apikey.example.json b/DeepDrftManager/apikey.example.json new file mode 100644 index 0000000..9300155 --- /dev/null +++ b/DeepDrftManager/apikey.example.json @@ -0,0 +1,5 @@ +{ + "DeepDrftContent": { + "ApiKey": "your-secret-api-key-here" + } +} diff --git a/DeepDrftManager/appsettings.json b/DeepDrftManager/appsettings.json new file mode 100644 index 0000000..3353dcd --- /dev/null +++ b/DeepDrftManager/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ApiUrls": { + "ApiHost": "https://localhost:5001" + }, + "ForwardedHeaders": { + "DisableHttpsRedirection": false + } +} diff --git a/DeepDrftManager/authblocks.example.json b/DeepDrftManager/authblocks.example.json new file mode 100644 index 0000000..1cf53de --- /dev/null +++ b/DeepDrftManager/authblocks.example.json @@ -0,0 +1,19 @@ +{ + "AuthBlocks": { + "SupportEmail": "admin@deepdrft.com", + "Jwt": { + "Secret": "your-jwt-secret-here", + "Issuer": "https://deepdrft.com", + "Audience": "deepdrft-users" + }, + "Email": { + "Host": "smtp.your-provider.com", + "Token": "your-email-token-here" + }, + "Admin": { + "UserName": "admin", + "Email": "admin@deepdrft.com", + "Password": "your-admin-password-here" + } + } +} diff --git a/DeepDrftManager/connections.example.json b/DeepDrftManager/connections.example.json new file mode 100644 index 0000000..d411981 --- /dev/null +++ b/DeepDrftManager/connections.example.json @@ -0,0 +1,6 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5433;Database=deepdrft;Username=postgres;Password=your-password-here", + "Auth": "Host=localhost;Port=5433;Database=deepdrft_auth;Username=postgres;Password=your-password-here" + } +}