changes for image

This commit is contained in:
Tizian.Breuch
2025-08-08 16:08:19 +02:00
parent 37dd4f2db1
commit b6f677477c
2 changed files with 47 additions and 88 deletions

View File

@@ -1,36 +1,35 @@
# Phase 1: Die Build-Phase # Phase 1: Die Build-Phase
# Wir verwenden das offizielle .NET 8 SDK-Image, das alle Werkzeuge zum Bauen enth<74>lt. # Verwendet das .NET 8 SDK-Image, das alle Werkzeuge zum Bauen enth<74>lt.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src WORKDIR /src
# Kopiere zuerst die Projekt- und Solution-Dateien. # Kopiere zuerst die Projekt- und Solution-Dateien, um den Docker-Cache optimal zu nutzen.
# Docker ist schlau: Wenn sich diese Dateien nicht <20>ndern, verwendet es den Cache und der 'restore'-Schritt wird <20>bersprungen.
COPY ["ShopSolution.sln", "."] COPY ["ShopSolution.sln", "."]
COPY ["Webshop.Api/Webshop.Api.csproj", "Webshop.Api/"] COPY ["Webshop.Api/Webshop.Api.csproj", "Webshop.Api/"]
COPY ["Webshop.Application/Webshop.Application.csproj", "Webshop.Application/"] COPY ["Webshop.Application/Webshop.Application.csproj", "Webshop.Application/"]
COPY ["Webshop.Domain/Webshop.Domain.csproj", "Webshop.Domain/"] COPY ["Webshop.Domain/Webshop.Domain.csproj", "Webshop.Domain/"]
COPY ["Webshop.Infrastructure/Webshop.Infrastructure.csproj", "Webshop.Infrastructure/"] COPY ["Webshop.Infrastructure/Webshop.Infrastructure.csproj", "Webshop.Infrastructure/"]
# Lade alle NuGet-Abh<62>ngigkeiten f<EFBFBD>r die gesamte Solution herunter. # Lade alle NuGet-Abh<62>ngigkeiten herunter.
RUN dotnet restore "ShopSolution.sln" RUN dotnet restore "ShopSolution.sln"
# Kopiere den gesamten restlichen Quellcode. # Kopiere den gesamten restlichen Quellcode.
COPY . . COPY . .
# Wechsle in das Verzeichnis des API-Projekts. # Publiziert die Anwendung in einer Release-Konfiguration.
# `dotnet publish` kopiert automatisch alle notwendigen Dateien, einschlie<69>lich `wwwroot`.
WORKDIR "/src/Webshop.Api" 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 RUN dotnet publish "Webshop.Api.csproj" -c Release -o /app/publish --no-restore
# --- # ---
# Phase 2: Die finale Runtime-Phase # 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<47><72>e.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app WORKDIR /app
# Kopiere nur die publizierte Anwendung aus der Build-Phase. # Kopiere nur die publizierte Anwendung aus der Build-Phase.
# Der `publish`-Ordner enth<74>lt die DLLs, Abh<62>ngigkeiten und den `wwwroot`-Ordner.
COPY --from=build /app/publish . COPY --from=build /app/publish .
# Definiere den Befehl, der ausgef<65>hrt wird, wenn der Container startet. # Definiere den Befehl, der ausgef<65>hrt wird, wenn der Container startet.

View File

