Initial Blazor birth registry app, theming, and services
Build and Push Docker Image / build-and-push (push) Failing after 10s

Implemented a Blazor Web App (.NET 8) for a public-by-link birth registry platform, following project guidelines. Added domain entities, EF Core context, and Blazor components for authentication, registry management, and public views. Introduced core services for registries, theming, user context, platform owner bootstrapping, and SMTP email. Included static assets (Bootstrap, favicon), launch settings, Dockerfile, CI workflow, and deployment configs. Added bootstrap.min.css.map for improved CSS debugging.
This commit is contained in:
Arne Moerman
2026-05-14 11:48:15 +02:00
commit 0878405e9d
107 changed files with 6503 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
using BirthList.Infrastructure.Persistence;
using BirthList.Web.Authorization;
using BirthList.Web.Configuration;
using BirthList.Web.Features.Registries;
using BirthList.Web.Services;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using BirthList.Web.Components;
using BirthList.Web.Components.Account;
using BirthList.Web.Data;
using System.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddHttpClient("RegistryMetadata", client =>
{
client.Timeout = TimeSpan.FromSeconds(10);
});
builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"));
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
builder.Services.AddScoped<RegistryAuthorizationService>();
builder.Services.AddScoped<OwnerBootstrapService>();
builder.Services.AddScoped<RegistryService>();
builder.Services.AddScoped<RegistryMetadataService>();
builder.Services.AddScoped<RegistryThemeService>();
builder.Services.AddScoped<RegistryUserContext>();
builder.Services.AddScoped<SmtpConfigurationStatusService>();
var googleClientId = builder.Configuration["Authentication:Google:ClientId"];
var googleClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
var microsoftClientId = builder.Configuration["Authentication:Microsoft:ClientId"];
var microsoftClientSecret = builder.Configuration["Authentication:Microsoft:ClientSecret"];
var authBuilder = builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
authBuilder.AddIdentityCookies();
if (!string.IsNullOrWhiteSpace(googleClientId) && !string.IsNullOrWhiteSpace(googleClientSecret))
{
authBuilder.AddGoogle(options =>
{
options.ClientId = googleClientId;
options.ClientSecret = googleClientSecret;
});
}
if (!string.IsNullOrWhiteSpace(microsoftClientId) && !string.IsNullOrWhiteSpace(microsoftClientSecret))
{
authBuilder.AddMicrosoftAccount(options =>
{
options.ClientId = microsoftClientId;
options.ClientSecret = microsoftClientSecret;
});
}
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
var dataProvider = builder.Configuration["Data:Provider"] ?? "SqlServer";
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
if (string.Equals(dataProvider, "SqlServer", StringComparison.OrdinalIgnoreCase))
{
options.UseSqlServer(connectionString);
return;
}
options.UseSqlite(connectionString);
});
builder.Services.AddDbContext<RegistryDbContext>(options =>
{
if (string.Equals(dataProvider, "SqlServer", StringComparison.OrdinalIgnoreCase))
{
options.UseSqlServer(connectionString);
return;
}
options.UseSqlite(connectionString);
});
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
builder.Services.AddScoped<SmtpEmailSender>();
builder.Services.AddScoped<IEmailSender<ApplicationUser>>(serviceProvider => serviceProvider.GetRequiredService<SmtpEmailSender>());
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var applicationDbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await applicationDbContext.Database.MigrateAsync();
var registryDbContext = scope.ServiceProvider.GetRequiredService<RegistryDbContext>();
var registryMigrations = registryDbContext.Database.GetMigrations();
if (registryMigrations.Any())
{
await registryDbContext.Database.MigrateAsync();
}
else
{
var databaseCreator = registryDbContext.Database.GetService<IRelationalDatabaseCreator>();
if (!await databaseCreator.ExistsAsync())
{
await registryDbContext.Database.EnsureCreatedAsync();
}
else
{
var platformOwnersTableExists = await TableExistsAsync(registryDbContext, "PlatformOwners");
if (!platformOwnersTableExists)
{
await databaseCreator.CreateTablesAsync();
}
}
}
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
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.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints();
app.Run();
static async Task<bool> TableExistsAsync(DbContext context, string tableName)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentException.ThrowIfNullOrWhiteSpace(tableName);
var connection = context.Database.GetDbConnection();
var closeConnection = connection.State != ConnectionState.Open;
if (closeConnection)
{
await connection.OpenAsync();
}
try
{
await using var command = connection.CreateCommand();
if (context.Database.IsSqlServer())
{
command.CommandText = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @tableName";
}
else if (context.Database.IsSqlite())
{
command.CommandText = "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = @tableName";
}
else
{
throw new NotSupportedException("Unsupported database provider for table existence check.");
}
var parameter = command.CreateParameter();
parameter.ParameterName = "@tableName";
parameter.Value = tableName;
command.Parameters.Add(parameter);
var result = await command.ExecuteScalarAsync();
return result is not null && result != DBNull.Value;
}
finally
{
if (closeConnection)
{
await connection.CloseAsync();
}
}
}