Files
Budgeteer/BudgetApp/Program.cs
T

187 lines
6.5 KiB
C#

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using BudgetApp.Components;
using BudgetApp.Data;
using BudgetApp.Data.Entities;
using BudgetApp.Data.Repositories;
using BudgetApp.Services;
using BudgetApp.Data.Interfaces;
var builder = WebApplication.CreateBuilder(args);
// Database
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=budgetapp.db",
o => o.MigrationsAssembly("BudgetApp.Data")));
// 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 facebookSection = builder.Configuration.GetSection("Authentication:Facebook");
if (!string.IsNullOrEmpty(facebookSection["ClientId"]) && !string.IsNullOrEmpty(facebookSection["ClientSecret"]))
{
authBuilder.AddFacebook(options =>
{
options.ClientId = facebookSection["ClientId"]!;
options.ClientSecret = facebookSection["ClientSecret"]!;
});
}
// Application services
builder.Services.AddScoped<IAccountRepository, AccountRepository>();
builder.Services.AddScoped<ITransactionRepository, TransactionRepository>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddScoped<ISalaryRepository, SalaryRepository>();
builder.Services.AddScoped<DashboardService>();
builder.Services.AddScoped<ToastService>();
builder.Services.AddScoped<AppState>();
builder.Services.AddCascadingAuthenticationState();
// HSTS
if (bool.Parse(Environment.GetEnvironmentVariable("ENABLE_HSTS") ?? "false"))
{
builder.Services.AddHsts(options =>
{
var hstsSection = builder.Configuration.GetSection("Hsts");
options.MaxAge = TimeSpan.FromDays(hstsSection.GetValue<int>("MaxAgeDays", 365));
options.IncludeSubDomains = hstsSection.GetValue<bool>("IncludeSubDomains", true);
options.Preload = hstsSection.GetValue<bool>("Preload", false);
});
}
// 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();