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