Update managing api security

This commit is contained in:
2025-11-01 18:01:08 +07:00
parent 56c22ce806
commit b8c6f05805
8 changed files with 296 additions and 62 deletions

View File

@@ -1,8 +1,8 @@
using Managing.Domain.Users; using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using Managing.Domain.Users;
using Microsoft.IdentityModel.Tokens;
namespace Managing.Api.Authorization; namespace Managing.Api.Authorization;
@@ -16,21 +16,31 @@ public interface IJwtUtils
public class JwtUtils : IJwtUtils public class JwtUtils : IJwtUtils
{ {
private readonly string _secret; private readonly string _secret;
private readonly string? _issuer;
private readonly string? _audience;
public JwtUtils(IConfiguration config) public JwtUtils(IConfiguration config)
{ {
_secret = config.GetValue<string>("Jwt:Secret"); _secret = config.GetValue<string>("Jwt:Secret")
?? throw new InvalidOperationException("JWT secret is not configured.");
_issuer = config.GetValue<string>("Authentication:Schemes:Bearer:ValidIssuer");
_audience = config.GetValue<string>("Authentication:Schemes:Bearer:ValidAudiences");
} }
public string GenerateJwtToken(User user, string publicAddress) public string GenerateJwtToken(User user, string publicAddress)
{ {
// generate token that is valid for 15 minutes // Generate token that is valid for 15 days (as per original implementation)
var tokenHandler = new JwtSecurityTokenHandler(); var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_secret); var key = Encoding.UTF8.GetBytes(_secret); // Use UTF8 consistently with Program.cs
var tokenDescriptor = new SecurityTokenDescriptor var tokenDescriptor = new SecurityTokenDescriptor
{ {
Subject = new ClaimsIdentity(new[] { new Claim("address", publicAddress) }), Subject = new ClaimsIdentity(new[] { new Claim("address", publicAddress) }),
Expires = DateTime.UtcNow.AddDays(15), Expires = DateTime.UtcNow.AddDays(15),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) Issuer = _issuer, // Include issuer if configured
Audience = _audience, // Include audience if configured
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
}; };
var token = tokenHandler.CreateToken(tokenDescriptor); var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token); return tokenHandler.WriteToken(token);
@@ -42,7 +52,7 @@ public class JwtUtils : IJwtUtils
return null; return null;
var tokenHandler = new JwtSecurityTokenHandler(); var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_secret); var key = Encoding.UTF8.GetBytes(_secret); // Use UTF8 consistently with Program.cs
try try
{ {
tokenHandler.ValidateToken(token, new TokenValidationParameters tokenHandler.ValidateToken(token, new TokenValidationParameters

View File

@@ -1,4 +1,6 @@
using Managing.Api.Models.Requests; using System.Net.Http.Headers;
using System.Text;
using Managing.Api.Models.Requests;
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Shared; using Managing.Application.Shared;
@@ -289,6 +291,16 @@ public class TradingController : BaseController
var httpClient = _httpClientFactory.CreateClient(); var httpClient = _httpClientFactory.CreateClient();
// Add basic authentication if credentials are provided
var username = _configuration["N8n:Username"];
var password = _configuration["N8n:Password"];
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
}
_logger.LogInformation( _logger.LogInformation(
"Submitting indicator request: {IndicatorName} - {Strategy} by {Requester}", "Submitting indicator request: {IndicatorName} - {Strategy} by {Requester}",
request.IndicatorName, request.IndicatorName,

View File

@@ -11,35 +11,36 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.Npgsql" Version="8.1.0"/> <PackageReference Include="AspNetCore.HealthChecks.Npgsql" Version="8.1.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0"/> <PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0"/> <PackageReference Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2"/> <PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5"/> <PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2" />
<PackageReference Include="Microsoft.Orleans.Core" Version="9.2.1"/> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1"/> <PackageReference Include="Microsoft.Orleans.Core" Version="9.2.1" />
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7"/> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="OrleansDashboard" Version="8.2.0"/> <PackageReference Include="NSwag.AspNetCore" Version="14.0.7" />
<PackageReference Include="Sentry.AspNetCore" Version="5.5.1"/> <PackageReference Include="OrleansDashboard" Version="8.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/> <PackageReference Include="Sentry.AspNetCore" Version="5.5.1" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0"/> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0"/> <PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/> <PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0"/> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="10.0.0"/> <PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.6.1"/> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="10.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.6.1"/> <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.6.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.1"/> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.6.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.1"/> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.1" />
<PackageReference Include="xunit" Version="2.8.0"/> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.1" />
<PackageReference Include="Polly" Version="8.4.0"/> <PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0"/> <PackageReference Include="Polly" Version="8.4.0" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Managing.Bootstrap\Managing.Bootstrap.csproj"/> <ProjectReference Include="..\Managing.Bootstrap\Managing.Bootstrap.csproj" />
<ProjectReference Include="..\Managing.Aspire.ServiceDefaults\Managing.Aspire.ServiceDefaults.csproj"/> <ProjectReference Include="..\Managing.Aspire.ServiceDefaults\Managing.Aspire.ServiceDefaults.csproj" />
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/> <ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -61,6 +62,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Extensions\"/> <Folder Include="Extensions\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,6 @@
using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AspNetCoreRateLimit;
using HealthChecks.UI.Client; using HealthChecks.UI.Client;
using Managing.Api.Authorization; using Managing.Api.Authorization;
using Managing.Api.Filters; using Managing.Api.Filters;
@@ -15,6 +15,8 @@ using Managing.Infrastructure.Databases.PostgreSql;
using Managing.Infrastructure.Databases.PostgreSql.Configurations; using Managing.Infrastructure.Databases.PostgreSql.Configurations;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@@ -175,9 +177,64 @@ builder.Host.UseSerilog((hostBuilder, loggerConfiguration) =>
builder.Services.AddOptions(); builder.Services.AddOptions();
builder.Services.Configure<PostgreSqlSettings>(builder.Configuration.GetSection(Constants.Databases.PostgreSql)); builder.Services.Configure<PostgreSqlSettings>(builder.Configuration.GetSection(Constants.Databases.PostgreSql));
builder.Services.Configure<InfluxDbSettings>(builder.Configuration.GetSection(Constants.Databases.InfluxDb)); builder.Services.Configure<InfluxDbSettings>(builder.Configuration.GetSection(Constants.Databases.InfluxDb));
// Configure Request Size Limits (protect against DoS attacks)
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 10485760; // 10MB
options.ValueLengthLimit = 10485760; // 10MB
options.MultipartBoundaryLengthLimit = 256;
options.KeyLengthLimit = 2048;
});
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.Limits.MaxRequestBodySize = 10485760; // 10MB
options.Limits.MaxRequestHeadersTotalSize = 32768; // 32KB
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
// Configure Rate Limiting
builder.Services.AddMemoryCache();
builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
builder.Services.Configure<IpRateLimitPolicies>(builder.Configuration.GetSection("IpRateLimitPolicies"));
builder.Services.AddInMemoryRateLimiting();
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
builder.Services.AddControllers().AddJsonOptions(options => builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
// Validate JWT secret configuration
var jwtSecret = builder.Configuration["Jwt:Secret"];
if (string.IsNullOrWhiteSpace(jwtSecret))
{
throw new InvalidOperationException(
"JWT secret is not configured. Please set 'Jwt:Secret' in appsettings.json or environment variables.");
}
if (jwtSecret.Length < 32)
{
throw new InvalidOperationException(
$"JWT secret must be at least 32 characters long. Current length: {jwtSecret.Length}. " +
"This is a security requirement for production environments.");
}
// Get issuer and audience configuration
var validIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"];
var validAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"];
// Determine if validation should be enabled (enable in production, allow override via config)
var enableIssuerValidation = builder.Configuration.GetValue<bool>("Jwt:ValidateIssuer",
!builder.Environment.IsDevelopment());
var enableAudienceValidation = builder.Configuration.GetValue<bool>("Jwt:ValidateAudience",
!builder.Environment.IsDevelopment());
// Configure clock skew (tolerance for time differences between servers)
var clockSkewSeconds = builder.Configuration.GetValue<int>("Jwt:ClockSkewSeconds", 0);
var clockSkew = TimeSpan.FromSeconds(clockSkewSeconds);
builder.Services builder.Services
.AddAuthentication(options => .AddAuthentication(options =>
{ {
@@ -189,13 +246,16 @@ builder.Services
o.SaveToken = true; o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters o.TokenValidationParameters = new TokenValidationParameters
{ {
ValidIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"], ValidIssuer = validIssuer,
ValidAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"], ValidAudience = validAudience,
IssuerSigningKey = new SymmetricSecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])), ValidateIssuerSigningKey = true,
ValidateIssuer = false, ValidateIssuer = enableIssuerValidation && !string.IsNullOrWhiteSpace(validIssuer),
ValidateAudience = false, ValidateAudience = enableAudienceValidation && !string.IsNullOrWhiteSpace(validAudience),
ValidateIssuerSigningKey = true ValidateLifetime = true, // Explicitly validate token expiration
ClockSkew = clockSkew, // Configure clock skew tolerance
RequireExpirationTime = true, // Ensure tokens have expiration
RequireSignedTokens = true // Ensure tokens are signed
}; };
o.Events = new JwtBearerEvents o.Events = new JwtBearerEvents
{ {
@@ -212,9 +272,20 @@ builder.Services
}, },
OnAuthenticationFailed = context => OnAuthenticationFailed = context =>
{ {
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) var logger = context.HttpContext.RequestServices
.GetService<ILogger<Program>>();
if (context.Exception is SecurityTokenExpiredException)
{ {
context.Response.Headers.Add("Token-Expired", "true"); context.Response.Headers["Token-Expired"] = "true";
logger?.LogWarning("JWT token expired for request: {Path}",
context.Request.Path);
}
else
{
logger?.LogError(context.Exception,
"JWT authentication failed for request: {Path}",
context.Request.Path);
} }
return Task.CompletedTask; return Task.CompletedTask;
@@ -222,19 +293,46 @@ builder.Services
// --- IMPORTANT: Attach User to Context Here --- // --- IMPORTANT: Attach User to Context Here ---
OnTokenValidated = async context => OnTokenValidated = async context =>
{ {
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>(); var logger = context.HttpContext.RequestServices
// Assuming your JWT token contains a 'nameid' claim (or similar) for the user ID .GetService<ILogger<Program>>();
var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
try
if (!string.IsNullOrEmpty(userId))
{ {
// Fetch the full user object from your service var userService = context.HttpContext.RequestServices
var user = await userService.GetUserByAddressAsync(userId); .GetRequiredService<IUserService>();
if (user != null)
// JWT token contains 'address' claim (not NameIdentifier)
var address = context.Principal.FindFirst("address")?.Value;
if (!string.IsNullOrEmpty(address))
{ {
// Attach the user object to HttpContext.Items // Fetch the full user object from your service
context.HttpContext.Items["User"] = user; var user = await userService.GetUserByAddressAsync(address);
if (user != null)
{
// Attach the user object to HttpContext.Items
context.HttpContext.Items["User"] = user;
logger?.LogDebug("User {Address} authenticated successfully", address);
}
else
{
logger?.LogWarning(
"JWT token validated but user not found for address: {Address}",
address);
context.Fail("User not found");
}
} }
else
{
logger?.LogWarning("JWT token validated but 'address' claim not found");
context.Fail("Invalid token: missing address claim");
}
}
catch (Exception ex)
{
logger?.LogError(ex,
"Error during JWT token validation - user lookup failed");
context.Fail("Authentication failed: user lookup error");
} }
await Task.CompletedTask; await Task.CompletedTask;
@@ -248,6 +346,7 @@ var allowedCorsOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins") .GetSection("Cors:AllowedOrigins")
.Get<string[]>() ?? Array.Empty<string>(); .Get<string[]>() ?? Array.Empty<string>();
// Configure CORS with improved security
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy("CorsPolicy", policy => options.AddPolicy("CorsPolicy", policy =>
@@ -256,9 +355,11 @@ builder.Services.AddCors(options =>
{ {
policy policy
.WithOrigins(allowedCorsOrigins) .WithOrigins(allowedCorsOrigins)
.AllowAnyMethod() .WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.AllowAnyHeader() .WithHeaders("Content-Type", "Authorization", "X-Requested-With", "X-Correlation-ID")
.AllowCredentials(); .WithExposedHeaders("Token-Expired", "X-Correlation-ID")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromHours(24));
} }
else else
{ {
@@ -331,6 +432,41 @@ builder.WebHost.SetupDiscordBot();
var app = builder.Build(); var app = builder.Build();
app.UseSerilogRequestLogging(); app.UseSerilogRequestLogging();
// Rate Limiting - must be before authentication/authorization
app.UseIpRateLimiting();
// Security Headers Middleware - add early in pipeline
app.Use(async (context, next) =>
{
// Security headers for all responses
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers.Append("X-Frame-Options", "DENY");
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
context.Response.Headers.Append("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
// Content Security Policy - only for non-Swagger endpoints
if (!context.Request.Path.StartsWithSegments("/swagger") &&
!context.Request.Path.StartsWithSegments("/health") &&
!context.Request.Path.StartsWithSegments("/alive"))
{
context.Response.Headers.Append("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;");
}
// Remove server header (optional - Kestrel can be configured separately)
context.Response.Headers.Remove("Server");
await next();
});
// HSTS - HTTP Strict Transport Security (production only)
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
if (enableSwagger) if (enableSwagger)
{ {
app.UseOpenApi(); app.UseOpenApi();

View File

@@ -9,7 +9,10 @@
"Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ==" "Token": "eOuXcXhH7CS13Iw4CTiDDpRjIjQtEVPOloD82pLPOejI4n0BsEj1YzUw0g3Cs1mdDG5m-RaxCavCMsVTtS5wIQ=="
}, },
"N8n": { "N8n": {
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951" "WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951",
"IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11",
"Username": "",
"Password": ""
}, },
"Serilog": { "Serilog": {
"MinimumLevel": { "MinimumLevel": {

View File

@@ -23,12 +23,17 @@
} }
} }
}, },
"AllowedHosts": "*",
"Kaigen": { "Kaigen": {
"BaseUrl": "https://api.kaigen.managing.live", "BaseUrl": "https://api.kaigen.managing.live",
"DebitEndpoint": "/api/credits/debit", "DebitEndpoint": "/api/credits/debit",
"RefundEndpoint": "/api/credits/refund" "RefundEndpoint": "/api/credits/refund"
}, },
"N8n": {
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951",
"IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11",
"Username": "managing-api",
"Password": "mH%g5qr!WvCd6%9Fck22Xo"
},
"SqlMonitoring": { "SqlMonitoring": {
"Enabled": true, "Enabled": true,
"LoggingEnabled": false, "LoggingEnabled": false,
@@ -41,5 +46,58 @@
"https://app.kaigen.ai", "https://app.kaigen.ai",
"https://api.kaigen.ai" "https://api.kaigen.ai"
] ]
},
"Authentication": {
"Schemes": {
"Bearer": {
"ValidIssuer": "https://api.kaigen.ai",
"ValidAudiences": "https://app.kaigen.ai"
}
}
},
"Jwt": {
"ValidateIssuer": true,
"ValidateAudience": true,
"ClockSkewSeconds": 0
},
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"IpWhitelist": [],
"EndpointWhitelist": [],
"ClientWhitelist": [],
"GeneralRules": [
{
"Endpoint": "*",
"Period": "1m",
"Limit": 100
},
{
"Endpoint": "POST:/user/create-token",
"Period": "1m",
"Limit": 5
},
{
"Endpoint": "POST:*",
"Period": "1m",
"Limit": 30
}
]
},
"IpRateLimitPolicies": {
"IpRules": [],
"ClientRules": [],
"EndpointRules": []
},
"Kestrel": {
"Limits": {
"MaxRequestBodySize": 10485760,
"MaxRequestHeadersTotalSize": 32768,
"MaxConcurrentConnections": 100,
"MaxConcurrentUpgradedConnections": 100
}
} }
} }

View File

@@ -33,7 +33,9 @@
}, },
"N8n": { "N8n": {
"WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951", "WebhookUrl": "https://n8n.kai.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951",
"IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11" "IndicatorRequestWebhookUrl": "https://n8n.kai.managing.live/webhook/3aa07b66-1e64-46a7-8618-af300914cb11",
"Username": "",
"Password": ""
}, },
"Sentry": { "Sentry": {
"Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1", "Dsn": "https://fe12add48c56419bbdfa86227c188e7a@glitch.kai.managing.live/1",

View File

@@ -1,4 +1,6 @@
using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Common; using Managing.Common;
using Managing.Domain.Users; using Managing.Domain.Users;
@@ -20,6 +22,16 @@ public class WebhookService : IWebhookService
_configuration = configuration; _configuration = configuration;
_logger = logger; _logger = logger;
_n8nWebhookUrl = _configuration["N8n:WebhookUrl"] ?? string.Empty; _n8nWebhookUrl = _configuration["N8n:WebhookUrl"] ?? string.Empty;
// Configure basic authentication if credentials are provided
var username = _configuration["N8n:Username"];
var password = _configuration["N8n:Password"];
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
}
} }
public async Task SendTradeNotification(User user, string message, bool isBadBehavior = false) public async Task SendTradeNotification(User user, string message, bool isBadBehavior = false)