using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
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;
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.
/// İki yol da üzerinden aynı migration akışını kullanır.
///
[Route("api/setup")]
public class SetupController : ControllerBase
{
///
/// Yeni izin verilmemiş kurulumlarda kilitlenmemek için tanınan geriye dönük rol.
/// rollere atandıktan sonra kaldırılabilir.
///
private const string LegacyAdminRoleName = "admin";
private const int DistributedLockTimeoutSeconds = 5;
private readonly IConfiguration _configuration;
private readonly IHostEnvironment _env;
private readonly IHostApplicationLifetime _lifetime;
private readonly IAuthorizationService _authorizationService;
private readonly ICurrentTenant _currentTenant;
private readonly IAbpDistributedLock _distributedLock;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly BackgroundWorkerInitializer _backgroundWorkerInitializer;
private readonly AbpBackgroundWorkerRecurringJobInitializer _abpBackgroundWorkerRecurringJobInitializer;
private readonly ILogger _logger;
public SetupController(
IConfiguration configuration,
IHostEnvironment env,
IHostApplicationLifetime lifetime,
IAuthorizationService authorizationService,
ICurrentTenant currentTenant,
IAbpDistributedLock distributedLock,
IUnitOfWorkManager unitOfWorkManager,
BackgroundWorkerInitializer backgroundWorkerInitializer,
AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer,
ILogger logger)
{
_configuration = configuration;
_env = env;
_lifetime = lifetime;
_authorizationService = authorizationService;
_currentTenant = currentTenant;
_distributedLock = distributedLock;
_unitOfWorkManager = unitOfWorkManager;
_backgroundWorkerInitializer = backgroundWorkerInitializer;
_abpBackgroundWorkerRecurringJobInitializer = abpBackgroundWorkerRecurringJobInitializer;
_logger = logger;
}
///
/// 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.
///
[HttpGet("application-status")]
[AllowAnonymous]
public async Task StatusAsync(CancellationToken cancellationToken)
{
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);
}
///
/// Migration + seed çalıştırır ve logları SSE olarak akıtır.
///
/// Yetki: veritabanı ayaktayken host tarafında
/// izni (veya geriye dönük olarak admin rolü); veritabanı yokken kurulum parolası.
///
[HttpPost("migrate")]
[AllowAnonymous]
public async Task MigrateAsync()
{
var (authorized, denialReason) = await AuthorizeMigrateRequestAsync();
if (!authorized)
{
_logger.LogWarning("Database migration request denied: {Reason}", denialReason);
Response.StatusCode = StatusCodes.Status403Forbidden;
Response.ContentType = "text/plain; charset=utf-8";
await Response.WriteAsync(denialReason);
return;
}
var stream = new SetupEventStream(Response);
await stream.StartAsync();
// Ç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)
{
await stream.ErrorAsync("Another migration is already in progress. Please try again later.");
await stream.DoneAsync("Failed.");
return;
}
_logger.LogWarning(
"Database migration triggered via /api/setup/migrate by user {UserId}.",
User?.Identity?.Name ?? "(setup password)");
// ApplicationStopping kullanılıyor: istemci sekmesini kapatsa da migration tamamlanır.
var outcome = await SetupMigrationRunner.ExecuteAsync(
_configuration, _env.ContentRootPath, stream, _lifetime.ApplicationStopping);
if (outcome != MigrationOutcome.Succeeded)
{
_logger.LogWarning("Database migration finished with outcome {Outcome}.", outcome);
return;
}
await stream.InfoAsync("Migration and seed completed successfully.");
// Yeni/değişen tanımlar Hangfire'a yansısın diye worker'lar tazelenir.
try
{
await stream.InfoAsync("Loading background workers into Hangfire...");
await _abpBackgroundWorkerRecurringJobInitializer.RunAsync(_lifetime.ApplicationStopping);
await _backgroundWorkerInitializer.RunAsync();
await stream.SuccessAsync("Background workers loaded successfully.");
}
catch (Exception ex)
{
_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;
}
await stream.DoneAsync("Completed.");
}
// Yetkilendirme
///
/// İki geçerli kimlik yolu vardır:
///
/// 1. X-Setup-Password header'ı — veritabanına hiç ihtiyaç duymaz, bu yüzden
/// veritabanı düşmüşken de çalışır.
/// 2. Oturum açmış host kullanıcısı + 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.
///
private async Task<(bool Authorized, string Reason)> AuthorizeMigrateRequestAsync()
{
if (Request.Headers.ContainsKey(SetupPasswordValidator.HeaderName))
{
if (!SetupPasswordValidator.IsConfigured(_configuration))
{
return (false, "Setup password is not configured on the server (Setup:MigrationPassword).");
}
return SetupPasswordValidator.IsValid(_configuration, Request.Headers)
? (true, string.Empty)
: (false, "Invalid setup password.");
}
if (!DatabaseReadinessChecker.GetStatus(_configuration).IsReady)
{
return (false,
"The database is not available, so permissions cannot be verified. " +
$"Send the {SetupPasswordValidator.HeaderName} header to run migration.");
}
if (User?.Identity?.IsAuthenticated != true)
{
return (false, "Authentication is required to run database migration.");
}
if (_currentTenant.Id is not null)
{
return (false, "Database migration can only be run by a host user.");
}
return await CanMigrateAsync()
? (true, string.Empty)
: (false, $"The '{PlatformPermissions.Setup.Migrate}' permission is required.");
}
private async Task CanMigrateAsync()
{
if (User?.Identity?.IsAuthenticated != true)
{
return false;
}
// Host veritabanına migration uygulanır — tenant kullanıcıları tetikleyemez.
if (_currentTenant.Id is not null)
{
return false;
}
if ((await _authorizationService.AuthorizeAsync(User, PlatformPermissions.Setup.Migrate)).Succeeded)
{
return true;
}
return User.IsInRole(LegacyAdminRoleName);
}
// Bekleyen migration sayısı
private async Task GetPendingMigrationCountAsync(CancellationToken cancellationToken)
{
try
{
using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
var dbContext = await uow.ServiceProvider
.GetRequiredService>()
.GetDbContextAsync();
var pending = await dbContext.Database.GetPendingMigrationsAsync(cancellationToken);
return pending.Count();
}
catch (Exception ex)
{
// Bu bilgi opsiyonel — alınamıyorsa durum raporu yine de dönmeli.
_logger.LogDebug(ex, "Pending migration count could not be determined.");
return null;
}
}
}