From 409093c44415e90d0eb848f9c08b534d116329a0 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Thu, 23 Jul 2026 14:28:48 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20tansu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Directory.Packages.props | 1 + .../Tnsu.ApprovalRoute.Api/appsettings.json | 7 +++ ...frastructureServiceCollectionExtensions.cs | 10 +++++ .../Tnsu.ApprovalRoute.Infrastructure.csproj | 1 + .../TnsuPortal/HttpTnsuPortalClient.cs | 20 +++++++++ .../TnsuPortal/TnsuPortalDtos.cs | 7 +++ .../TnsuPortal/TnsuPortalOptions.cs | 10 +++++ .../TnsuPortal/TnsuPortalTokenProvider.cs | 43 +++++++++++++++++++ 8 files changed, 99 insertions(+) create mode 100644 backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs create mode 100644 backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs create mode 100644 backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs create mode 100644 backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props index 30eec15..6673224 100644 --- a/backend/Directory.Packages.props +++ b/backend/Directory.Packages.props @@ -11,6 +11,7 @@ + diff --git a/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json b/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json index 9631835..3205e20 100644 --- a/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json +++ b/backend/src/Tnsu.ApprovalRoute.Api/appsettings.json @@ -14,5 +14,12 @@ }, "Dev": { "DefaultUserEmail": "dev@tnsu.kz" + }, + "TnsuPortal": { + "BaseUrl": "https://api.tnsu.kz", + "TenantId": "", + "ClientId": "", + "ClientSecret": "", + "Scope": "" } } diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs index a006dd4..bf2a02e 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/InfrastructureServiceCollectionExtensions.cs @@ -2,7 +2,9 @@ using Extensions.Hosting.AsyncInitialization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Tnsu.ApprovalRoute.Infrastructure.Persistence; +using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal; namespace Tnsu.ApprovalRoute.Infrastructure; @@ -16,6 +18,14 @@ public static class InfrastructureServiceCollectionExtensions .UseSnakeCaseNamingConvention()); services.AddAsyncInitializer(); + services.Configure(configuration.GetSection("TnsuPortal")); + services.AddHttpClient(); + services.AddHttpClient((sp, client) => + { + var options = sp.GetRequiredService>().Value; + client.BaseAddress = new Uri(options.BaseUrl); + }); + return services; } } diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj index b5db8c5..5ea6939 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Tnsu.ApprovalRoute.Infrastructure.csproj @@ -11,6 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs new file mode 100644 index 0000000..f2f1488 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpTnsuPortalClient.cs @@ -0,0 +1,20 @@ +namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal; + +// Клиент api.tnsu.kz (api-tnsu-portal). GetEmployeeByEmailAsync и GetProjectExecutorsAsync — заглушки: +// на момент написания у api-tnsu-portal нет эндпоинта GET /Employee/ByEmail (есть только ByIIN/Manager), +// а форма ответа GET /Project/{oid} в его openapi-спеке не типизирована. Заменить на реальные вызовы, +// когда эндпоинты будут готовы — токен уже прокидывается через TnsuPortalTokenProvider. +public sealed class HttpTnsuPortalClient(HttpClient httpClient, TnsuPortalTokenProvider tokenProvider) +{ + public Task GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken) + { + return Task.FromResult(new EmployeeDirectoryInfo( + DepartmentId: Guid.Parse("00000000-0000-0000-0000-000000000001"), + CompanyId: Guid.Parse("00000000-0000-0000-0000-000000000002"))); + } + + public Task> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken) + { + return Task.FromResult>([]); + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs new file mode 100644 index 0000000..befde6a --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalDtos.cs @@ -0,0 +1,7 @@ +using Tnsu.ApprovalRoute.Domain.Enums; + +namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal; + +public sealed record EmployeeDirectoryInfo(Guid DepartmentId, Guid CompanyId); + +public sealed record ProjectExecutorInfo(string Email, string Position, ProjectRole ProjectRole); diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs new file mode 100644 index 0000000..47469a7 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalOptions.cs @@ -0,0 +1,10 @@ +namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal; + +public sealed class TnsuPortalOptions +{ + public string BaseUrl { get; set; } = "https://api.tnsu.kz"; + public string TenantId { get; set; } = string.Empty; + public string ClientId { get; set; } = string.Empty; + public string ClientSecret { get; set; } = string.Empty; + public string Scope { get; set; } = string.Empty; +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs new file mode 100644 index 0000000..6f7dfd4 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/TnsuPortalTokenProvider.cs @@ -0,0 +1,43 @@ +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Options; + +namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal; + +// Сервисный (client-credentials) токен для вызова api.tnsu.kz — не связан с токеном конечного пользователя. +public sealed class TnsuPortalTokenProvider(HttpClient httpClient, IOptions options) +{ + private string? _cachedToken; + private DateTimeOffset _expiresAt = DateTimeOffset.MinValue; + + public async Task GetAccessTokenAsync(CancellationToken cancellationToken) + { + if (_cachedToken is not null && DateTimeOffset.UtcNow < _expiresAt) + return _cachedToken; + + var opts = options.Value; + var response = await httpClient.PostAsync( + $"https://login.microsoftonline.com/{opts.TenantId}/oauth2/v2.0/token", + new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "client_credentials", + ["client_id"] = opts.ClientId, + ["client_secret"] = opts.ClientSecret, + ["scope"] = opts.Scope + }), + cancellationToken); + response.EnsureSuccessStatusCode(); + + var payload = await response.Content.ReadFromJsonAsync(cancellationToken) + ?? throw new InvalidOperationException("Не удалось получить токен для api.tnsu.kz."); + + _cachedToken = payload.AccessToken; + _expiresAt = DateTimeOffset.UtcNow.AddSeconds(payload.ExpiresIn - 60); + + return _cachedToken; + } + + private sealed record TokenResponse( + [property: JsonPropertyName("access_token")] string AccessToken, + [property: JsonPropertyName("expires_in")] int ExpiresIn); +}