using DodoSSH.Api.Authorization; using DodoSSH.Api.Features.Events; using DodoSSH.Api.Features.Identity; using DodoSSH.Api.Features.Sync; using DodoSSH.Api.Features.Teams; using DodoSSH.Api.Setup; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Policy; var builder = WebApplication.CreateBuilder(args); // Docker secrets, if mounted. Optional so the same image works with plain environment variables. builder.Configuration.AddKeyPerFile("/run/secrets", optional: true); builder.Configuration.AddEnvironmentVariables(prefix: "DODOSSH_"); builder.Services.AddDodoJson(); builder.Services.AddDodoOptions(); builder.Services.AddDodoPersistence(builder.Configuration); builder.Services.AddDodoAuthentication(); builder.Services.AddDodoEndpoints(); builder.Services.AddDodoOpenApi(); builder.Services.AddDodoHealthChecks(); // DateTime.UtcNow is banned repo-wide (see BannedSymbols.txt); everything takes // TimeProvider so time can be faked in tests. builder.Services.AddSingleton(TimeProvider.System); // FastEndpoints registers this too. Kept because CurrentUserContext needs it on its own merits, and // the registration would be silently lost the day the endpoint framework changed again. builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Registered twice on purpose, resolving to the same scoped instance: the endpoints take the // concrete service, and CurrentUserContext takes only the claim it needs, so the sign-in path does // not gain a reference to the whole of a feature it calls one method on. builder.Services.AddScoped( provider => provider.GetRequiredService()); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); // A singleton, because the sockets it holds outlive the requests that opened them. Registered twice // resolving to the same instance, for the reason the invitation claim above is: the endpoint needs the // whole hub — admit, remove, count — while the write paths that announce a change need only the two // methods that announce one, and should not gain a reference to connection management to get them. builder.Services.AddSingleton(); builder.Services.AddSingleton( provider => provider.GetRequiredService()); // Scoped rather than the AddAuthorization default of singleton: the handler reads the request's // DbContext, and a singleton would capture one for the lifetime of the process. builder.Services.AddScoped(); // Replaces the framework default, which is also registered as a singleton. Last registration wins. builder.Services.AddSingleton(); builder.Services.AddProblemDetails(); var app = builder.Build(); // Deliberately no UseHttpsRedirection: the API is always fronted by a reverse proxy // (Caddy in the reference compose stack) which terminates TLS. Redirecting here // produces redirect loops behind a proxy. HTTPS in development comes from the // launch profile instead. app.BlockFastEndpointsRouteTable(); // Before the authentication middleware, because the upgrade handshake has to survive it: the events // endpoint answers an ordinary authenticated request that happens to become a socket, and without // this the upgrade is never offered and the handler sees a plain GET. No allow-list of origins is // configured, deliberately — every client here is a native application sending a bearer token, so // there is no browser origin to trust and nothing a cross-site request could reach without one. app.UseWebSockets(); app.UseAuthentication(); app.UseAuthorization(); app.MapDodoHealthChecks(); app.MapDodoEndpoints(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.WarnOnRiskyConfiguration(); // Before the port opens, not after. See Persistence.MigrateDodoDatabaseAsync. await app.MigrateDodoDatabaseAsync().ConfigureAwait(false); await app.RunAsync().ConfigureAwait(false);