основное сделано

This commit is contained in:
gorbenko.pavel
2026-08-12 15:12:11 +05:00
parent 4b83d666f1
commit 9fc089c37a
47 changed files with 2481 additions and 52 deletions
@@ -0,0 +1,70 @@
using System.Security.Cryptography.X509Certificates;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Infrastructure.SharePoint;
public interface ICertificateProvider
{
Task<X509Certificate2> GetCertificateAsync(CancellationToken cancellationToken);
}
/// <summary>
/// Сертификат для входа в SharePoint — из таблицы settings (option_name задаётся
/// в SharePointOptions.CertificateSettingName, по умолчанию 'AzurePfx').
///
/// Синглтон с кешем: разбор .pfx стоит заметно дороже запроса к базе, а сам
/// сертификат меняется раз в год. Блокировка — чтобы параллельные запросы не
/// разбирали его одновременно при холодном старте.
///
/// Собственная таблица, а не общая с api-tnsu-portal, — осознанный выбор: базы
/// у сервисов разные и связывать их не хотим. Цена — копию сертификата придётся
/// обновлять в двух местах.
/// </summary>
public sealed class DbCertificateProvider(
IServiceScopeFactory scopeFactory,
IOptions<SharePointOptions> options) : ICertificateProvider
{
private readonly SemaphoreSlim _lock = new(1, 1);
private X509Certificate2? _cached;
public async Task<X509Certificate2> GetCertificateAsync(CancellationToken cancellationToken)
{
if (_cached is not null) return _cached;
await _lock.WaitAsync(cancellationToken);
try
{
if (_cached is not null) return _cached;
var settingName = options.Value.CertificateSettingName;
// Своя область: провайдер — синглтон, а DbContext зарегистрирован
// как scoped, напрямую внедрять его сюда нельзя.
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApprovalRouteDbContext>();
var pfx = await db.Settings.AsNoTracking()
.Where(x => x.OptionName == settingName)
.Select(x => x.FileData)
.FirstOrDefaultAsync(cancellationToken);
if (pfx is null || pfx.Length == 0)
throw new InvalidOperationException(
$"Сертификат не найден: в таблице settings нет записи «{settingName}» с непустым file_data.");
_cached = X509CertificateLoader.LoadPkcs12(
pfx,
options.Value.CertificatePassword,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet);
return _cached;
}
finally
{
_lock.Release();
}
}
}