@@ -29,15 +29,17 @@ var builder = WebApplication.CreateBuilder(args);
// --- 1. Dependency Injection Konfiguration --- // --- 1. Dependency Injection Konfiguration ---
// Datenbank-Kontext // Datenbank-Kontext mit Split-Query-Verhalten f<>r Performance-Optimierung
builder.Services.AddDbContext<ApplicationDbContext>(options => builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")) options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"),
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery))
); );
// ASP.NET Core Identity mit ApplicationUser // ASP.NET Core Identity mit ApplicationUser
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options => builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{ {
options.SignIn.RequireConfirmedEmail = true; // In Produktion auf 'true' setzen, f<>r Entwicklung ist 'false' einfacher.
options.SignIn.RequireConfirmedEmail = false;
}) })
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
@@ -75,7 +77,15 @@ builder.Services.AddAuthentication(options =>
}); });
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
// Infrastruktur & Externe Dienste
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IFileStorageService, LocalFileStorageService>();
builder.Services.AddHttpClient<ResendClient>();
builder.Services.Configure<ResendClientOptions>(options =>
{
options.ApiToken = builder.Configuration["Resend:ApiToken"]!;
});
builder.Services.AddTransient<IResend, ResendClient>();
// Repositories // Repositories
builder.Services.AddScoped<IProductRepository, ProductRepository>(); builder.Services.AddScoped<IProductRepository, ProductRepository>();
@@ -109,16 +119,6 @@ builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IAddressService, AddressService>(); builder.Services.AddScoped<IAddressService, AddressService>();
builder.Services.AddScoped<ICheckoutService, CheckoutService>(); builder.Services.AddScoped<ICheckoutService, CheckoutService>();
builder.Services.AddScoped<IReviewService, ReviewService>(); builder.Services.AddScoped<IReviewService, ReviewService>();
builder.Services.AddScoped<IFileStorageService, LocalFileStorageService>();
// Externe Dienste (Resend)
builder.Services.AddHttpClient<ResendClient>();
builder.Services.Configure<ResendClientOptions>(options =>
{
options.ApiToken = builder.Configuration["Resend:ApiToken"]!;
});
builder.Services.AddTransient<IResend, ResendClient>();
// Controller und API-Infrastruktur // Controller und API-Infrastruktur
builder.Services.AddControllers() builder.Services.AddControllers()
@@ -131,54 +131,29 @@ builder.Services.AddEndpointsApiExplorer();
// Swagger / OpenAPI Konfiguration // Swagger / OpenAPI Konfiguration
builder.Services.AddSwaggerGen(c => builder.Services.AddSwaggerGen(c =>
{ {
// --- 1. Gruppierung der Endpunkte (visuelle Tags) ---
c.TagActionsBy(api => c.TagActionsBy(api =>
{ {
var controllerName = api.ActionDescriptor.RouteValues["controller"]; var controllerName = api.ActionDescriptor.RouteValues["controller"];
if (controllerName == null) return new[] { "Default" }; if (controllerName == null) return new[] { "Default" };
// Bereinige den Controller-Namen f<>r einen sauberen Tag
var tag = controllerName.Replace("Admin", "").Replace("Controller", ""); var tag = controllerName.Replace("Admin", "").Replace("Controller", "");
if (api.RelativePath.StartsWith("api/v1/auth")) return new[] { "Auth" };
if (api.RelativePath.StartsWith("api/v1/auth")) if (api.RelativePath.StartsWith("api/v1/admin")) return new[] { $"Admin - {tag}" };
{ if (api.RelativePath.StartsWith("api/v1/customer")) return new[] { $"Customer - {tag}" };
return new[] { "Auth" }; if (api.RelativePath.StartsWith("api/v1/public")) return new[] { $"Public - {tag}" };
}
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 }; return new[] { tag };
}); });
// --- NEU: Sortierung der Gruppen in der UI ---
c.OrderActionsBy(apiDesc => c.OrderActionsBy(apiDesc =>
{ {
var relativePath = apiDesc.RelativePath ?? ""; var relativePath = apiDesc.RelativePath ?? "";
// Sortierung
if (relativePath.StartsWith("api/v1/auth")) return "1"; if (relativePath.StartsWith("api/v1/auth")) return "1";
if (relativePath.StartsWith("api/v1/admin")) return "2"; if (relativePath.StartsWith("api/v1/public")) return "2";
if (relativePath.StartsWith("api/v1/customer")) return "3"; if (relativePath.StartsWith("api/v1/customer")) return "3";
if (relativePath.StartsWith("api/v1/public")) return "4"; if (relativePath.StartsWith("api/v1/admin")) return "4";
return "5";
return "5"; // Fallback
}); });
// --- 2. JWT Security Konfiguration ---
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{ {
Description = "JWT Authorization header. Example: \"Authorization: Bearer {token}\"", Description = "JWT Authorization header. Example: \"Authorization: Bearer {token}\"",
@@ -190,38 +165,24 @@ builder.Services.AddSwaggerGen(c =>
c.AddSecurityRequirement(new OpenApiSecurityRequirement 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<AddExampleSchemaFilter>(); c.SchemaFilter<AddExampleSchemaFilter>();
c.OperationFilter<AuthorizeOperationFilter>(); c.OperationFilter<AuthorizeOperationFilter>();
// Endpunktspezifische Beispiele
c.OperationFilter<LoginExampleOperationFilter>(); c.OperationFilter<LoginExampleOperationFilter>();
c.OperationFilter<PaymentMethodExampleOperationFilter>(); c.OperationFilter<PaymentMethodExampleOperationFilter>();
c.OperationFilter<SupplierExampleOperationFilter>(); c.OperationFilter<SupplierExampleOperationFilter>();
c.OperationFilter<ShippingMethodExampleOperationFilter>(); c.OperationFilter<ShippingMethodExampleOperationFilter>();
c.OperationFilter<AdminCategorieExampleOperationFilter>();
c.OperationFilter<AdminProductExampleOperationFilter>(); c.OperationFilter<AdminProductExampleOperationFilter>();
c.OperationFilter<CustomerAddressExampleOperationFilter>(); c.OperationFilter<CustomerAddressExampleOperationFilter>();
c.OperationFilter<CustomerOrderExampleOperationFilter>(); c.OperationFilter<CustomerOrderExampleOperationFilter>();
}); });
var app = builder.Build();
// --- 2. HTTP Request Pipeline Konfiguration --- // --- 2. HTTP Request Pipeline Konfiguration ---
var app = builder.Build();
// F<>hre Datenbank-Migrationen und Initialisierungslogik beim Start aus // F<>hre Datenbank-Migrationen und Initialisierungslogik beim Start aus
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
@@ -231,7 +192,6 @@ using (var scope = app.Services.CreateScope())
var context = services.GetRequiredService<ApplicationDbContext>(); var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate(); context.Database.Migrate();
// TEMPOR<4F>RER INITIALER ADMIN- UND KUNDEN-SETUP
// HINWEIS: Dieser Block sollte in der Produktion entfernt oder deaktiviert werden. // HINWEIS: Dieser Block sollte in der Produktion entfernt oder deaktiviert werden.
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>(); var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>(); var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
@@ -248,58 +208,58 @@ using (var scope = app.Services.CreateScope())
if (adminUser == null) if (adminUser == null)
{ {
adminUser = new ApplicationUser { UserName = "admin@yourwebshop.com", Email = "admin@yourwebshop.com", EmailConfirmed = true, CreatedDate = DateTimeOffset.UtcNow, LastActive = DateTimeOffset.UtcNow }; 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!"); var createAdminResult = await userManager.CreateAsync(adminUser, "SecureAdminPass123!");
if (createAdmin.Succeeded) if (createAdminResult.Succeeded)
{ {
await userManager.AddToRoleAsync(adminUser, "Admin"); 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"); var customerUser = await userManager.FindByEmailAsync("customer@yourwebshop.com");
if (customerUser == null) if (customerUser == null)
{ {
customerUser = new ApplicationUser { UserName = "customer@yourwebshop.com", Email = "customer@yourwebshop.com", EmailConfirmed = true, CreatedDate = DateTimeOffset.UtcNow, LastActive = DateTimeOffset.UtcNow }; 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!"); var createCustomerResult = await userManager.CreateAsync(customerUser, "SecureCustomerPass12-3!");
if (createCustomer.Succeeded) if (createCustomerResult.Succeeded)
{ {
await userManager.AddToRoleAsync(customerUser, "Customer"); 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)) if (!await context.Customers.AnyAsync(c => c.AspNetUserId == customerUser.Id))
{ {
var customerProfile = new Customer { AspNetUserId = customerUser.Id, FirstName = "Test", LastName = "Kunde" }; var customerProfile = new Customer { AspNetUserId = customerUser.Id, FirstName = "Test", LastName = "Kunde" };
context.Customers.Add(customerProfile); context.Customers.Add(customerProfile);
await context.SaveChangesAsync(); 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) catch (Exception ex)
{ {
var logger = services.GetRequiredService<ILogger<Program>>(); var logger = services.GetRequiredService<ILogger<Program>>();
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 // Middleware f<>r Reverse-Proxies
var forwardedHeadersOptions = new ForwardedHeadersOptions app.UseForwardedHeaders(new ForwardedHeadersOptions
{ {
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto 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(); app.UseStaticFiles();
// Swagger/SwaggerUI f<>r API-Dokumentation aktivieren // Swagger/SwaggerUI f<>r API-Dokumentation aktivieren
//if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
//{ {
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();
//} }
// app.UseHttpsRedirection(); // Auskommentiert f<>r Docker HTTP-Entwicklung // app.UseHttpsRedirection(); // Auskommentiert f<>r Docker HTTP-Entwicklung