Files
Budgeteer/BudgetApp/Program.cs
T
2026-03-02 11:50:08 +00:00

179 lines
6.4 KiB
C#

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using BudgetApp.Components;
using BudgetApp.Data;
using BudgetApp.Services;
var builder = WebApplication.CreateBuilder(args);
// Database
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")
?? "Data Source=budgetapp.db"));
// Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 8;
options.Password.RequiredUniqueChars = 4;
options.SignIn.RequireConfirmedAccount = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
});
// External authentication providers (only registered when configured)
var authBuilder = builder.Services.AddAuthentication();
var googleSection = builder.Configuration.GetSection("Authentication:Google");
if (!string.IsNullOrEmpty(googleSection["ClientId"]) && !string.IsNullOrEmpty(googleSection["ClientSecret"]))
{
authBuilder.AddGoogle(options =>
{
options.ClientId = googleSection["ClientId"]!;
options.ClientSecret = googleSection["ClientSecret"]!;
});
}
var microsoftSection = builder.Configuration.GetSection("Authentication:Microsoft");
if (!string.IsNullOrEmpty(microsoftSection["ClientId"]) && !string.IsNullOrEmpty(microsoftSection["ClientSecret"]))
{
authBuilder.AddMicrosoftAccount(options =>
{
options.ClientId = microsoftSection["ClientId"]!;
options.ClientSecret = microsoftSection["ClientSecret"]!;
});
}
var githubSection = builder.Configuration.GetSection("Authentication:GitHub");
if (!string.IsNullOrEmpty(githubSection["ClientId"]) && !string.IsNullOrEmpty(githubSection["ClientSecret"]))
{
authBuilder.AddOAuth("GitHub", "GitHub", options =>
{
options.ClientId = githubSection["ClientId"]!;
options.ClientSecret = githubSection["ClientSecret"]!;
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.UserInformationEndpoint = "https://api.github.com/user";
options.CallbackPath = "/signin-github";
options.Scope.Add("user:email");
options.ClaimActions.MapJsonKey(System.Security.Claims.ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(System.Security.Claims.ClaimTypes.Name, "login");
options.ClaimActions.MapJsonKey(System.Security.Claims.ClaimTypes.Email, "email");
});
}
// Application services
builder.Services.AddScoped<AccountService>();
builder.Services.AddScoped<TransactionService>();
builder.Services.AddScoped<CategoryService>();
builder.Services.AddScoped<DashboardService>();
builder.Services.AddScoped<ToastService>();
builder.Services.AddScoped<AppState>();
builder.Services.AddCascadingAuthenticationState();
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
// Auto-migrate database
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
db.Database.Migrate();
}
// 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.UseStatusCodePagesWithReExecute("/not-found");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// External authentication endpoints
app.MapPost("/api/auth/external-login", (HttpContext context) =>
{
var provider = context.Request.Form["provider"].ToString();
var returnUrl = context.Request.Form["returnUrl"].ToString();
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "/";
var redirectUrl = $"/api/auth/external-callback?returnUrl={Uri.EscapeDataString(returnUrl)}";
var properties = new Microsoft.AspNetCore.Authentication.AuthenticationProperties { RedirectUri = redirectUrl };
return Results.Challenge(properties, [provider]);
});
app.MapGet("/api/auth/external-callback", async (
HttpContext context,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
string? returnUrl) =>
{
returnUrl ??= "/";
var info = await signInManager.GetExternalLoginInfoAsync();
if (info is null)
return Results.Redirect("/login");
var signInResult = await signInManager.ExternalLoginSignInAsync(
info.LoginProvider, info.ProviderKey, isPersistent: false);
if (signInResult.Succeeded)
return Results.Redirect(returnUrl);
// Auto-register: create user from external claims
var email = info.Principal.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value;
var userName = email ?? info.Principal.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value
?? $"{info.LoginProvider}_{info.ProviderKey}";
var user = new ApplicationUser { UserName = userName, Email = email };
var createResult = await userManager.CreateAsync(user);
if (createResult.Succeeded)
{
await userManager.AddLoginAsync(user, info);
await signInManager.SignInAsync(user, isPersistent: false);
return Results.Redirect(returnUrl);
}
// If the email is already taken, try linking
if (email is not null)
{
var existingUser = await userManager.FindByEmailAsync(email);
if (existingUser is not null)
{
await userManager.AddLoginAsync(existingUser, info);
await signInManager.SignInAsync(existingUser, isPersistent: false);
return Results.Redirect(returnUrl);
}
}
return Results.Redirect("/login");
});
app.Run();