Update settings for production deployment
This commit is contained in:
@@ -30,3 +30,4 @@ auth-secrets.json
|
|||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
/WebApp/logs/*
|
/WebApp/logs/*
|
||||||
|
/WebApp/DataProtectionKeys/*
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
@ChildContent
|
@ChildContent
|
||||||
</ChildContent>
|
</ChildContent>
|
||||||
<ErrorContent Context="ex">
|
<ErrorContent Context="ex">
|
||||||
|
@{
|
||||||
|
Logger.LogError(ex, "Error boundary triggered - Exception details: {Message}", ex.Message);
|
||||||
|
}
|
||||||
<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-8">
|
<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-8">
|
||||||
<MudPaper Elevation="3" Class="pa-6">
|
<MudPaper Elevation="3" Class="pa-6">
|
||||||
<MudAlert Severity="Severity.Error" Variant="Variant.Filled">
|
<MudAlert Severity="Severity.Error" Variant="Variant.Filled">
|
||||||
@@ -47,9 +50,4 @@
|
|||||||
[Inject] private ILogger<AppErrorBoundary> Logger { get; set; } = default!;
|
[Inject] private ILogger<AppErrorBoundary> Logger { get; set; } = default!;
|
||||||
|
|
||||||
private bool ShowDetails => Environment.IsDevelopment();
|
private bool ShowDetails => Environment.IsDevelopment();
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
|
||||||
{
|
|
||||||
Logger.LogError("Error boundary triggered");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-20
@@ -1,13 +1,59 @@
|
|||||||
using Data;
|
using Data;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using System.Text.Json;
|
||||||
using WebApp;
|
using WebApp;
|
||||||
using WebApp.Authentication;
|
using WebApp.Authentication;
|
||||||
using WebApp.Components;
|
using WebApp.Components;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Load optional appsettings from Data directory (overrides defaults)
|
||||||
|
var dataAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "appsettings.json");
|
||||||
|
if (!File.Exists(dataAppSettingsPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"appsettings.json not found at {dataAppSettingsPath}. Creating with template values...");
|
||||||
|
|
||||||
|
var templateSettings = new
|
||||||
|
{
|
||||||
|
ChapterSettings = new
|
||||||
|
{
|
||||||
|
Name = "Your Chapter Name",
|
||||||
|
ShortName = "YCN",
|
||||||
|
NationalId = "0000",
|
||||||
|
StateId = "00000",
|
||||||
|
RegionalId = "00000",
|
||||||
|
CompetitionYear = "2025"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(templateSettings, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure Data directory exists
|
||||||
|
Directory.CreateDirectory(Path.Combine(builder.Environment.ContentRootPath, "Data"));
|
||||||
|
File.WriteAllText(dataAppSettingsPath, json);
|
||||||
|
Console.WriteLine("appsettings.json created in Data directory. Please update ChapterSettings with your chapter information.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"WARNING: Unable to create appsettings.json: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Data/appsettings.json as additional configuration source
|
||||||
|
if (File.Exists(dataAppSettingsPath))
|
||||||
|
{
|
||||||
|
builder.Configuration.AddJsonFile(dataAppSettingsPath, optional: true, reloadOnChange: true);
|
||||||
|
Console.WriteLine($"Loaded configuration from {dataAppSettingsPath}");
|
||||||
|
}
|
||||||
|
|
||||||
// Configure Serilog
|
// Configure Serilog
|
||||||
builder.Host.UseSerilog((context, configuration) =>
|
builder.Host.UseSerilog((context, configuration) =>
|
||||||
configuration
|
configuration
|
||||||
@@ -24,8 +70,8 @@ builder.Host.UseSerilog((context, configuration) =>
|
|||||||
// Configure authentication secrets for production (Docker, etc.)
|
// Configure authentication secrets for production (Docker, etc.)
|
||||||
if (builder.Environment.IsProduction())
|
if (builder.Environment.IsProduction())
|
||||||
{
|
{
|
||||||
// Option 1: Load from volume-mounted secrets file
|
// Option 1: Load from volume-mounted secrets file in Data directory
|
||||||
var secretsPath = "/app/secrets/auth-secrets.json";
|
var secretsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "auth-secrets.json");
|
||||||
if (File.Exists(secretsPath))
|
if (File.Exists(secretsPath))
|
||||||
{
|
{
|
||||||
builder.Configuration.AddJsonFile(secretsPath, optional: false, reloadOnChange: true);
|
builder.Configuration.AddJsonFile(secretsPath, optional: false, reloadOnChange: true);
|
||||||
@@ -42,9 +88,52 @@ builder.Services.AddRazorComponents()
|
|||||||
|
|
||||||
builder.Services.AddMudServices();
|
builder.Services.AddMudServices();
|
||||||
|
|
||||||
// Configure SQLite
|
// Configure Data Protection to persist keys across container restarts
|
||||||
var connectionString = builder.Configuration.GetConnectionString("SQLiteDefault");
|
var keysPath = Path.Combine(builder.Environment.ContentRootPath, "DataProtectionKeys");
|
||||||
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite(connectionString));
|
|
||||||
|
// 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.AddQuickGridEntityFrameworkAdapter();
|
||||||
|
|
||||||
@@ -71,7 +160,9 @@ builder.Services.AddAuthentication("Auth")
|
|||||||
|
|
||||||
// Enhanced security settings
|
// Enhanced security settings
|
||||||
options.Cookie.HttpOnly = true;
|
options.Cookie.HttpOnly = true;
|
||||||
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
options.Cookie.SecurePolicy = builder.Environment.IsDevelopment()
|
||||||
|
? CookieSecurePolicy.SameAsRequest
|
||||||
|
: CookieSecurePolicy.Always;
|
||||||
options.Cookie.SameSite = SameSiteMode.Strict;
|
options.Cookie.SameSite = SameSiteMode.Strict;
|
||||||
options.Cookie.Name = "TSA.Auth";
|
options.Cookie.Name = "TSA.Auth";
|
||||||
});
|
});
|
||||||
@@ -89,25 +180,19 @@ using (var scope = app.Services.CreateScope())
|
|||||||
{
|
{
|
||||||
var context = services.GetRequiredService<AppDbContext>();
|
var context = services.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
// Check for pending migrations
|
||||||
|
var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
|
||||||
|
|
||||||
|
if (pendingMigrations.Any())
|
||||||
{
|
{
|
||||||
// Auto-apply migrations in development
|
logger.LogInformation(
|
||||||
logger.LogInformation("Applying database migrations (Development)...");
|
"Applying database migrations: {Migrations}",
|
||||||
|
string.Join(", ", pendingMigrations));
|
||||||
await context.Database.MigrateAsync();
|
await context.Database.MigrateAsync();
|
||||||
logger.LogInformation("Database migrations applied successfully");
|
logger.LogInformation("Database migrations applied successfully");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Check if migrations are needed in production
|
|
||||||
var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
|
|
||||||
if (pendingMigrations.Any())
|
|
||||||
{
|
|
||||||
logger.LogError(
|
|
||||||
"Database has pending migrations: {Migrations}",
|
|
||||||
string.Join(", ", pendingMigrations));
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Database migrations are pending. Run migrations before starting the application.");
|
|
||||||
}
|
|
||||||
logger.LogInformation("Database is up to date");
|
logger.LogInformation("Database is up to date");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,8 +211,11 @@ if (!app.Environment.IsDevelopment())
|
|||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
app.UseMigrationsEndPoint();
|
app.UseMigrationsEndPoint();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Only redirect to HTTPS in development (production should use reverse proxy)
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
}
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Debug",
|
||||||
@@ -14,5 +8,13 @@
|
|||||||
"System": "Warning"
|
"System": "Warning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ChapterSettings": {
|
||||||
|
"Name": "Robertsville Middle School",
|
||||||
|
"ShortName": "RMS",
|
||||||
|
"NationalId": "2227",
|
||||||
|
"StateId": "12227",
|
||||||
|
"RegionalId": "12227",
|
||||||
|
"CompetitionYear": "2026"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-15
@@ -1,10 +1,4 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
@@ -16,16 +10,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
|
||||||
"SQLiteDefault": "Data Source=data.db"
|
|
||||||
},
|
|
||||||
"Name" : "Test" ,
|
|
||||||
"ChapterSettings": {
|
"ChapterSettings": {
|
||||||
"Name": "Robertsville Middle School",
|
"Name": "Your Chapter Name",
|
||||||
"ShortName": "RMS",
|
"ShortName": "YCN",
|
||||||
"NationalId": "2227",
|
"NationalId": "0000",
|
||||||
"StateId": "12227",
|
"StateId": "00000",
|
||||||
"RegionalId": "12227",
|
"RegionalId": "00000",
|
||||||
"CompetitionYear": "2026"
|
"CompetitionYear": "2026"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user