using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.Cookies; using NoticeBoard.Data; using NoticeBoard.Infrastructure; var builder = WebApplication.CreateBuilder(args); // Configure the shared "now" used for Meow expiry checks. Blank ("" or unset) keeps the // original behaviour (host machine's local time zone, e.g. IIS on a Windows Server set to // AEST). Docker's compose file sets this to "UTC" by default — see docker-compose.yml. Clock.Configure(builder.Configuration["TimeZone"]); // SQLite database. Defaults to the app's own directory (unchanged behaviour for IIS). // Set the "DataDirectory" setting (env var DataDirectory=... in Docker) to store the // database elsewhere — used by the Docker image so the DB lives on a mounted volume. var dataDir = builder.Configuration["DataDirectory"] ?? builder.Environment.ContentRootPath; Directory.CreateDirectory(dataDir); var dbPath = Path.Combine(dataDir, "noticeboard.db"); builder.Services.AddDbContext(options => options.UseSqlite($"Data Source={dbPath}")); builder.Services.AddControllersWithViews(); builder.Services.AddHttpClient(); // Cookie authentication for admin panel builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/account/login"; options.LogoutPath = "/account/logout"; options.ExpireTimeSpan = TimeSpan.FromHours(12); options.SlidingExpiration = true; }); var app = builder.Build(); // Auto-create database on startup using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); // This project doesn't use formal EF Core migrations — EnsureCreated only builds a // brand-new database from the current model. For an existing Slides table (from before // the ExpiresAt column existed), add it here so upgrades don't require a manual step. var connection = db.Database.GetDbConnection(); await connection.OpenAsync(); var hasExpiresAt = false; await using (var checkCmd = connection.CreateCommand()) { checkCmd.CommandText = "PRAGMA table_info(Slides);"; await using var reader = await checkCmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { if (string.Equals(reader["name"]?.ToString(), "ExpiresAt", StringComparison.OrdinalIgnoreCase)) { hasExpiresAt = true; break; } } } if (!hasExpiresAt) { await using var alterCmd = connection.CreateCommand(); alterCmd.CommandText = "ALTER TABLE Slides ADD COLUMN ExpiresAt TEXT NULL;"; await alterCmd.ExecuteNonQueryAsync(); } } if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/error"); app.UseHsts(); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); // Ensure uploads directory exists var uploadsPath = Path.Combine(app.Environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); app.MapControllerRoute( name: "account", pattern: "account/{action=Login}", defaults: new { controller = "Account" }); app.MapControllerRoute( name: "admin", pattern: "admin/{action=Index}/{id?}", defaults: new { controller = "Admin" }); app.MapControllerRoute( name: "slides", pattern: "admin/slides/{action=Index}/{id?}", defaults: new { controller = "Slides" }); app.MapControllerRoute( name: "devices", pattern: "admin/devices/{action=Index}/{id?}", defaults: new { controller = "Devices" }); app.MapControllerRoute( name: "api", pattern: "api/{action}/{id?}", defaults: new { controller = "Api" }); app.MapControllerRoute( name: "display", pattern: "d/{slug}", defaults: new { controller = "Display", action = "Show" }); app.MapControllerRoute( name: "display-root", pattern: "{slug}", defaults: new { controller = "Display", action = "Show" }, constraints: new { slug = new NoticeBoard.Routing.DeviceSlugConstraint() }); app.MapControllerRoute( name: "default", pattern: "", defaults: new { controller = "Admin", action = "Index" }); app.Run();