Update managing api security
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<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="AspNetCoreRateLimit" Version="5.0.0" />
|
||||||
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2" />
|
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
|
||||||
<PackageReference Include="Microsoft.Orleans.Core" Version="9.2.1" />
|
<PackageReference Include="Microsoft.Orleans.Core" Version="9.2.1" />
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(userId))
|
try
|
||||||
|
{
|
||||||
|
var userService = context.HttpContext.RequestServices
|
||||||
|
.GetRequiredService<IUserService>();
|
||||||
|
|
||||||
|
// JWT token contains 'address' claim (not NameIdentifier)
|
||||||
|
var address = context.Principal.FindFirst("address")?.Value;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(address))
|
||||||
{
|
{
|
||||||
// Fetch the full user object from your service
|
// Fetch the full user object from your service
|
||||||
var user = await userService.GetUserByAddressAsync(userId);
|
var user = await userService.GetUserByAddressAsync(address);
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
// Attach the user object to HttpContext.Items
|
// Attach the user object to HttpContext.Items
|
||||||
context.HttpContext.Items["User"] = user;
|
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();
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user