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