using System.Text.Json.Serialization; using GerbilManagerWebAPI.Endpoints; using GerbilManagerWebAPI.Models; using GerbilManagerWebAPI.Services; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); // LAN reachability: bind on all interfaces (keep the Aspire/launch-assigned port), // so phones/laptops on the home network can reach the API (trusted LAN, no auth). var aspnetUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); if (!string.IsNullOrWhiteSpace(aspnetUrls)) { var lanUrls = string.Join(';', aspnetUrls .Split(';', StringSplitOptions.RemoveEmptyEntries) .Select(u => u.Replace("localhost", "0.0.0.0").Replace("127.0.0.1", "0.0.0.0"))); builder.WebHost.UseUrls(lanUrls); } builder.AddNpgsqlDbContext("gerbilmanager"); // JSON: camelCase property names + enums serialised as their string names (frontend contract). builder.Services.ConfigureHttpJsonOptions(o => { o.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); // Permissive CORS for the trusted home LAN (no auth — see board constraint). const string LanCorsPolicy = "lan"; builder.Services.AddCors(options => options.AddPolicy(LanCorsPolicy, policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod())); // OpenAPI document for the Scalar API reference (Swashbuckle removed). builder.Services.AddOpenApi(); // FEAT-12a: provider-agnostic AI config (env vars AI__BaseUrl/AI__ApiKey/AI__Model // or user-secrets — never committed) + typed client for sale-ad generation. builder.Services.AddOptions() .BindConfiguration(GerbilManagerWebAPI.SaleAd.AiOptions.SectionName); builder.Services.AddHttpClient( http => http.Timeout = TimeSpan.FromSeconds(60)); // INBOX-2: KI-Antwortentwurf (gleiche AI-Sektion, gleicher Wire-Client). builder.Services.AddHttpClient( http => http.Timeout = TimeSpan.FromSeconds(60)); // FEAT-NAMEGEN: Name suggestions via Gemini (same AI section, same wire client). builder.Services.AddHttpClient( http => http.Timeout = TimeSpan.FromSeconds(60)); // WEB-2: public site publish path (env var PublicSite__RootPath; empty = disabled) builder.Services.AddOptions() .BindConfiguration(GerbilManagerWebAPI.Cms.PublicSiteOptions.SectionName); // INBOX-0: Gmail inbox. App Password encrypted at rest via Data Protection. // AR-3: persist the key ring so encrypted passwords survive image redeployments. // In prod the path is mounted to a persistent volume (compose DataProtection__KeyRingPath). // In dev (Aspire) keys live in the content root — ephemeral, which is fine there. { var keyRingPath = builder.Configuration["DataProtection:KeyRingPath"] ?? Path.Combine(builder.Environment.ContentRootPath, ".data-protection-keys"); Directory.CreateDirectory(keyRingPath); builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(keyRingPath)) .SetApplicationName("GerbilManager"); } builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); var app = builder.Build(); app.MapDefaultEndpoints(); // API reference at /scalar (built on the OpenAPI doc at /openapi/v1.json). app.MapOpenApi(); app.MapScalarApiReference(); // Apply EF migrations at startup (no-op if schema is current; safe for single-instance deploy). // Unter "Testing" übersprungen: die Endpoint-Tests laufen auf SQLite in-memory // (EnsureCreated) — die Npgsql-Migrationen sind dort nicht anwendbar. if (!app.Environment.IsEnvironment("Testing")) { using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.Database.Migrate(); // Startup sweep: derive status for animals that silently crossed the 7-year threshold // since the last write. No-op if all statuses are already current. var today = DateOnly.FromDateTime(DateTime.UtcNow); var candidates = await db.Gerbils .Where(g => g.Status != GerbilStatus.Deceased && g.Status != GerbilStatus.GivenAway && g.DateOfDeath == null && g.ReceiverContactId == null && g.DateOfBirth != null && g.DateOfBirth < today.AddYears(-GerbilStatusService.MaxAgeYears)) .ToListAsync(); if (candidates.Count > 0) { foreach (var g in candidates) g.Status = GerbilStatus.Deceased; await db.SaveChangesAsync(); } } app.UseCors(LanCorsPolicy); // Endpoint groups (Minimal API, no controllers). app.MapGerbilEndpoints(); app.MapLitterEndpoints(); app.MapContactEndpoints(); app.MapEnclosureEndpoints(); app.MapColorVarietyEndpoints(); app.MapHealthRecordEndpoints(); app.MapWeightRecordEndpoints(); app.MapInbreedingEndpoints(); app.MapPhotoEndpoints(); app.MapSaleAdEndpoints(); app.MapImportEndpoints(); app.MapImportDocxEndpoints(); app.MapContractEndpoints(); app.MapSettingsEndpoints(); app.MapExportEndpoints(); app.MapCmsEndpoints(); app.MapRequestEndpoints(); app.MapNamesEndpoints(); app.MapFeedbackEndpoints(); app.MapVerifiedGerbilEndpoints(); app.MapPushEndpoints(); app.MapAcquisitionEndpoints(); app.MapSaleReservationEndpoints(); app.MapWaitingListEndpoints(); app.MapReturnRecordEndpoints(); app.MapExhibitionEndpoints(); app.MapRefsEndpoints(); app.Run(); // Sichtbarer Programmtyp für WebApplicationFactory (Endpoint-Tests). public partial class Program { }