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