/setup yönlendirmesi
This commit is contained in:
parent
20cbd50dc8
commit
f6ee88baf3
7 changed files with 127 additions and 41 deletions
10
README.md
10
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.
|
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:
|
"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).
|
- `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.
|
- 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.
|
- 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ı
|
- 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 |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,9 @@ namespace Sozsoft.Platform.Controllers;
|
||||||
/// Canlı sistemde (tam ABP stack ayaktayken) veritabanı durumunu raporlar ve
|
/// Canlı sistemde (tam ABP stack ayaktayken) veritabanı durumunu raporlar ve
|
||||||
/// yetkili host kullanıcısının migration tetiklemesine izin verir.
|
/// yetkili host kullanıcısının migration tetiklemesine izin verir.
|
||||||
///
|
///
|
||||||
/// Veritabanı hiç yokken bu controller çalışmaz; o durumda
|
/// Açılışta veritabanı yoksa bu controller çalışmaz; o durumda
|
||||||
/// <see cref="DbStartup.SetupAppRunner"/> içindeki minimal uygulama devrededir.
|
/// <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.
|
/// İki yol da <see cref="SetupMigrationRunner"/> üzerinden aynı migration akışını kullanır.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("api/setup")]
|
[Route("api/setup")]
|
||||||
|
|
@ -122,7 +123,11 @@ public class SetupController : ControllerBase
|
||||||
"Database migration triggered via /api/setup/migrate by user {UserId}.",
|
"Database migration triggered via /api/setup/migrate by user {UserId}.",
|
||||||
User?.Identity?.Name ?? "(setup password)");
|
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>
|
/// <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.
|
/// 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
|
/// 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="reloadWorkers"/> yalnızca tam migrate'te açıktır.
|
||||||
|
/// <paramref name="restartAfterMigration"/> açıkken worker tazelenmez, uygulama kendini durdurur.
|
||||||
/// </summary>
|
/// </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);
|
var stream = new SetupEventStream(Response);
|
||||||
await stream.StartAsync();
|
await stream.StartAsync();
|
||||||
|
|
@ -199,6 +208,18 @@ public class SetupController : ControllerBase
|
||||||
return;
|
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)
|
if (!reloadWorkers)
|
||||||
{
|
{
|
||||||
await stream.SuccessAsync("Migration and seed completed successfully.");
|
await stream.SuccessAsync("Migration and seed completed successfully.");
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ public sealed record DatabaseReadiness(
|
||||||
/// <summary>Uygulamanın tam ABP stack ile açılabilmesi için yeterli mi.</summary>
|
/// <summary>Uygulamanın tam ABP stack ile açılabilmesi için yeterli mi.</summary>
|
||||||
public bool IsReady => DatabaseExists && SchemaReady;
|
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)
|
public static DatabaseReadiness NotReady(string? error)
|
||||||
=> new(false, false, null, error);
|
=> new(false, false, null, error);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -12,11 +12,12 @@ public sealed class SetupStatusDto
|
||||||
public bool DbExists { get; init; }
|
public bool DbExists { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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
|
/// Minimal kurulum uygulamasında her zaman <c>true</c>. Tam ABP stack ayaktayken yalnızca
|
||||||
/// ayaktayken bu değer her zaman <c>false</c>'tur; böylece canlı sistemde geçici bir
|
/// katalog/şema yokken (<see cref="DatabaseReadiness.RequiresSetup"/>) <c>true</c>; bağlantı
|
||||||
/// veritabanı sorunu anonim kullanıcıyı kurulum ekranına düşürmez.
|
/// hatası gibi geçici bir sorun anonim kullanıcıyı kurulum ekranına düşürmez.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SetupMode { get; init; }
|
public bool SetupMode { get; init; }
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ public sealed class SetupStatusDto
|
||||||
return new SetupStatusDto
|
return new SetupStatusDto
|
||||||
{
|
{
|
||||||
DbExists = readiness.IsReady,
|
DbExists = readiness.IsReady,
|
||||||
SetupMode = setupMode,
|
SetupMode = setupMode || readiness.RequiresSetup,
|
||||||
PendingMigrationCount = includeDiagnostics ? readiness.PendingMigrationCount : null,
|
PendingMigrationCount = includeDiagnostics ? readiness.PendingMigrationCount : null,
|
||||||
// Hata detayı bağlantı dizesi/sunucu adı sızdırabilir — sadece yetkiliye.
|
// Hata detayı bağlantı dizesi/sunucu adı sızdırabilir — sadece yetkiliye.
|
||||||
Error = includeDiagnostics ? readiness.Error : null,
|
Error = includeDiagnostics ? readiness.Error : null,
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ namespace Sozsoft.Platform.Identity;
|
||||||
|
|
||||||
public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDependency
|
public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDependency
|
||||||
{
|
{
|
||||||
private const string SetupPathPrefix = "/api/setup";
|
private const string SetupStatusPath = DatabaseReadinessGate.SetupPathPrefix + "/application-status";
|
||||||
private const string SetupStatusPath = "/api/setup/application-status";
|
|
||||||
|
|
||||||
private readonly IIdentitySessionRepository sessionRepository;
|
private readonly IIdentitySessionRepository sessionRepository;
|
||||||
private readonly ICurrentTenant currentTenant;
|
private readonly ICurrentTenant currentTenant;
|
||||||
|
|
@ -101,7 +100,7 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend
|
||||||
{
|
{
|
||||||
var path = context.Request.Path;
|
var path = context.Request.Path;
|
||||||
|
|
||||||
if (!path.StartsWithSegments(SetupPathPrefix))
|
if (!path.StartsWithSegments(DatabaseReadinessGate.SetupPathPrefix))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Extensions.DependencyInjection;
|
using Microsoft.AspNetCore.Extensions.DependencyInjection;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.ResponseCompression;
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
|
|
@ -471,7 +470,12 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
app.UseDeveloperExceptionPage();
|
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ı
|
// Auth hataları MVC tarafından /Error sayfasına yönlendirilmesin diye bu kaldırıldı
|
||||||
// if (!env.IsDevelopment())
|
// if (!env.IsDevelopment())
|
||||||
|
|
@ -483,29 +487,6 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
app.MapAbpStaticAssets();
|
app.MapAbpStaticAssets();
|
||||||
app.UseRouting();
|
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) =>
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
|
|
@ -528,8 +509,13 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
|
|
||||||
if (PlatformConsts.IsMultiTenant)
|
if (PlatformConsts.IsMultiTenant)
|
||||||
{
|
{
|
||||||
app.UseMultiTenancy();
|
app.UseWhen(
|
||||||
app.UseMiddleware<TenantLocalizationMiddleware>();
|
httpContext => !DatabaseReadinessGate.IsDatabaseBypassed(httpContext),
|
||||||
|
branch =>
|
||||||
|
{
|
||||||
|
branch.UseMultiTenancy();
|
||||||
|
branch.UseMiddleware<TenantLocalizationMiddleware>();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseUnitOfWork();
|
app.UseUnitOfWork();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue