/setup yönlendirmesi

This commit is contained in:
Sedat Öztürk 2026-10-19 00:47:36 +03:00
parent 20cbd50dc8
commit f6ee88baf3
7 changed files with 127 additions and 41 deletions

View file

@ -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. |

View file

@ -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
/// <see cref="DbStartup.SetupAppRunner"/> içindeki minimal uygulama devrededir.
/// Açılışta veritabanı yoksa bu controller çalışmaz; o durumda
/// <see cref="DbStartup.SetupAppRunner"/> içindeki minimal uygulama devrededir. Veritabanı tam stack
/// çalışırken kaybolursa istekler <see cref="DatabaseReadinessGate"/> üzerinden yine buraya ulaşır.
/// İki yol da <see cref="SetupMigrationRunner"/> üzerinden aynı migration akışını kullanır.
/// </summary>
[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);
}
/// <summary>
@ -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
/// <paramref name="reloadWorkers"/> yalnızca tam migrate'te açıktır.
/// <paramref name="restartAfterMigration"/> açıkken worker tazelenmez, uygulama kendini durdurur.
/// </summary>
private async Task RunMigratorAsync(IReadOnlyDictionary<string, string>? environment, bool reloadWorkers)
private async Task RunMigratorAsync(
IReadOnlyDictionary<string, string>? 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.");

View file

@ -19,6 +19,13 @@ public sealed record DatabaseReadiness(
/// <summary>Uygulamanın tam ABP stack ile açılabilmesi için yeterli mi.</summary>
public bool IsReady => DatabaseExists && SchemaReady;
/// <summary>
/// Kurulum gerekiyor mu: sunucuya ulaşıldı ama katalog ya da çekirdek şema yok. Bağlantı/oturum
/// hatası (<see cref="Error"/> dolu) geçici sayılır ve kurulum değildir; o durumda istemci
/// bakım ekranında kalır.
/// </summary>
public bool RequiresSetup => !IsReady && Error is null;
public static DatabaseReadiness NotReady(string? error)
=> new(false, false, null, error);

View file

@ -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;
/// <summary>
/// Veritabanı hazır değilken boru hattının kapısı.
///
/// Kurulum moduna yalnızca açılışta karar verilir (<see cref="Program"/>); 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 <c>/api/setup/*</c> uçları da dahil.
///
/// - Kurulum dışı her isteğe anında 503 döner; EF sorguları ve bağlantı zaman aşımı devreye girmez.
/// - <c>/api/setup/*</c> isteklerini geçirir ama veritabanına dokunan ara katmanların atlanması için
/// işaretler (<see cref="IsDatabaseBypassed"/>). Oturum veritabanı olmadan doğrulanamayacağı için
/// Authorization başlığı düşürülür; migration bu durumda yalnızca kurulum parolasıyla çalışır
/// (<see cref="Controllers.SetupController"/>).
///
/// Kontrol cache'lidir (<see cref="DatabaseReadinessChecker"/>), istek başına maliyeti yoktur.
/// </summary>
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.");
}
/// <summary>
/// Bu istekte veritabanına dokunan ara katmanlar atlanmalı mı (veritabanı hazır değilken
/// geçirilen kurulum isteği).
/// </summary>
public static bool IsDatabaseBypassed(HttpContext context)
=> context.Items.ContainsKey(BypassItemKey);
}

View file

@ -12,11 +12,12 @@ public sealed class SetupStatusDto
public bool DbExists { get; init; }
/// <summary>
/// Sunucu kurulum modunda mı (minimal uygulama) çalışıyor.
/// Kurulum ekranı gösterilmeli mi. Frontend yalnızca bu değer <c>true</c> iken /setup
/// sayfasına yönlendirir.
///
/// Frontend yalnızca bu değer <c>true</c> iken /setup sayfasına yönlendirir. Tam ABP stack
/// ayaktayken bu değer her zaman <c>false</c>'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 <c>true</c>. Tam ABP stack ayaktayken yalnızca
/// katalog/şema yokken (<see cref="DatabaseReadiness.RequiresSetup"/>) <c>true</c>; bağlantı
/// hatası gibi geçici bir sorun anonim kullanıcıyı kurulum ekranına düşürmez.
/// </summary>
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,

View file

@ -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;
}

View file

@ -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<DatabaseReadinessGate>();
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<IConfiguration>();
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<TenantLocalizationMiddleware>();
app.UseWhen(
httpContext => !DatabaseReadinessGate.IsDatabaseBypassed(httpContext),
branch =>
{
branch.UseMultiTenancy();
branch.UseMiddleware<TenantLocalizationMiddleware>();
});
}
app.UseUnitOfWork();