From f6ee88baf376566febea822fdb701d6770baf771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Mon, 19 Oct 2026 00:47:36 +0300 Subject: [PATCH] =?UTF-8?q?/setup=20y=C3=B6nlendirmesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++- .../Controllers/SetupController.cs | 29 ++++++-- .../DbStartup/DatabaseReadiness.cs | 7 ++ .../DbStartup/DatabaseReadinessGate.cs | 66 +++++++++++++++++++ .../DbStartup/SetupStatusDto.cs | 11 ++-- .../PlatformSessionValidationMiddleware.cs | 5 +- .../PlatformHttpApiHostModule.cs | 40 ++++------- 7 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/DatabaseReadinessGate.cs diff --git a/README.md b/README.md index 513339af..9e128a06 100644 --- a/README.md +++ b/README.md @@ -225,10 +225,16 @@ dotnet run --project src/Sozsoft.Platform.HttpApi.Host Uygulama `https://localhost:44344` adresinde açılır. Swagger UI kök adreste yayınlanır. -**Veritabanı ilk kurulum:** Veritabanı yoksa host, tam ABP yığını yerine minimal bir +**Veritabanı ilk kurulum:** Açılışta veritabanı yoksa host, tam ABP yığını yerine minimal bir "setup" uygulaması ile açılır (`DbStartup/SetupAppRunner`). Bu durumda: - `GET /api/setup/application-status` — veritabanının hazır olup olmadığını döner (anonim). + `setupMode: true` ise UI `/setup`'a yönlendirir; katalog ya da çekirdek şema yokken tam yığın da + bunu döner (`DatabaseReadiness.RequiresSetup`), bağlantı hatası ise bakım ekranında kalır. +- Veritabanı tam yığın çalışırken silinirse süreç kurulum moduna geçmez; `DatabaseReadinessGate` + kurulum dışı her isteğe 503 döner, `/api/setup/*` isteklerini veritabanına dokunan ara katmanları + (dil, tenant, oturum) atlayarak geçirir ve Authorization başlığını düşürür — migration yalnızca + kurulum parolasıyla çalışır, bitince uygulama kendini durdurur (process yöneticisi yeniden başlatır). - UI tarafında `/setup` route'u (`views/setup/DatabaseSetup.tsx`) migration'ı tetikleyen ekranı gösterir. - Migration için `Setup:MigrationPassword` yapılandırması veya `App.Setup.Migrate` yetkisi gerekir. - Ekran DbMigrator'ü `SetupMigrationRunner` ile çalıştırır. `Setup:MigratorPath` publish çıktısını @@ -2233,7 +2239,7 @@ configs/seeds/ | Belirti | Olası neden ve çözüm | | --- | --- | -| `/api/abp/application-configuration` 503 dönüyor | Veritabanına erişilemiyor. Bağlantı dizesini ve veri konteynerini kontrol edin; `/api/setup/application-status` ile durumu görün. | +| `/api/abp/application-configuration` (ve kurulum dışı her uç) 503 dönüyor | Veritabanına erişilemiyor (`DatabaseReadinessGate`). Bağlantı dizesini ve veri konteynerini kontrol edin; `/api/setup/application-status` ile durumu görün. | | UI `/setup` ekranına düşüyor | Şema oluşmamış veya bekleyen migration var. Setup ekranından migration'ı tetikleyin ya da DbMigrator'ı çalıştırın. | | Giriş "UserCannotSignIn..." hatası veriyor | [11.3](#113-giriş-engelleri) tablosuna bakın: IP, çalışma saati, doğrulama, şube/eş zamanlı kullanıcı limiti. | | Yeni sürüm tarayıcıda görünmüyor | Yeni worker indirmeyi bitirince kendiliğinden devreye girer ve sayfa yenilenir (`swRegistration.ts`). Kurulum takıldıysa sekmeyi tamamen kapatıp açın ya da DevTools › Application › Service Workers'tan eski worker'ı kaldırın. | diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs index b07355b2..5b182221 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs @@ -27,8 +27,9 @@ namespace Sozsoft.Platform.Controllers; /// Canlı sistemde (tam ABP stack ayaktayken) veritabanı durumunu raporlar ve /// yetkili host kullanıcısının migration tetiklemesine izin verir. /// -/// Veritabanı hiç yokken bu controller çalışmaz; o durumda -/// içindeki minimal uygulama devrededir. +/// Açılışta veritabanı yoksa bu controller çalışmaz; o durumda +/// içindeki minimal uygulama devrededir. Veritabanı tam stack +/// çalışırken kaybolursa istekler üzerinden yine buraya ulaşır. /// İki yol da üzerinden aynı migration akışını kullanır. /// [Route("api/setup")] @@ -122,7 +123,11 @@ public class SetupController : ControllerBase "Database migration triggered via /api/setup/migrate by user {UserId}.", User?.Identity?.Name ?? "(setup password)"); - await RunMigratorAsync(environment: null, reloadWorkers: true); + // Katalog yokken kurulan veritabanı yeni kimliklerle gelir; çalışan stack'in cache'leri, + // Hangfire ve OpenIddict depoları eski veritabanına aittir. Minimal kurulum uygulamasındaki + // gibi süreç durur, process yöneticisi yeniden başlatır. + var restartAfterMigration = DatabaseReadinessChecker.GetStatus(_configuration).RequiresSetup; + await RunMigratorAsync(environment: null, reloadWorkers: !restartAfterMigration, restartAfterMigration); } /// @@ -170,8 +175,12 @@ public class SetupController : ControllerBase /// DbMigrator'ü tek kilit altında çalıştırır; migrate ile tenant seed aynı anda yürümez. /// Worker tanımları host verisidir; tek tenant seed'i onlara dokunmadığı için /// yalnızca tam migrate'te açıktır. + /// açıkken worker tazelenmez, uygulama kendini durdurur. /// - private async Task RunMigratorAsync(IReadOnlyDictionary? environment, bool reloadWorkers) + private async Task RunMigratorAsync( + IReadOnlyDictionary? environment, + bool reloadWorkers, + bool restartAfterMigration = false) { var stream = new SetupEventStream(Response); await stream.StartAsync(); @@ -199,6 +208,18 @@ public class SetupController : ControllerBase return; } + if (restartAfterMigration) + { + await stream.SuccessAsync("Migration and seed completed successfully."); + await stream.RestartAsync("Application server is restarting..."); + await stream.DoneAsync("Completed."); + + // İstemcinin "restart" olayını işleyip poll'a geçmesi için kısa gecikme. + _ = Task.Delay(TimeSpan.FromSeconds(1.5)) + .ContinueWith(_ => _lifetime.StopApplication(), TaskScheduler.Default); + return; + } + if (!reloadWorkers) { await stream.SuccessAsync("Migration and seed completed successfully."); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/DatabaseReadiness.cs b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/DatabaseReadiness.cs index 98d0be98..ffff7bc5 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/DatabaseReadiness.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/DatabaseReadiness.cs @@ -19,6 +19,13 @@ public sealed record DatabaseReadiness( /// Uygulamanın tam ABP stack ile açılabilmesi için yeterli mi. public bool IsReady => DatabaseExists && SchemaReady; + /// + /// Kurulum gerekiyor mu: sunucuya ulaşıldı ama katalog ya da çekirdek şema yok. Bağlantı/oturum + /// hatası ( dolu) geçici sayılır ve kurulum değildir; o durumda istemci + /// bakım ekranında kalır. + /// + public bool RequiresSetup => !IsReady && Error is null; + public static DatabaseReadiness NotReady(string? error) => new(false, false, null, error); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/DatabaseReadinessGate.cs b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/DatabaseReadinessGate.cs new file mode 100644 index 00000000..02287c8a --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/DatabaseReadinessGate.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Net.Http.Headers; + +namespace Sozsoft.Platform.DbStartup; + +/// +/// Veritabanı hazır değilken boru hattının kapısı. +/// +/// Kurulum moduna yalnızca açılışta karar verilir (); tam ABP stack +/// çalışırken veritabanı kaybolursa (katalog düşürüldü, sunucu erişilemez) süreç kurulum moduna +/// geçmez. Bu durumda istek yolundaki dil sağlayıcısı, tenant çözümleyici ve oturum doğrulaması +/// veritabanına gidip 500 ile düşüyordu — kurulumu yapacak /api/setup/* uçları da dahil. +/// +/// - Kurulum dışı her isteğe anında 503 döner; EF sorguları ve bağlantı zaman aşımı devreye girmez. +/// - /api/setup/* isteklerini geçirir ama veritabanına dokunan ara katmanların atlanması için +/// işaretler (). Oturum veritabanı olmadan doğrulanamayacağı için +/// Authorization başlığı düşürülür; migration bu durumda yalnızca kurulum parolasıyla çalışır +/// (). +/// +/// Kontrol cache'lidir (), istek başına maliyeti yoktur. +/// +public sealed class DatabaseReadinessGate +{ + public const string SetupPathPrefix = "/api/setup"; + + private const string BypassItemKey = "Sozsoft.DatabaseReadinessGate.Bypass"; + + private readonly RequestDelegate _next; + private readonly IConfiguration _configuration; + + public DatabaseReadinessGate(RequestDelegate next, IConfiguration configuration) + { + _next = next; + _configuration = configuration; + } + + public async Task InvokeAsync(HttpContext context) + { + if (DatabaseReadinessChecker.GetStatus(_configuration).IsReady) + { + await _next(context); + return; + } + + if (context.Request.Path.StartsWithSegments(SetupPathPrefix)) + { + context.Items[BypassItemKey] = true; + context.Request.Headers.Remove(HeaderNames.Authorization); + await _next(context); + return; + } + + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + context.Response.ContentType = "text/plain; charset=utf-8"; + await context.Response.WriteAsync("The database is not reachable."); + } + + /// + /// Bu istekte veritabanına dokunan ara katmanlar atlanmalı mı (veritabanı hazır değilken + /// geçirilen kurulum isteği). + /// + public static bool IsDatabaseBypassed(HttpContext context) + => context.Items.ContainsKey(BypassItemKey); +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupStatusDto.cs b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupStatusDto.cs index 4c4135bf..5a2e58fe 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupStatusDto.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupStatusDto.cs @@ -12,11 +12,12 @@ public sealed class SetupStatusDto public bool DbExists { get; init; } /// - /// Sunucu kurulum modunda mı (minimal uygulama) çalışıyor. + /// Kurulum ekranı gösterilmeli mi. Frontend yalnızca bu değer true iken /setup + /// sayfasına yönlendirir. /// - /// Frontend yalnızca bu değer true iken /setup sayfasına yönlendirir. Tam ABP stack - /// ayaktayken bu değer her zaman false'tur; böylece canlı sistemde geçici bir - /// veritabanı sorunu anonim kullanıcıyı kurulum ekranına düşürmez. + /// Minimal kurulum uygulamasında her zaman true. Tam ABP stack ayaktayken yalnızca + /// katalog/şema yokken () true; bağlantı + /// hatası gibi geçici bir sorun anonim kullanıcıyı kurulum ekranına düşürmez. /// public bool SetupMode { get; init; } @@ -37,7 +38,7 @@ public sealed class SetupStatusDto return new SetupStatusDto { DbExists = readiness.IsReady, - SetupMode = setupMode, + SetupMode = setupMode || readiness.RequiresSetup, PendingMigrationCount = includeDiagnostics ? readiness.PendingMigrationCount : null, // Hata detayı bağlantı dizesi/sunucu adı sızdırabilir — sadece yetkiliye. Error = includeDiagnostics ? readiness.Error : null, diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs index 8d348aa1..36bc1158 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs @@ -15,8 +15,7 @@ namespace Sozsoft.Platform.Identity; public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDependency { - private const string SetupPathPrefix = "/api/setup"; - private const string SetupStatusPath = "/api/setup/application-status"; + private const string SetupStatusPath = DatabaseReadinessGate.SetupPathPrefix + "/application-status"; private readonly IIdentitySessionRepository sessionRepository; private readonly ICurrentTenant currentTenant; @@ -101,7 +100,7 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend { var path = context.Request.Path; - if (!path.StartsWithSegments(SetupPathPrefix)) + if (!path.StartsWithSegments(DatabaseReadinessGate.SetupPathPrefix)) { return false; } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs index 9ba1c014..67e0de2a 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Extensions.DependencyInjection; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.ResponseCompression; @@ -471,7 +470,12 @@ public class PlatformHttpApiHostModule : AbpModule app.UseDeveloperExceptionPage(); } - app.UseAbpRequestLocalization(); + // Veritabanı hazır değilken kurulum dışı istekler burada 503 ile kesilir; kurulum istekleri + // veritabanına dokunan ara katmanları (dil, tenant) atlayarak SetupController'a ulaşır. + app.UseMiddleware(); + app.UseWhen( + httpContext => !DatabaseReadinessGate.IsDatabaseBypassed(httpContext), + branch => branch.UseAbpRequestLocalization()); // Auth hataları MVC tarafından /Error sayfasına yönlendirilmesin diye bu kaldırıldı // if (!env.IsDevelopment()) @@ -483,29 +487,6 @@ public class PlatformHttpApiHostModule : AbpModule app.UseResponseCompression(); app.MapAbpStaticAssets(); app.UseRouting(); - // Not: /api/setup/application-status artık SetupController tarafından karşılanıyor. - // Buradaki kopya middleware kaldırıldı — durum mantığı tek yerde (DatabaseReadinessChecker). - - // Veritabanı erişilemezken uygulama konfigürasyonu üretilemez. İsteği burada, - // EF sorguları ve bağlantı zaman aşımları devreye girmeden kesiyoruz: aksi halde - // istemci 10+ saniye bekleyip, contributor'ın içinde patlayan bir bağlantı - // hatasından türeyen belirsiz bir yanıt alıyordu. Kontrol cache'li, maliyeti yok. - app.Use(async (httpContext, next) => - { - if (httpContext.Request.Path.StartsWithSegments("/api/abp/application-configuration")) - { - var configuration = httpContext.RequestServices.GetRequiredService(); - if (!DatabaseReadinessChecker.GetStatus(configuration).IsReady) - { - httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - httpContext.Response.ContentType = "text/plain; charset=utf-8"; - await httpContext.Response.WriteAsync("The database is not reachable."); - return; - } - } - - await next(); - }); app.Use(async (context, next) => { @@ -528,8 +509,13 @@ public class PlatformHttpApiHostModule : AbpModule if (PlatformConsts.IsMultiTenant) { - app.UseMultiTenancy(); - app.UseMiddleware(); + app.UseWhen( + httpContext => !DatabaseReadinessGate.IsDatabaseBypassed(httpContext), + branch => + { + branch.UseMultiTenancy(); + branch.UseMiddleware(); + }); } app.UseUnitOfWork();