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(options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=budgetapp.db")); // Identity builder.Services.AddIdentity(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() .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(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); 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(); 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() .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 signInManager, UserManager 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();