2026-08-21 19:27:59 +00:00
|
|
|
|
using System;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
using System.Linq;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
using System.Threading;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
using Microsoft.Extensions.Configuration;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
using Microsoft.Extensions.Hosting;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using Sozsoft.Platform.DbStartup;
|
|
|
|
|
|
using Sozsoft.Platform.EntityFrameworkCore;
|
|
|
|
|
|
using Sozsoft.Platform.Permissions;
|
|
|
|
|
|
using Volo.Abp.DistributedLocking;
|
|
|
|
|
|
using Volo.Abp.EntityFrameworkCore;
|
|
|
|
|
|
using Volo.Abp.MultiTenancy;
|
|
|
|
|
|
using Volo.Abp.Uow;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
|
|
|
|
|
namespace Sozsoft.Platform.Controllers;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-08-09 08:52:28 +00:00
|
|
|
|
/// 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.
|
|
|
|
|
|
/// İki yol da <see cref="SetupMigrationRunner"/> üzerinden aynı migration akışını kullanır.
|
2026-04-28 14:53:58 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Route("api/setup")]
|
|
|
|
|
|
public class SetupController : ControllerBase
|
|
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Yeni izin verilmemiş kurulumlarda kilitlenmemek için tanınan geriye dönük rol.
|
|
|
|
|
|
/// <see cref="PlatformPermissions.Setup.Migrate"/> rollere atandıktan sonra kaldırılabilir.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const string LegacyAdminRoleName = "admin";
|
|
|
|
|
|
|
|
|
|
|
|
private const int DistributedLockTimeoutSeconds = 5;
|
|
|
|
|
|
|
2026-04-28 14:53:58 +00:00
|
|
|
|
private readonly IConfiguration _configuration;
|
|
|
|
|
|
private readonly IHostEnvironment _env;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
private readonly IHostApplicationLifetime _lifetime;
|
|
|
|
|
|
private readonly IAuthorizationService _authorizationService;
|
|
|
|
|
|
private readonly ICurrentTenant _currentTenant;
|
|
|
|
|
|
private readonly IAbpDistributedLock _distributedLock;
|
|
|
|
|
|
private readonly IUnitOfWorkManager _unitOfWorkManager;
|
2026-06-29 09:37:52 +00:00
|
|
|
|
private readonly BackgroundWorkerInitializer _backgroundWorkerInitializer;
|
|
|
|
|
|
private readonly AbpBackgroundWorkerRecurringJobInitializer _abpBackgroundWorkerRecurringJobInitializer;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
private readonly ILogger<SetupController> _logger;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
|
|
|
|
|
public SetupController(
|
|
|
|
|
|
IConfiguration configuration,
|
2026-06-29 09:37:52 +00:00
|
|
|
|
IHostEnvironment env,
|
2026-08-09 08:52:28 +00:00
|
|
|
|
IHostApplicationLifetime lifetime,
|
|
|
|
|
|
IAuthorizationService authorizationService,
|
|
|
|
|
|
ICurrentTenant currentTenant,
|
|
|
|
|
|
IAbpDistributedLock distributedLock,
|
|
|
|
|
|
IUnitOfWorkManager unitOfWorkManager,
|
2026-06-29 09:37:52 +00:00
|
|
|
|
BackgroundWorkerInitializer backgroundWorkerInitializer,
|
2026-08-09 08:52:28 +00:00
|
|
|
|
AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer,
|
|
|
|
|
|
ILogger<SetupController> logger)
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
|
|
|
|
|
_configuration = configuration;
|
|
|
|
|
|
_env = env;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
_lifetime = lifetime;
|
|
|
|
|
|
_authorizationService = authorizationService;
|
|
|
|
|
|
_currentTenant = currentTenant;
|
|
|
|
|
|
_distributedLock = distributedLock;
|
|
|
|
|
|
_unitOfWorkManager = unitOfWorkManager;
|
2026-06-29 09:37:52 +00:00
|
|
|
|
_backgroundWorkerInitializer = backgroundWorkerInitializer;
|
|
|
|
|
|
_abpBackgroundWorkerRecurringJobInitializer = abpBackgroundWorkerRecurringJobInitializer;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
_logger = logger;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Veritabanı durumu. Anonim çağırana yalnızca "hazır mı" bilgisi döner;
|
|
|
|
|
|
/// bekleyen migration sayısı ve hata detayı sadece yetkili host kullanıcısına verilir.
|
|
|
|
|
|
/// </summary>
|
2026-05-24 15:48:55 +00:00
|
|
|
|
[HttpGet("application-status")]
|
2026-04-28 14:53:58 +00:00
|
|
|
|
[AllowAnonymous]
|
2026-08-09 08:52:28 +00:00
|
|
|
|
public async Task<SetupStatusDto> StatusAsync(CancellationToken cancellationToken)
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
var readiness = DatabaseReadinessChecker.GetStatus(_configuration);
|
|
|
|
|
|
var canMigrate = await CanMigrateAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (canMigrate && readiness.IsReady)
|
|
|
|
|
|
{
|
|
|
|
|
|
readiness = readiness.WithPendingMigrations(
|
|
|
|
|
|
await GetPendingMigrationCountAsync(cancellationToken));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return SetupStatusDto.From(readiness, includeDiagnostics: canMigrate);
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Migration + seed çalıştırır ve logları SSE olarak akıtır.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Yetki: veritabanı ayaktayken host tarafında <see cref="PlatformPermissions.Setup.Migrate"/>
|
|
|
|
|
|
/// izni (veya geriye dönük olarak admin rolü); veritabanı yokken kurulum parolası.
|
|
|
|
|
|
/// </summary>
|
2026-04-28 14:53:58 +00:00
|
|
|
|
[HttpPost("migrate")]
|
2026-06-29 09:37:52 +00:00
|
|
|
|
[AllowAnonymous]
|
2026-08-09 08:52:28 +00:00
|
|
|
|
public async Task MigrateAsync()
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
var (authorized, denialReason) = await AuthorizeMigrateRequestAsync();
|
|
|
|
|
|
if (!authorized)
|
2026-06-29 09:37:52 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
_logger.LogWarning("Database migration request denied: {Reason}", denialReason);
|
2026-06-29 09:37:52 +00:00
|
|
|
|
Response.StatusCode = StatusCodes.Status403Forbidden;
|
2026-08-09 08:52:28 +00:00
|
|
|
|
Response.ContentType = "text/plain; charset=utf-8";
|
|
|
|
|
|
await Response.WriteAsync(denialReason);
|
2026-06-29 09:37:52 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
var stream = new SetupEventStream(Response);
|
|
|
|
|
|
await stream.StartAsync();
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
// Çok örnekli (multi-instance) dağıtımlarda aynı anda tek migration çalışsın.
|
|
|
|
|
|
await using var lockHandle = await _distributedLock.TryAcquireAsync(
|
|
|
|
|
|
"Sozsoft.Platform.SetupMigration",
|
|
|
|
|
|
TimeSpan.FromSeconds(DistributedLockTimeoutSeconds),
|
|
|
|
|
|
_lifetime.ApplicationStopping);
|
|
|
|
|
|
|
|
|
|
|
|
if (lockHandle is null)
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
await stream.ErrorAsync("Another migration is already in progress. Please try again later.");
|
|
|
|
|
|
await stream.DoneAsync("Failed.");
|
|
|
|
|
|
return;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
_logger.LogWarning(
|
|
|
|
|
|
"Database migration triggered via /api/setup/migrate by user {UserId}.",
|
|
|
|
|
|
User?.Identity?.Name ?? "(setup password)");
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
// ApplicationStopping kullanılıyor: istemci sekmesini kapatsa da migration tamamlanır.
|
|
|
|
|
|
var outcome = await SetupMigrationRunner.ExecuteAsync(
|
|
|
|
|
|
_configuration, _env.ContentRootPath, stream, _lifetime.ApplicationStopping);
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
if (outcome != MigrationOutcome.Succeeded)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning("Database migration finished with outcome {Outcome}.", outcome);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
await stream.InfoAsync("Migration and seed completed successfully.");
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
// Yeni/değişen tanımlar Hangfire'a yansısın diye worker'lar tazelenir.
|
|
|
|
|
|
try
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
await stream.InfoAsync("Loading background workers into Hangfire...");
|
|
|
|
|
|
await _abpBackgroundWorkerRecurringJobInitializer.RunAsync(_lifetime.ApplicationStopping);
|
|
|
|
|
|
await _backgroundWorkerInitializer.RunAsync();
|
|
|
|
|
|
await stream.SuccessAsync("Background workers loaded successfully.");
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
2026-08-09 08:52:28 +00:00
|
|
|
|
catch (Exception ex)
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
_logger.LogError(ex, "Background workers could not be reloaded after migration.");
|
|
|
|
|
|
await stream.ErrorAsync($"Background workers could not be loaded: {ex.Message}");
|
|
|
|
|
|
await stream.DoneAsync("Failed.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
await stream.DoneAsync("Completed.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Yetkilendirme
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// İki geçerli kimlik yolu vardır:
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 1. <c>X-Setup-Password</c> header'ı — veritabanına hiç ihtiyaç duymaz, bu yüzden
|
|
|
|
|
|
/// veritabanı düşmüşken de çalışır.
|
|
|
|
|
|
/// 2. Oturum açmış host kullanıcısı + <see cref="PlatformPermissions.Setup.Migrate"/> izni —
|
|
|
|
|
|
/// izin kontrolü veritabanı okuması gerektirdiği için yalnızca veritabanı ayaktayken
|
|
|
|
|
|
/// (örn. yalnızca bekleyen migration varken) kullanılabilir.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Reddetme nedeni çağırana döndürülür; aksi halde "yetkisiz" hatası teşhis edilemez oluyordu.
|
|
|
|
|
|
/// Nedenler yapılandırma/izin adından fazlasını sızdırmaz.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<(bool Authorized, string Reason)> AuthorizeMigrateRequestAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Request.Headers.ContainsKey(SetupPasswordValidator.HeaderName))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!SetupPasswordValidator.IsConfigured(_configuration))
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
return (false, "Setup password is not configured on the server (Setup:MigrationPassword).");
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
|
|
|
|
|
return SetupPasswordValidator.IsValid(_configuration, Request.Headers)
|
|
|
|
|
|
? (true, string.Empty)
|
|
|
|
|
|
: (false, "Invalid setup password.");
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
|
|
|
|
|
if (!DatabaseReadinessChecker.GetStatus(_configuration).IsReady)
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
return (false,
|
|
|
|
|
|
"The database is not available, so permissions cannot be verified. " +
|
|
|
|
|
|
$"Send the {SetupPasswordValidator.HeaderName} header to run migration.");
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
if (User?.Identity?.IsAuthenticated != true)
|
|
|
|
|
|
{
|
|
|
|
|
|
return (false, "Authentication is required to run database migration.");
|
|
|
|
|
|
}
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
if (_currentTenant.Id is not null)
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
return (false, "Database migration can only be run by a host user.");
|
|
|
|
|
|
}
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
return await CanMigrateAsync()
|
|
|
|
|
|
? (true, string.Empty)
|
|
|
|
|
|
: (false, $"The '{PlatformPermissions.Setup.Migrate}' permission is required.");
|
|
|
|
|
|
}
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
private async Task<bool> CanMigrateAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (User?.Identity?.IsAuthenticated != true)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-04-28 14:53:58 +00:00
|
|
|
|
|
2026-08-09 08:52:28 +00:00
|
|
|
|
// Host veritabanına migration uygulanır — tenant kullanıcıları tetikleyemez.
|
|
|
|
|
|
if (_currentTenant.Id is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
|
|
|
|
|
if ((await _authorizationService.AuthorizeAsync(User, PlatformPermissions.Setup.Migrate)).Succeeded)
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
return true;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
2026-08-09 08:52:28 +00:00
|
|
|
|
|
|
|
|
|
|
return User.IsInRole(LegacyAdminRoleName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Bekleyen migration sayısı
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<int?> GetPendingMigrationCountAsync(CancellationToken cancellationToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
|
|
|
|
|
|
|
|
|
|
|
var dbContext = await uow.ServiceProvider
|
|
|
|
|
|
.GetRequiredService<IDbContextProvider<PlatformDbContext>>()
|
|
|
|
|
|
.GetDbContextAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var pending = await dbContext.Database.GetPendingMigrationsAsync(cancellationToken);
|
|
|
|
|
|
return pending.Count();
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
2026-08-09 08:52:28 +00:00
|
|
|
|
catch (Exception ex)
|
2026-04-28 14:53:58 +00:00
|
|
|
|
{
|
2026-08-09 08:52:28 +00:00
|
|
|
|
// Bu bilgi opsiyonel — alınamıyorsa durum raporu yine de dönmeli.
|
|
|
|
|
|
_logger.LogDebug(ex, "Pending migration count could not be determined.");
|
|
|
|
|
|
return null;
|
2026-04-28 14:53:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|