065a83442c
Enhanced the application's service layer by adding FormValidationService and EventDefinitionService to the dependency injection container in Program.cs. Updated Create, Edit, and other relevant components to utilize these services for improved form validation and event processing functionality.
294 lines
10 KiB
C#
294 lines
10 KiB
C#
using Data;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using MudBlazor.Services;
|
|
using Serilog;
|
|
using System.Text.Json;
|
|
using WebApp;
|
|
using WebApp.Authentication;
|
|
using WebApp.Components;
|
|
using WebApp.Logging;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Load appsettings from Data directory (works in both development and production)
|
|
// This allows runtime configuration changes without touching the codebase
|
|
var dataAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "appsettings.json");
|
|
if (!File.Exists(dataAppSettingsPath))
|
|
{
|
|
Console.WriteLine($"appsettings.json not found at {dataAppSettingsPath}. Creating from template...");
|
|
|
|
try
|
|
{
|
|
// Ensure Data directory exists
|
|
Directory.CreateDirectory(Path.Combine(builder.Environment.ContentRootPath, "Data"));
|
|
|
|
// Copy ChapterSettings and ValidationSettings from the base appsettings.json
|
|
var baseAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "appsettings.json");
|
|
if (File.Exists(baseAppSettingsPath))
|
|
{
|
|
var baseConfig = File.ReadAllText(baseAppSettingsPath);
|
|
var baseDoc = JsonDocument.Parse(baseConfig);
|
|
|
|
var templateSettings = new Dictionary<string, object?>();
|
|
|
|
if (baseDoc.RootElement.TryGetProperty("ChapterSettings", out var chapterSettings))
|
|
{
|
|
templateSettings["ChapterSettings"] = JsonSerializer.Deserialize<object>(chapterSettings.GetRawText());
|
|
}
|
|
|
|
if (baseDoc.RootElement.TryGetProperty("ValidationSettings", out var validationSettings))
|
|
{
|
|
templateSettings["ValidationSettings"] = JsonSerializer.Deserialize<object>(validationSettings.GetRawText());
|
|
}
|
|
|
|
if (templateSettings.Any())
|
|
{
|
|
var json = JsonSerializer.Serialize(templateSettings, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true
|
|
});
|
|
|
|
File.WriteAllText(dataAppSettingsPath, json);
|
|
Console.WriteLine("appsettings.json created in Data directory.");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("WARNING: No settings to copy from base appsettings.json");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"WARNING: Unable to create appsettings.json: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Add Data/appsettings.json as additional configuration source (all environments)
|
|
if (File.Exists(dataAppSettingsPath))
|
|
{
|
|
builder.Configuration.AddJsonFile(dataAppSettingsPath, optional: true, reloadOnChange: true);
|
|
Console.WriteLine($"Loaded configuration from {dataAppSettingsPath}");
|
|
}
|
|
|
|
// Configure Serilog with custom handling for antiforgery errors
|
|
builder.Host.UseSerilog((context, configuration) =>
|
|
{
|
|
var consoleOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}";
|
|
var fileOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}";
|
|
|
|
// Create console logger with antiforgery error handling
|
|
var consoleConfig = new LoggerConfiguration()
|
|
.WriteTo.Console(outputTemplate: consoleOutputTemplate);
|
|
var consoleLogger = consoleConfig.CreateLogger();
|
|
|
|
// Create file logger with antiforgery error handling
|
|
var fileConfig = new LoggerConfiguration()
|
|
.WriteTo.File(
|
|
path: "logs/webapp-.txt",
|
|
rollingInterval: RollingInterval.Day,
|
|
retainedFileCountLimit: 30,
|
|
outputTemplate: fileOutputTemplate);
|
|
var fileLogger = fileConfig.CreateLogger();
|
|
|
|
configuration
|
|
.ReadFrom.Configuration(context.Configuration)
|
|
.Enrich.FromLogContext()
|
|
.WriteTo.Sink(new AntiforgeryLogEventSink(consoleLogger))
|
|
.WriteTo.Sink(new AntiforgeryLogEventSink(fileLogger));
|
|
});
|
|
|
|
// Configure authentication secrets for production (Docker, etc.)
|
|
if (builder.Environment.IsProduction())
|
|
{
|
|
// Option 1: Load from volume-mounted secrets file in Data directory
|
|
var secretsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "auth-secrets.json");
|
|
if (File.Exists(secretsPath))
|
|
{
|
|
builder.Configuration.AddJsonFile(secretsPath, optional: false, reloadOnChange: true);
|
|
}
|
|
|
|
// Option 2: Environment variables with prefix
|
|
builder.Configuration.AddEnvironmentVariables(prefix: "TSA_");
|
|
}
|
|
|
|
// Add services to the container.
|
|
builder.Services.AddControllersWithViews();
|
|
builder.Services.AddRazorComponents()
|
|
.AddInteractiveServerComponents();
|
|
|
|
builder.Services.AddMudServices();
|
|
|
|
// Configure Data Protection to persist keys across container restarts
|
|
var keysPath = Path.Combine(builder.Environment.ContentRootPath, "DataProtectionKeys");
|
|
|
|
// Try to create the directory if it doesn't exist
|
|
try
|
|
{
|
|
if (!Directory.Exists(keysPath))
|
|
{
|
|
Directory.CreateDirectory(keysPath);
|
|
Console.WriteLine($"Created DataProtectionKeys directory at: {keysPath}");
|
|
}
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
Console.WriteLine($"WARNING: Unable to create DataProtectionKeys directory at {keysPath}");
|
|
Console.WriteLine($"Error: {ex.Message}");
|
|
Console.WriteLine("This may cause issues with antiforgery tokens and authentication cookies.");
|
|
Console.WriteLine("Ensure the volume mount has correct permissions for the container user.");
|
|
}
|
|
|
|
builder.Services.AddDataProtection()
|
|
.PersistKeysToFileSystem(new DirectoryInfo(keysPath))
|
|
.SetApplicationName("TSA-Chapter-Organizer");
|
|
|
|
// Ensure Data directory exists
|
|
var dataPath = Path.Combine(builder.Environment.ContentRootPath, "Data");
|
|
try
|
|
{
|
|
if (!Directory.Exists(dataPath))
|
|
{
|
|
Directory.CreateDirectory(dataPath);
|
|
Console.WriteLine($"Created Data directory at: {dataPath}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"WARNING: Unable to create Data directory at {dataPath}: {ex.Message}");
|
|
}
|
|
|
|
// Configure SQLite with hardcoded connection string
|
|
var connectionString = "Data Source=Data/app.db";
|
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
|
{
|
|
options.UseSqlite(connectionString,
|
|
sqliteOptions => sqliteOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
|
|
});
|
|
|
|
builder.Services.AddQuickGridEntityFrameworkAdapter();
|
|
|
|
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
|
|
|
builder.Services.AddScoped<ClipboardService>();
|
|
builder.Services.AddScoped<WebApp.LocalStorageService>();
|
|
builder.Services.AddScoped<WebApp.Services.IEventOccurrenceService, WebApp.Services.EventOccurrenceService>();
|
|
builder.Services.AddScoped<Core.Services.IEventOccurrenceParserService, Core.Services.EventOccurrenceParserService>();
|
|
builder.Services.AddScoped<WebApp.Services.FormValidationService>();
|
|
builder.Services.AddScoped<WebApp.Services.EventDefinitionService>();
|
|
|
|
// State container for maintaining state per user connection (Blazor Server)
|
|
builder.Services.AddScoped<StateContainer>();
|
|
|
|
// Load validation configuration from appsettings (or use default if not found)
|
|
var validationConfig = builder.Configuration
|
|
.GetSection("ValidationSettings")
|
|
.Get<Core.Validation.ValidationConfiguration>() ?? Core.Validation.ValidationConfiguration.Default;
|
|
|
|
// Validation service with loaded configuration
|
|
builder.Services.AddScoped<Core.Validation.ValidationService>(sp =>
|
|
new Core.Validation.ValidationService(validationConfig));
|
|
|
|
// Add authentication services
|
|
builder.Services.AddHttpContextAccessor();
|
|
builder.Services.AddScoped<AuthenticationService>();
|
|
builder.Services.AddSingleton<LoginRateLimitService>();
|
|
builder.Services.AddHostedService<LoginRateLimitService>(sp => sp.GetRequiredService<LoginRateLimitService>());
|
|
|
|
// Add authentication options
|
|
builder.Services.AddAuthentication("Auth")
|
|
.AddCookie("Auth", options =>
|
|
{
|
|
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
|
|
options.SlidingExpiration = true;
|
|
options.LoginPath = "/login";
|
|
|
|
// Enhanced security settings
|
|
options.Cookie.HttpOnly = true;
|
|
options.Cookie.SecurePolicy = builder.Environment.IsDevelopment()
|
|
? CookieSecurePolicy.SameAsRequest
|
|
: CookieSecurePolicy.Always;
|
|
options.Cookie.SameSite = SameSiteMode.Strict;
|
|
options.Cookie.Name = "TSA.Auth";
|
|
});
|
|
builder.Services.AddCascadingAuthenticationState();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Check and apply database migrations
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var services = scope.ServiceProvider;
|
|
var logger = services.GetRequiredService<ILogger<Program>>();
|
|
|
|
try
|
|
{
|
|
var context = services.GetRequiredService<AppDbContext>();
|
|
|
|
// Check for pending migrations
|
|
var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
|
|
|
|
if (pendingMigrations.Any())
|
|
{
|
|
logger.LogInformation(
|
|
"Applying database migrations: {Migrations}",
|
|
string.Join(", ", pendingMigrations));
|
|
await context.Database.MigrateAsync();
|
|
logger.LogInformation("Database migrations applied successfully");
|
|
}
|
|
else
|
|
{
|
|
logger.LogInformation("Database is up to date");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "An error occurred while checking database migrations");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Configure the HTTP request pipeline.
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
|
app.UseHsts();
|
|
app.UseMigrationsEndPoint();
|
|
}
|
|
else
|
|
{
|
|
// Only redirect to HTTPS in development (production should use reverse proxy)
|
|
app.UseHttpsRedirection();
|
|
}
|
|
app.UseRouting();
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.UseStaticFiles();
|
|
app.UseAntiforgery();
|
|
|
|
app.MapRazorComponents<App>()
|
|
.AddInteractiveServerRenderMode();
|
|
|
|
// Used for AuthController
|
|
app.MapControllerRoute("default", "{controller}/{action}");
|
|
|
|
// Development-only password hash generator endpoint
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.MapGet("/dev/hash-password", (string password) =>
|
|
{
|
|
var hash = PasswordHashGenerator.GenerateHash(password);
|
|
return Results.Ok(new
|
|
{
|
|
password,
|
|
hash,
|
|
message = "Copy the hash value to your User Secrets configuration"
|
|
});
|
|
}).WithName("GeneratePasswordHash");
|
|
}
|
|
|
|
app.Run();
|