using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Diagnostics; using Microsoft.EntityFrameworkCore; using Novelly.Api.Activity; using Novelly.Api.Agent; using Novelly.Api.Beats; using Novelly.Api.Chapters; using Novelly.Api.Characters; using Novelly.Api.Common; using Novelly.Api.Data; using Novelly.Api.Genres; using Novelly.Api.Imports; using Novelly.Api.Locations; using Novelly.Api.Novels; using Novelly.Api.Questions; using Novelly.Api.Tags; using Novelly.Api.Trash; using Novelly.Api.Users; using Serilog; var builder = WebApplication.CreateBuilder(args); builder.Services.AddSerilog((services, config) => config .ReadFrom.Configuration(builder.Configuration) .ReadFrom.Services(services) .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}")); builder.AddServiceDefaults(); builder.Services.AddNovelly(builder.Configuration); builder.Services.AddOpenApi(); builder.Services.AddProblemDetails(); builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); var corsOrigins = builder.Configuration.GetSection("Cors:Origins").Get() ?? ["http://localhost:5173"]; builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy .WithOrigins(corsOrigins) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials())); var migrateOnly = args.Contains("--migrate-only"); var app = builder.Build(); using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); try { await db.Database.MigrateAsync(); } catch (Exception ex) { app.Logger.LogCritical(ex, "Database migration failed on startup"); Environment.Exit(1); } if (migrateOnly) { app.Logger.LogInformation("Migration complete, exiting ({MigrateOnlyFlag})", "--migrate-only"); Environment.Exit(0); } await ServiceUser.EnsureSeededAsync(db, builder.Configuration[ServiceApiKeyAuthenticationHandler.ConfigurationKey], app.Logger); await ActivityBackfill.RunAsync(db, app.Logger); var importRoot = builder.Configuration.GetSection(ImportOptions.SectionName)[nameof(ImportOptions.RootPath)]; if (!string.IsNullOrWhiteSpace(importRoot)) { Directory.CreateDirectory(importRoot); } } app.UseSerilogRequestLogging(); app.UseExceptionHandler(handler => handler.Run(async context => { var exception = context.Features.Get()?.Error; var (status, title) = exception switch { AgentNotConfiguredException => (StatusCodes.Status503ServiceUnavailable, "Agent unavailable"), NotAuthorizedException => (StatusCodes.Status403Forbidden, "Forbidden"), ArgumentException or InvalidOperationException => (StatusCodes.Status400BadRequest, "Invalid request"), _ => (StatusCodes.Status500InternalServerError, "Unexpected error") }; app.Logger.Log( status == StatusCodes.Status500InternalServerError ? LogLevel.Error : LogLevel.Warning, exception, "Handled {StatusCode} on {Path}: {Title}", status, context.Request.Path, title); await Results .Problem(title: title, detail: exception?.Message, statusCode: status) .ExecuteAsync(context); })); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.MapDefaultEndpoints(); app.MapGet("/api/health", () => Results.Ok(new { status = "ok" })).WithTags("Health").AllowAnonymous(); app.MapUiSettingsEndpoints(); app.MapUserEndpoints(); app.MapNovelMemberEndpoints(); app.MapNovelEndpoints() .MapCharacterEndpoints() .MapChapterEndpoints() .MapBeatEndpoints() .MapTagEndpoints() .MapLocationEndpoints() .MapGenreEndpoints() .MapOpenQuestionEndpoints() .MapAgentEndpoints() .MapImportEndpoints() .MapActivityEndpoints() .MapTrashEndpoints(); app.Run(); [ExcludeFromCodeCoverage] public partial class Program;