201 lines
7.8 KiB
C#
201 lines
7.8 KiB
C#
using System.Diagnostics;
|
|
using Baya.Application.Features.Users.Commands.Create;
|
|
using Baya.Application.Models.ApiResult;
|
|
using Baya.Application.Models.Identity;
|
|
using Baya.Application.ServiceConfiguration;
|
|
using Baya.Domain.Entities.User;
|
|
using Baya.Infrastructure.CrossCutting.Logging;
|
|
using Baya.Infrastructure.CrossCutting.ServiceConfiguration;
|
|
using Baya.Infrastructure.Identity.Identity.Dtos;
|
|
using Baya.Infrastructure.Identity.Jwt;
|
|
using Baya.Infrastructure.Identity.ServiceConfiguration;
|
|
using Baya.Infrastructure.Monitoring.Configurations;
|
|
using Baya.Infrastructure.Persistence.ServiceConfiguration;
|
|
using Baya.SharedKernel.Extensions;
|
|
using Baya.Web.Api.Configuration;
|
|
using Baya.Web.Plugins.Grpc;
|
|
using Baya.WebFramework.Filters;
|
|
using Baya.WebFramework.Middlewares;
|
|
using Baya.WebFramework.Routing;
|
|
using Baya.WebFramework.ServiceConfiguration;
|
|
using Baya.WebFramework.Swagger;
|
|
using Mapster;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
|
using Serilog;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Host.UseSerilog(LoggingConfiguration.ConfigureLogger);
|
|
|
|
var configuration = builder.Configuration;
|
|
|
|
// Fail fast if a load-bearing secret (DB connection, JWE/field-encryption keys) is missing or still a
|
|
// committed placeholder — before any service reaches for it. Skipped in the "Testing" environment.
|
|
builder.ValidateRequiredSecrets();
|
|
|
|
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
|
|
|
|
// HTTPS metadata is required for the token exchange in deployed environments; relaxed for local
|
|
// Development and the Testing host, which run over plain HTTP.
|
|
var requireHttpsMetadata = !builder.Environment.IsDevelopment() && !builder.Environment.IsEnvironment("Testing");
|
|
|
|
builder
|
|
.ConfigureHealthChecks()
|
|
.SetupOpenTelemetry();
|
|
|
|
builder.Services.Configure<IdentitySettings>(configuration.GetSection(nameof(IdentitySettings)));
|
|
|
|
var identitySettings = configuration.GetSection(nameof(IdentitySettings)).Get<IdentitySettings>();
|
|
|
|
builder.Services.AddControllers(options =>
|
|
{
|
|
options.Conventions.Add(new RouteTokenTransformerConvention(new SnakeCaseParameterTransformer()));
|
|
options.Filters.Add(typeof(OkResultAttribute));
|
|
options.Filters.Add(typeof(NotFoundResultAttribute));
|
|
options.Filters.Add(typeof(ContentResultFilterAttribute));
|
|
options.Filters.Add(typeof(ModelStateValidationAttribute));
|
|
options.Filters.Add(typeof(BadRequestResultFilterAttribute));
|
|
options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult<Dictionary<string, List<string>>>),
|
|
StatusCodes.Status400BadRequest));
|
|
options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult),
|
|
StatusCodes.Status401Unauthorized));
|
|
options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult),
|
|
StatusCodes.Status403Forbidden));
|
|
options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult),
|
|
StatusCodes.Status500InternalServerError));
|
|
|
|
}).ConfigureApiBehaviorOptions(options =>
|
|
{
|
|
options.SuppressModelStateInvalidFilter = true;
|
|
options.SuppressMapClientErrors = true;
|
|
});
|
|
|
|
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwagger("v1","v1.1");
|
|
|
|
|
|
builder.Services.AddApplicationServices()
|
|
.RegisterIdentityServices(identitySettings, requireHttpsMetadata)
|
|
.AddPersistenceServices(configuration)
|
|
.AddCrossCuttingSeams(configuration)
|
|
.AddWebFrameworkServices()
|
|
.AddCorsPolicies(configuration)
|
|
.AddForwardedHeadersConfiguration(configuration)
|
|
.AddRateLimitingPolicies();
|
|
|
|
// Development-only: capture each OTP in-memory so GET /api/v1/dev/last_otp/{phone} can complete a login
|
|
// without an SMS gateway. refinement-phase-8: the capture bridge runs ONLY while the log-only mock SMS sender is
|
|
// selected — once a real gateway (Seams:Sms:Provider) ships, the OTP is delivered over the wire and never logged
|
|
// or captured. Nothing here is wired in any other environment.
|
|
var smsProvider = configuration["Seams:Sms:Provider"];
|
|
var usingMockSms = string.IsNullOrWhiteSpace(smsProvider) || smsProvider.Equals("mock", StringComparison.OrdinalIgnoreCase);
|
|
if (builder.Environment.IsDevelopment() && usingMockSms)
|
|
builder.Services.AddDevelopmentOtpCapture();
|
|
|
|
// The IPaymentCaptureSimulator + bookings/convert path is a Development/Testing affordance (b10's real webhook
|
|
// confirm supersedes it in production). Re-register the succeeding mock over the production fail-closed stand-in.
|
|
if (builder.Environment.IsDevelopment() || builder.Environment.IsEnvironment("Testing"))
|
|
builder.Services.AddDevelopmentPaymentCapture();
|
|
|
|
builder.Services.RegisterValidatorsAsServices();
|
|
builder.Services.AddExceptionHandler<ExceptionHandler>();
|
|
|
|
builder.Services.AddMapster();
|
|
|
|
TypeAdapterConfig.GlobalSettings.Scan(typeof(UserCreateCommand).Assembly,
|
|
typeof(GetRolesDto).Assembly);
|
|
|
|
|
|
#region Plugin Services Configuration
|
|
|
|
builder.Services.ConfigureGrpcPluginServices(builder.Environment);
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
var app = builder.Build();
|
|
|
|
|
|
// Deploy-time migration one-shot (refinement-phase-7): `dotnet run -- migrate` (or `<binary> migrate`) applies
|
|
// EF migrations + the idempotent seeders, then exits. Running DDL as a separate deploy step means normal boots —
|
|
// especially concurrent multi-instance start-ups — never race on schema, and the runtime login needs no
|
|
// permanent DDL rights.
|
|
if (args.Any(a => string.Equals(a, "migrate", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
await app.ApplyMigrationsAsync();
|
|
await app.SeedDefaultUsersAsync();
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
await app.SeedPaymentGatewaysAsync();
|
|
await app.SeedDemoWorldAsync();
|
|
await app.SeedDemoLifecycleAsync();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Integration tests (WebApplicationFactory, env "Testing") run on in-memory SQLite — the SQL Server
|
|
// migrations can't apply there; the test factory does EnsureCreated + seeding itself.
|
|
if (!app.Environment.IsEnvironment("Testing"))
|
|
{
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
// Local convenience: apply migrations + seed on boot. Development-only: a sandbox payment gateway
|
|
// (all-zeros merchant id), a demo marketplace (nurses/variants/search rows, customers/patients), and
|
|
// the lifecycle demo world (bookings/money/reviews/tickets in every state) so the real-path screens
|
|
// aren't empty. None of it belongs in a deployed DB — all are idempotent.
|
|
await app.ApplyMigrationsAsync();
|
|
await app.SeedDefaultUsersAsync();
|
|
await app.SeedPaymentGatewaysAsync();
|
|
await app.SeedDemoWorldAsync();
|
|
await app.SeedDemoLifecycleAsync();
|
|
}
|
|
else
|
|
{
|
|
// Deployed: DDL is the separate `migrate` step above. Boot only *checks* the schema is current (fail fast
|
|
// on a pending migration) and seeds idempotent runtime data (roles + any configured break-glass admin).
|
|
await app.EnsureSchemaUpToDateAsync();
|
|
await app.SeedDefaultUsersAsync();
|
|
}
|
|
}
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseDeveloperExceptionPage();
|
|
}
|
|
else
|
|
app.UseExceptionHandler(_=>{});
|
|
|
|
// First in the pipeline so the resolved client IP (X-Forwarded-For, from a trusted proxy) is in place
|
|
// before anything downstream — notably the rate limiter — reads HttpContext.Connection.RemoteIpAddress.
|
|
app.UseForwardedHeaders();
|
|
|
|
app.UseSwaggerAndUi();
|
|
|
|
app.UseRouting();
|
|
|
|
// After UseRouting and before the rate limiter / authentication so a pre-flight OPTIONS is answered
|
|
// (and not rejected as 429/401) before the browser sends the real cross-origin request.
|
|
app.UseCors(CorsServiceExtension.PolicyName);
|
|
|
|
app.UseRateLimiter();
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
app.MapControllers();
|
|
|
|
app.UseMetrics()
|
|
.UseHealthChecks();
|
|
|
|
app.ConfigureGrpcPipeline();
|
|
|
|
await app.RunAsync();
|
|
|
|
/// <summary>Exposes the entry point to <c>WebApplicationFactory<Program></c>-based integration tests.</summary>
|
|
public partial class Program;
|
|
|
|
|
|
|