using System; using System.Collections.Generic; 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.Data; 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.TenantManagement; 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 ITenantRepository _tenantRepository; 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, ITenantRepository tenantRepository, BackgroundWorkerInitializer backgroundWorkerInitializer, AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer, ILogger logger) { _configuration = configuration; _env = env; _lifetime = lifetime; _authorizationService = authorizationService; _currentTenant = currentTenant; _distributedLock = distributedLock; _unitOfWorkManager = unitOfWorkManager; _tenantRepository = tenantRepository; _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); await WritePlainTextAsync(StatusCodes.Status403Forbidden, denialReason); return; } _logger.LogWarning( "Database migration triggered via /api/setup/migrate by user {UserId}.", User?.Identity?.Name ?? "(setup password)"); await RunMigratorAsync(environment: null, reloadWorkers: true); } /// /// Tenants ekranının Seed sekmesi: yalnızca bu tenant için migration + seed çalıştırır ve /// logları ile aynı SSE biçiminde akıtır. Yönetici e-posta/parolası /// DbMigrator'a ortam değişkeniyle geçer (). /// [HttpPost("tenants/{id:guid}/seed")] public async Task SeedTenantAsync(Guid id, [FromBody] SeedTenantInput input) { var (authorized, denialReason) = await AuthorizeTenantSeedRequestAsync(); if (!authorized) { _logger.LogWarning("Tenant seed request denied: {Reason}", denialReason); await WritePlainTextAsync(StatusCodes.Status403Forbidden, denialReason); return; } if (string.IsNullOrWhiteSpace(input.AdminEmail) || string.IsNullOrWhiteSpace(input.AdminPassword)) { await WritePlainTextAsync(StatusCodes.Status400BadRequest, "Admin email and password are required."); return; } var tenant = await _tenantRepository.FindAsync(id, includeDetails: false); if (tenant is null) { await WritePlainTextAsync(StatusCodes.Status404NotFound, $"Tenant not found: {id}"); return; } _logger.LogWarning( "Tenant seed triggered for {TenantName} ({TenantId}) by user {UserId}.", tenant.Name, tenant.Id, User?.Identity?.Name); await RunMigratorAsync(new Dictionary { [TenantSeedOptions.ToEnvironmentVariable(TenantSeedOptions.TenantIdKey)] = tenant.Id.ToString(), [TenantSeedOptions.ToEnvironmentVariable(TenantSeedOptions.AdminEmailKey)] = input.AdminEmail.Trim(), [TenantSeedOptions.ToEnvironmentVariable(TenantSeedOptions.AdminPasswordKey)] = input.AdminPassword, }, reloadWorkers: false); } /// /// 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 /// yalnızca tam migrate'te açıktır. /// private async Task RunMigratorAsync(IReadOnlyDictionary? environment, bool reloadWorkers) { 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; } // ApplicationStopping kullanılıyor: istemci sekmesini kapatsa da migration tamamlanır. var outcome = await SetupMigrationRunner.ExecuteAsync( _configuration, _env.ContentRootPath, stream, _lifetime.ApplicationStopping, environment); if (outcome != MigrationOutcome.Succeeded) { _logger.LogWarning("Database migration finished with outcome {Outcome}.", outcome); return; } if (!reloadWorkers) { await stream.SuccessAsync("Migration and seed completed successfully."); await stream.DoneAsync("Completed."); 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."); } private async Task WritePlainTextAsync(int statusCode, string message) { Response.StatusCode = statusCode; Response.ContentType = "text/plain; charset=utf-8"; await Response.WriteAsync(message); } // 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."); } /// /// Tenant seed'i tenant yönetimi işidir: host kullanıcısı + tenant güncelleme yetkisi. /// Setup parolası bu uçta geçerli değildir; veritabanı ayakta olmadan tenant da yoktur. /// private async Task<(bool Authorized, string Reason)> AuthorizeTenantSeedRequestAsync() { if (User?.Identity?.IsAuthenticated != true) { return (false, "Authentication is required to seed tenant data."); } if (_currentTenant.Id is not null) { return (false, "Tenant data can only be seeded by a host user."); } return (await _authorizationService.AuthorizeAsync(User, TenantManagementPermissions.Tenants.Update)).Succeeded ? (true, string.Empty) : (false, $"The '{TenantManagementPermissions.Tenants.Update}' 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; } } }