fix: Update appsettings.example.json to include server URLs

This commit is contained in:
HueByte
2026-02-19 04:42:38 +01:00
parent c9c76c43fa
commit d4d5a71ec4
2 changed files with 162 additions and 116 deletions
+161 -116
View File
@@ -13,140 +13,185 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Serilog; using Serilog;
// ── First-run setup ────────────────────────────────────────────────────────── // ── First-run setup (once) ──────────────────────────────────────────────────
FirstRunSetup.EnsureAppSettings(); FirstRunSetup.EnsureAppSettings();
var builder = WebApplication.CreateBuilder(args); // ── Bootstrap logger (replaced by full Serilog once host starts) ────────────
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
// ── Serilog ────────────────────────────────────────────────────────────────── // ── Auto-restart loop ───────────────────────────────────────────────────────
builder.Host.UseSerilog((context, config) => const int maxConsecutiveFailures = 5;
config.ReadFrom.Configuration(context.Configuration)); var consecutiveFailures = 0;
// ── SQLite + EF Core ────────────────────────────────────────────────────────── while (true)
var defaultDbPath = Path.Combine(AppContext.BaseDirectory, "echohub.db");
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? $"Data Source={defaultDbPath}";
builder.Services.AddDbContext<EchoHubDbContext>(options =>
options.UseSqlite(connectionString));
// ── JWT Authentication ────────────────────────────────────────────────────────
var jwtSecret = builder.Configuration["Jwt:Secret"]
?? throw new InvalidOperationException("Jwt:Secret must be configured.");
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "EchoHub.Server";
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "EchoHub.Client";
builder.Services.AddAuthentication(options =>
{ {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; var startTime = DateTimeOffset.UtcNow;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
};
// Allow SignalR clients to send the JWT via query string try
options.Events = new JwtBearerEvents
{ {
OnMessageReceived = context => var builder = WebApplication.CreateBuilder(args);
// ── Serilog ──────────────────────────────────────────────────────────
builder.Host.UseSerilog((context, config) =>
config.ReadFrom.Configuration(context.Configuration));
// ── SQLite + EF Core ─────────────────────────────────────────────────
var defaultDbPath = Path.Combine(AppContext.BaseDirectory, "echohub.db");
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? $"Data Source={defaultDbPath}";
builder.Services.AddDbContext<EchoHubDbContext>(options =>
options.UseSqlite(connectionString));
// ── JWT Authentication ───────────────────────────────────────────────
var jwtSecret = builder.Configuration["Jwt:Secret"]
?? throw new InvalidOperationException("Jwt:Secret must be configured.");
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "EchoHub.Server";
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "EchoHub.Client";
builder.Services.AddAuthentication(options =>
{ {
var accessToken = context.Request.Query["access_token"]; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
var path = context.HttpContext.Request.Path; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments(HubConstants.ChatHubPath)) .AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{ {
context.Token = accessToken; ValidateIssuer = true,
} ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
};
return Task.CompletedTask; // Allow SignalR clients to send the JWT via query string
}, options.Events = new JwtBearerEvents
}; {
}); OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
builder.Services.AddAuthorization(); if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments(HubConstants.ChatHubPath))
{
context.Token = accessToken;
}
// ── Controllers + SignalR ─────────────────────────────────────────────────────── return Task.CompletedTask;
builder.Services.AddControllers(); },
builder.Services.AddSignalR(); };
});
// ── Services ────────────────────────────────────────────────────────────────── builder.Services.AddAuthorization();
builder.Services.AddSingleton<JwtTokenService>();
builder.Services.AddSingleton<PresenceTracker>();
builder.Services.AddSingleton<ImageToAsciiService>();
builder.Services.AddSingleton<FileStorageService>();
builder.Services.AddHttpClient("ImageDownload", client =>
{
client.Timeout = TimeSpan.FromSeconds(15);
client.MaxResponseContentBufferSize = 10 * 1024 * 1024; // 10 MB
});
// ── Rate Limiting ──────────────────────────────────────────────────────────── // ── Controllers + SignalR ────────────────────────────────────────────
builder.Services.AddRateLimiter(options => builder.Services.AddControllers();
{ builder.Services.AddSignalR();
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("auth", limiter => // ── Services ─────────────────────────────────────────────────────────
builder.Services.AddSingleton<JwtTokenService>();
builder.Services.AddSingleton<PresenceTracker>();
builder.Services.AddSingleton<ImageToAsciiService>();
builder.Services.AddSingleton<FileStorageService>();
builder.Services.AddHttpClient("ImageDownload", client =>
{
client.Timeout = TimeSpan.FromSeconds(15);
client.MaxResponseContentBufferSize = 10 * 1024 * 1024; // 10 MB
});
// ── Rate Limiting ────────────────────────────────────────────────────
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("auth", limiter =>
{
limiter.PermitLimit = 10;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
options.AddFixedWindowLimiter("upload", limiter =>
{
limiter.PermitLimit = 5;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
options.AddFixedWindowLimiter("general", limiter =>
{
limiter.PermitLimit = 100;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
});
// ── CORS ─────────────────────────────────────────────────────────────
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
if (allowedOrigins is { Length: > 0 })
policy.WithOrigins(allowedOrigins);
else
policy.SetIsOriginAllowed(_ => true);
});
});
await using var app = builder.Build();
// ── Database initialization ──────────────────────────────────────────
await DatabaseSetup.InitializeAsync(app.Services);
// ── Middleware ────────────────────────────────────────────────────────
app.UseCors();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
// ── Routing ──────────────────────────────────────────────────────────
app.MapControllers();
app.MapHub<ChatHub>(HubConstants.ChatHubPath);
await app.RunAsync();
// Graceful shutdown (Ctrl+C) — exit the loop
Log.Information("Server shut down gracefully");
break;
}
catch (Exception ex)
{ {
limiter.PermitLimit = 10; var uptime = DateTimeOffset.UtcNow - startTime;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
options.AddFixedWindowLimiter("upload", limiter => // If server ran for over 60 seconds, it's a runtime crash — reset failure count
{ if (uptime.TotalSeconds > 60)
limiter.PermitLimit = 5; consecutiveFailures = 0;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
options.AddFixedWindowLimiter("general", limiter => consecutiveFailures++;
{ Log.Fatal(ex, "Server crashed after {Uptime:g} (failure {Count}/{Max})",
limiter.PermitLimit = 100; uptime, consecutiveFailures, maxConsecutiveFailures);
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
});
// ── CORS ───────────────────────────────────────────────────────────────────── if (consecutiveFailures >= maxConsecutiveFailures)
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>(); {
Log.Fatal("Too many consecutive failures, server will not restart");
break;
}
builder.Services.AddCors(options => var delaySeconds = Math.Min(Math.Pow(2, consecutiveFailures), 30);
{ Log.Information("Restarting server in {Delay}s...", delaySeconds);
options.AddDefaultPolicy(policy => await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
{ }
policy.AllowAnyHeader() }
.AllowAnyMethod()
.AllowCredentials();
if (allowedOrigins is { Length: > 0 }) Log.CloseAndFlush();
policy.WithOrigins(allowedOrigins);
else
policy.SetIsOriginAllowed(_ => true);
});
});
var app = builder.Build();
// ── Database initialization ───────────────────────────────────────────────────
await DatabaseSetup.InitializeAsync(app.Services);
// ── Middleware ─────────────────────────────────────────────────────────────────
app.UseCors();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
// ── Routing ───────────────────────────────────────────────────────────────────
app.MapControllers();
app.MapHub<ChatHub>(HubConstants.ChatHubPath);
app.Run();
@@ -1,4 +1,5 @@
{ {
"Urls": "http://0.0.0.0:5000",
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Data Source=echohub.db" "DefaultConnection": "Data Source=echohub.db"
}, },