b2984fcf1a
Switch user admin handling from an AppUser boolean to ASP.NET Identity roles. Removed AppUser.IsAdmin and related configuration/model entries; added migration ReplaceIsAdminWithRoles to copy Users.IsAdmin=true into a persistent admin role and drop the IsAdmin column. CurrentUserResponse now exposes roles (string[]), AuthController returns ordered roles from UserManager, and IdentitySeedService now ensures the admin role exists and assigns/creates an initial admin user in that role. Program.cs registers an Admin-only policy (PolicyNames/RoleNames), adjusts cookie auth events to return 401/403 for API requests, and wires up authorization. Frontend updated to use roles: authSession normalizes roles, adds hasRole and ROLE_ADMIN, router and layout support meta.requiredRoles, and new Forbidden and AdminUsers pages/route are added. codexInfo.md updated to reflect the migration to role-based auth.
155 lines
4.6 KiB
C#
155 lines
4.6 KiB
C#
using API.Database;
|
|
using API.Models;
|
|
using API.Security;
|
|
using API.Services;
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.AspNetCore.HttpLogging;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
builder.Configuration.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true);
|
|
builder.Logging.ClearProviders();
|
|
builder.Logging.AddSimpleConsole(options =>
|
|
{
|
|
options.SingleLine = true;
|
|
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
|
|
});
|
|
builder.Logging.AddDebug();
|
|
|
|
var connectionString = builder.Configuration.GetConnectionString("Postgres")
|
|
?? throw new InvalidOperationException("Connection string 'Postgres' wurde nicht gefunden.");
|
|
|
|
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
|
{
|
|
options.UseNpgsql(connectionString);
|
|
});
|
|
|
|
builder.Services.AddControllers();
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
builder.Services.AddHttpLogging(options =>
|
|
{
|
|
options.LoggingFields = HttpLoggingFields.RequestMethod
|
|
| HttpLoggingFields.RequestPath
|
|
| HttpLoggingFields.ResponseStatusCode
|
|
| HttpLoggingFields.Duration;
|
|
});
|
|
|
|
builder.Services
|
|
.AddIdentity<AppUser, IdentityRole<Guid>>(options =>
|
|
{
|
|
options.Password.RequiredLength = 8;
|
|
options.Password.RequireDigit = false;
|
|
options.Password.RequireUppercase = false;
|
|
options.Password.RequireLowercase = false;
|
|
options.Password.RequireNonAlphanumeric = false;
|
|
|
|
options.User.RequireUniqueEmail = false;
|
|
|
|
options.SignIn.RequireConfirmedAccount = false;
|
|
options.SignIn.RequireConfirmedEmail = false;
|
|
})
|
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
|
.AddDefaultTokenProviders();
|
|
builder.Services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy(PolicyNames.AdminOnly, policy =>
|
|
{
|
|
policy.RequireRole(RoleNames.Admin);
|
|
});
|
|
});
|
|
|
|
builder.Services.ConfigureApplicationCookie(options =>
|
|
{
|
|
options.Cookie.Name = "hoard.auth";
|
|
options.LoginPath = "/auth/login";
|
|
options.LogoutPath = "/auth/logout";
|
|
options.AccessDeniedPath = "/auth/forbidden";
|
|
options.SlidingExpiration = true;
|
|
options.Cookie.HttpOnly = true;
|
|
options.Events = new CookieAuthenticationEvents
|
|
{
|
|
OnRedirectToLogin = context =>
|
|
{
|
|
if (IsApiRequest(context.Request))
|
|
{
|
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
context.Response.Redirect(context.RedirectUri);
|
|
return Task.CompletedTask;
|
|
},
|
|
OnRedirectToAccessDenied = context =>
|
|
{
|
|
if (IsApiRequest(context.Request))
|
|
{
|
|
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
context.Response.Redirect(context.RedirectUri);
|
|
return Task.CompletedTask;
|
|
}
|
|
};
|
|
});
|
|
|
|
builder.Services.AddScoped<IdentitySeedService>();
|
|
|
|
var app = builder.Build();
|
|
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var startupLogger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
startupLogger.LogInformation("Starte Datenbankmigrationen.");
|
|
dbContext.Database.Migrate();
|
|
var seedService = scope.ServiceProvider.GetRequiredService<IdentitySeedService>();
|
|
await seedService.SeedAsync();
|
|
startupLogger.LogInformation("Backend-Initialisierung abgeschlossen.");
|
|
}
|
|
|
|
var webRootPath = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot");
|
|
var indexFilePath = Path.Combine(webRootPath, "index.html");
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
|
|
app.UseDefaultFiles();
|
|
app.UseStaticFiles();
|
|
app.UseHttpLogging();
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.MapControllers();
|
|
app.MapFallback(async context =>
|
|
{
|
|
if (context.Request.Path.StartsWithSegments("/api"))
|
|
{
|
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
return;
|
|
}
|
|
|
|
if (!File.Exists(indexFilePath))
|
|
{
|
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
return;
|
|
}
|
|
|
|
context.Response.ContentType = "text/html; charset=utf-8";
|
|
await context.Response.SendFileAsync(indexFilePath);
|
|
});
|
|
|
|
app.Run();
|
|
|
|
static bool IsApiRequest(HttpRequest request)
|
|
{
|
|
return request.Path.StartsWithSegments("/api")
|
|
|| request.Path.StartsWithSegments("/auth");
|
|
}
|