Files
managing-apps/src/Managing.Api/Program.cs

330 lines
13 KiB
C#

using System.Security.Claims;
using System.Text;
using System.Text.Json.Serialization;
using HealthChecks.UI.Client;
using Managing.Api.Authorization;
using Managing.Api.Filters;
using Managing.Api.HealthChecks;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Bootstrap;
using Managing.Common;
using Managing.Core.Middleawares;
using Managing.Infrastructure.Databases.InfluxDb.Models;
using Managing.Infrastructure.Databases.PostgreSql;
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
using Managing.Infrastructure.Evm.Models.Privy;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using NSwag;
using NSwag.Generation.Processors.Security;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.Elasticsearch;
using OpenApiSecurityRequirement = Microsoft.OpenApi.Models.OpenApiSecurityRequirement;
using OpenApiSecurityScheme = NSwag.OpenApiSecurityScheme;
// Builder
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.SetBasePath(AppContext.BaseDirectory);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json")
.AddJsonFile($"config.{builder.Environment.EnvironmentName}.json",
optional: true, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddUserSecrets<Program>();
SentrySdk.Init(options =>
{
// A Sentry Data Source Name (DSN) is required.
// See https://docs.sentry.io/concepts/key-terms/dsn-explainer/
// You can set it in the SENTRY_DSN environment variable, or you can set it in code here.
options.Dsn = builder.Configuration["Sentry:Dsn"];
// When debug is enabled, the Sentry client will emit detailed debugging information to the console.
// This might be helpful, or might interfere with the normal operation of your application.
// We enable it here for demonstration purposes when first trying Sentry.
// You shouldn't do this in your applications unless you're troubleshooting issues with Sentry.
options.Debug = false;
// Adds request URL and headers, IP and name for users, etc.
options.SendDefaultPii = true;
// This option is recommended. It enables Sentry's "Release Health" feature.
options.AutoSessionTracking = true;
// Enabling this option is recommended for client applications only. It ensures all threads use the same global scope.
options.IsGlobalModeEnabled = false;
// Example sample rate for your transactions: captures 10% of transactions
options.TracesSampleRate = 0.1;
options.Environment = builder.Environment.EnvironmentName;
});
// Add Service Defaults - using extension methods directly
builder.Services.AddServiceDiscovery();
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), ["api"]);
var postgreSqlConnectionString = builder.Configuration.GetSection(Constants.Databases.PostgreSql)["ConnectionString"];
var influxUrl = builder.Configuration.GetSection(Constants.Databases.InfluxDb)["Url"];
var web3ProxyUrl = builder.Configuration.GetSection("Web3Proxy")["BaseUrl"];
// Add HTTP client for Web3Proxy health check with detailed response
builder.Services.AddHttpClient("Web3ProxyHealthCheck")
.ConfigureHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(15); });
// Add HTTP client for GMX API health check
builder.Services.AddHttpClient("GmxHealthCheck")
.ConfigureHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(10); });
// Register Web3ProxyHealthCheck with the web3ProxyUrl
builder.Services.AddSingleton<Web3ProxyHealthCheck>(sp =>
new Web3ProxyHealthCheck(sp.GetRequiredService<IHttpClientFactory>(), web3ProxyUrl));
// Add PostgreSQL DbContext with improved concurrency and connection management
builder.Services.AddDbContext<ManagingDbContext>(options =>
{
options.UseNpgsql(postgreSqlConnectionString, npgsqlOptions =>
{
// Configure connection pooling and timeout settings for better concurrency
npgsqlOptions.CommandTimeout(60); // Increase command timeout for complex queries
npgsqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10),
errorCodesToAdd: null);
});
// Enable detailed errors in development
if (builder.Environment.IsDevelopment())
{
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging();
options.EnableThreadSafetyChecks(); // Enable thread safety checks in development
}
// Configure query tracking behavior for better performance
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); // Default to no tracking for better performance
// Enable service provider caching for better performance
options.EnableServiceProviderCaching();
// Enable connection resiliency for backtest and high-load scenarios
options.LogTo(msg => Console.WriteLine(msg), LogLevel.Warning); // Log warnings for connection issues
}, ServiceLifetime.Scoped); // Explicitly specify scoped lifetime for proper request isolation
// Add specific health checks for databases and other services
builder.Services.AddHealthChecks()
.AddNpgSql(postgreSqlConnectionString, name: "postgresql", tags: ["database"])
.AddUrlGroup(new Uri($"{influxUrl}/health"), name: "influxdb", tags: ["database"])
.AddCheck<Web3ProxyHealthCheck>("web3proxy", tags: ["api", "external"])
.AddCheck<CandleDataHealthCheck>("candle-data", tags: ["database", "candles"])
.AddCheck<GmxConnectivityHealthCheck>("gmx-connectivity", tags: ["api", "external"]);
builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
{
var envName = builder.Environment.EnvironmentName.ToLower().Replace(".", "-");
var indexFormat = $"managing-{envName}-" + "{0:yyyy.MM.dd}";
var yourTemplateName = "dotnetlogs";
var es = new ElasticsearchSinkOptions(new Uri(hostBuilder.Configuration["ElasticConfiguration:Uri"]))
{
IndexFormat = indexFormat.ToLower(),
AutoRegisterTemplate = true,
OverwriteTemplate = true,
TemplateName = yourTemplateName,
AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
TypeName = null,
BatchAction = ElasticOpType.Create,
MinimumLogEventLevel = LogEventLevel.Information,
DetectElasticsearchVersion = true,
RegisterTemplateFailure = RegisterTemplateRecovery.IndexAnyway,
};
loggerConfiguration
.WriteTo.Console()
.WriteTo.Elasticsearch(es);
});
builder.Services.AddOptions();
builder.Services.Configure<PostgreSqlSettings>(builder.Configuration.GetSection(Constants.Databases.PostgreSql));
builder.Services.Configure<InfluxDbSettings>(builder.Configuration.GetSection(Constants.Databases.InfluxDb));
builder.Services.Configure<PrivySettings>(builder.Configuration.GetSection(Constants.ThirdParty.Privy));
builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"],
ValidAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"],
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])),
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true
};
o.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// If you want to get the token from a custom header or query string
// var accessToken = context.Request.Query["access_token"];
// if (!string.IsNullOrEmpty(accessToken) &&
// context.HttpContext.Request.Path.StartsWithSegments("/hub"))
// {
// context.Token = accessToken;
// }
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
},
// --- IMPORTANT: Attach User to Context Here ---
OnTokenValidated = async context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
// Assuming your JWT token contains a 'nameid' claim (or similar) for the user ID
var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userId))
{
// Fetch the full user object from your service
var user = await userService.GetUserByAddressAsync(userId);
if (user != null)
{
// Attach the user object to HttpContext.Items
context.HttpContext.Items["User"] = user;
}
}
await Task.CompletedTask;
}
// --- END IMPORTANT ---
};
});
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{
builder
.SetIsOriginAllowed((host) => true)
.AllowAnyOrigin()
.WithOrigins("http://localhost:3000/")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
builder.Services.AddSignalR().AddJsonProtocol();
builder.Services.AddScoped<IJwtUtils, JwtUtils>();
builder.Services.RegisterApiDependencies(builder.Configuration);
// Orleans is always configured, but grains can be controlled
builder.Host.ConfigureOrleans(builder.Configuration, builder.Environment.IsProduction());
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(document =>
{
document.AddSecurity("JWT", Enumerable.Empty<string>(), new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header,
Description = "Type into the textbox: Bearer {your JWT token}."
});
document.OperationProcessors.Add(
new AspNetCoreOperationSecurityScopeProcessor("JWT"));
});
builder.Services.AddSwaggerGen(options =>
{
options.SchemaFilter<EnumSchemaFilter>();
options.AddSecurityDefinition("Bearer,", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = "Please insert your JWT Token into field : Bearer {your_token}",
Name = "Authorization",
Type = SecuritySchemeType.Http,
In = ParameterLocation.Header,
Scheme = "Bearer",
BearerFormat = "JWT"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
builder.WebHost.SetupDiscordBot();
// App
var app = builder.Build();
app.UseSerilogRequestLogging();
app.UseOpenApi();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Managing API v1");
c.RoutePrefix = string.Empty;
});
app.UseCors("CorsPolicy");
// Add Sentry diagnostics middleware (now using shared version)
app.UseSentryDiagnostics();
// Using shared GlobalErrorHandlingMiddleware from core project
app.UseMiddleware<GlobalErrorHandlingMiddleware>();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<BotHub>("/bothub");
endpoints.MapHub<BacktestHub>("/backtesthub");
endpoints.MapHub<CandleHub>("/candlehub");
endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
endpoints.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
});
app.Run();