diff --git a/Webshop.Api/Dockerfile b/Webshop.Api/Dockerfile index 774bce7..7874818 100644 --- a/Webshop.Api/Dockerfile +++ b/Webshop.Api/Dockerfile @@ -1,36 +1,35 @@ # Phase 1: Die Build-Phase -# Wir verwenden das offizielle .NET 8 SDK-Image, das alle Werkzeuge zum Bauen enthält. +# Verwendet das .NET 8 SDK-Image, das alle Werkzeuge zum Bauen enthält. FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src -# Kopiere zuerst die Projekt- und Solution-Dateien. -# Docker ist schlau: Wenn sich diese Dateien nicht ändern, verwendet es den Cache und der 'restore'-Schritt wird übersprungen. +# Kopiere zuerst die Projekt- und Solution-Dateien, um den Docker-Cache optimal zu nutzen. COPY ["ShopSolution.sln", "."] COPY ["Webshop.Api/Webshop.Api.csproj", "Webshop.Api/"] COPY ["Webshop.Application/Webshop.Application.csproj", "Webshop.Application/"] COPY ["Webshop.Domain/Webshop.Domain.csproj", "Webshop.Domain/"] COPY ["Webshop.Infrastructure/Webshop.Infrastructure.csproj", "Webshop.Infrastructure/"] -# Lade alle NuGet-Abhängigkeiten für die gesamte Solution herunter. +# Lade alle NuGet-Abhängigkeiten herunter. RUN dotnet restore "ShopSolution.sln" # Kopiere den gesamten restlichen Quellcode. COPY . . -# Wechsle in das Verzeichnis des API-Projekts. +# Publiziert die Anwendung in einer Release-Konfiguration. +# `dotnet publish` kopiert automatisch alle notwendigen Dateien, einschließlich `wwwroot`. WORKDIR "/src/Webshop.Api" -# Baue und publiziere die Anwendung in einer Release-Konfiguration. -# Das Ergebnis wird in den Ordner /app/publish verschoben. RUN dotnet publish "Webshop.Api.csproj" -c Release -o /app/publish --no-restore # --- # Phase 2: Die finale Runtime-Phase -# Wir verwenden das deutlich kleinere ASP.NET Core Runtime-Image, da wir den Code nicht mehr kompilieren müssen. +# Verwendet das kleinere ASP.NET Core Runtime-Image für eine geringere Größe. FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final WORKDIR /app # Kopiere nur die publizierte Anwendung aus der Build-Phase. +# Der `publish`-Ordner enthält die DLLs, Abhängigkeiten und den `wwwroot`-Ordner. COPY --from=build /app/publish . # Definiere den Befehl, der ausgeführt wird, wenn der Container startet. diff --git a/Webshop.Api/Program.cs b/Webshop.Api/Program.cs index 62fd40e..92ace8f 100644 --- a/Webshop.Api/Program.cs +++ b/Webshop.Api/Program.cs @@ -29,15 +29,17 @@ var builder = WebApplication.CreateBuilder(args); // --- 1. Dependency Injection Konfiguration --- -// Datenbank-Kontext +// Datenbank-Kontext mit Split-Query-Verhalten für Performance-Optimierung builder.Services.AddDbContext(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")) + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"), + o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)) ); // ASP.NET Core Identity mit ApplicationUser builder.Services.AddIdentity(options => { - options.SignIn.RequireConfirmedEmail = true; + // In Produktion auf 'true' setzen, für Entwicklung ist 'false' einfacher. + options.SignIn.RequireConfirmedEmail = false; }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); @@ -75,7 +77,15 @@ builder.Services.AddAuthentication(options => }); builder.Services.AddAuthorization(); +// Infrastruktur & Externe Dienste builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); +builder.Services.AddHttpClient(); +builder.Services.Configure(options => +{ + options.ApiToken = builder.Configuration["Resend:ApiToken"]!; +}); +builder.Services.AddTransient(); // Repositories builder.Services.AddScoped(); @@ -109,16 +119,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); - - -// Externe Dienste (Resend) -builder.Services.AddHttpClient(); -builder.Services.Configure(options => -{ - options.ApiToken = builder.Configuration["Resend:ApiToken"]!; -}); -builder.Services.AddTransient(); // Controller und API-Infrastruktur builder.Services.AddControllers() @@ -131,54 +131,29 @@ builder.Services.AddEndpointsApiExplorer(); // Swagger / OpenAPI Konfiguration builder.Services.AddSwaggerGen(c => { - // --- 1. Gruppierung der Endpunkte (visuelle Tags) --- c.TagActionsBy(api => { var controllerName = api.ActionDescriptor.RouteValues["controller"]; if (controllerName == null) return new[] { "Default" }; - - // Bereinige den Controller-Namen für einen sauberen Tag var tag = controllerName.Replace("Admin", "").Replace("Controller", ""); - - if (api.RelativePath.StartsWith("api/v1/auth")) - { - return new[] { "Auth" }; - } - if (api.RelativePath.StartsWith("api/v1/admin")) - { - return new[] { $"Admin - {tag}" }; - } - if (api.RelativePath.StartsWith("api/v1/customer")) - { - return new[] { $"Customer - {tag}" }; - } - if (api.RelativePath.StartsWith("api/v1/public")) - { - return new[] { $"Public - {tag}" }; - } - + if (api.RelativePath.StartsWith("api/v1/auth")) return new[] { "Auth" }; + if (api.RelativePath.StartsWith("api/v1/admin")) return new[] { $"Admin - {tag}" }; + if (api.RelativePath.StartsWith("api/v1/customer")) return new[] { $"Customer - {tag}" }; + if (api.RelativePath.StartsWith("api/v1/public")) return new[] { $"Public - {tag}" }; return new[] { tag }; }); - // --- NEU: Sortierung der Gruppen in der UI --- c.OrderActionsBy(apiDesc => { var relativePath = apiDesc.RelativePath ?? ""; - - // Sortierung - if (relativePath.StartsWith("api/v1/auth")) return "1"; - if (relativePath.StartsWith("api/v1/admin")) return "2"; + if (relativePath.StartsWith("api/v1/auth")) return "1"; + if (relativePath.StartsWith("api/v1/public")) return "2"; if (relativePath.StartsWith("api/v1/customer")) return "3"; - if (relativePath.StartsWith("api/v1/public")) return "4"; - - - - return "5"; // Fallback + if (relativePath.StartsWith("api/v1/admin")) return "4"; + return "5"; }); - - // --- 2. JWT Security Konfiguration --- c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT Authorization header. Example: \"Authorization: Bearer {token}\"", @@ -190,38 +165,24 @@ builder.Services.AddSwaggerGen(c => c.AddSecurityRequirement(new OpenApiSecurityRequirement { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - new string[] {} - } + { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] {} } }); c.SchemaFilter(); c.OperationFilter(); - - // Endpunktspezifische Beispiele c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); - c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); }); +var app = builder.Build(); // --- 2. HTTP Request Pipeline Konfiguration --- -var app = builder.Build(); - // Führe Datenbank-Migrationen und Initialisierungslogik beim Start aus using (var scope = app.Services.CreateScope()) { @@ -231,7 +192,6 @@ using (var scope = app.Services.CreateScope()) var context = services.GetRequiredService(); context.Database.Migrate(); - // TEMPORÄRER INITIALER ADMIN- UND KUNDEN-SETUP // HINWEIS: Dieser Block sollte in der Produktion entfernt oder deaktiviert werden. var roleManager = services.GetRequiredService>(); var userManager = services.GetRequiredService>(); @@ -248,58 +208,58 @@ using (var scope = app.Services.CreateScope()) if (adminUser == null) { adminUser = new ApplicationUser { UserName = "admin@yourwebshop.com", Email = "admin@yourwebshop.com", EmailConfirmed = true, CreatedDate = DateTimeOffset.UtcNow, LastActive = DateTimeOffset.UtcNow }; - var createAdmin = await userManager.CreateAsync(adminUser, "SecureAdminPass123!"); - if (createAdmin.Succeeded) + var createAdminResult = await userManager.CreateAsync(adminUser, "SecureAdminPass123!"); + if (createAdminResult.Succeeded) { await userManager.AddToRoleAsync(adminUser, "Admin"); - Console.WriteLine("Admin user created."); + Console.WriteLine("INFO: Admin user created successfully."); } - else { Console.WriteLine($"Error creating admin user: {string.Join(", ", createAdmin.Errors.Select(e => e.Description))}"); } + else { Console.WriteLine($"ERROR: Could not create admin user. Errors: {string.Join(", ", createAdminResult.Errors.Select(e => e.Description))}"); } } var customerUser = await userManager.FindByEmailAsync("customer@yourwebshop.com"); if (customerUser == null) { customerUser = new ApplicationUser { UserName = "customer@yourwebshop.com", Email = "customer@yourwebshop.com", EmailConfirmed = true, CreatedDate = DateTimeOffset.UtcNow, LastActive = DateTimeOffset.UtcNow }; - var createCustomer = await userManager.CreateAsync(customerUser, "SecureCustomerPass123!"); - if (createCustomer.Succeeded) + var createCustomerResult = await userManager.CreateAsync(customerUser, "SecureCustomerPass12-3!"); + if (createCustomerResult.Succeeded) { await userManager.AddToRoleAsync(customerUser, "Customer"); - Console.WriteLine("Customer user created."); + Console.WriteLine("INFO: Customer user created successfully."); if (!await context.Customers.AnyAsync(c => c.AspNetUserId == customerUser.Id)) { var customerProfile = new Customer { AspNetUserId = customerUser.Id, FirstName = "Test", LastName = "Kunde" }; context.Customers.Add(customerProfile); await context.SaveChangesAsync(); - Console.WriteLine("Customer profile created for new customer user."); + Console.WriteLine("INFO: Customer profile created for new customer user."); } } - else { Console.WriteLine($"Error creating customer user: {string.Join(", ", createCustomer.Errors.Select(e => e.Description))}"); } + else { Console.WriteLine($"ERROR: Could not create customer user. Errors: {string.Join(", ", createCustomerResult.Errors.Select(e => e.Description))}"); } } } catch (Exception ex) { var logger = services.GetRequiredService>(); - logger.LogError(ex, "An error occurred during database migration or user initialization."); + logger.LogError(ex, "FATAL: An error occurred during database migration or user initialization."); } } // Middleware für Reverse-Proxies -var forwardedHeadersOptions = new ForwardedHeadersOptions +app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto -}; +}); -// << Statische Dateien aus wwwroot bereitstellen (z.B. /uploads/xyz.jpg) >> +// Statische Dateien aus wwwroot bereitstellen (z.B. /uploads/xyz.jpg) app.UseStaticFiles(); // Swagger/SwaggerUI für API-Dokumentation aktivieren -//if (app.Environment.IsDevelopment()) -//{ -app.UseSwagger(); +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); app.UseSwaggerUI(); -//} +} // app.UseHttpsRedirection(); // Auskommentiert für Docker HTTP-Entwicklung