клиент для tansu

This commit is contained in:
gorbenko.pavel
2026-07-23 14:28:48 +05:00
parent f2a448d985
commit 409093c444
8 changed files with 99 additions and 0 deletions
+1
View File
@@ -11,6 +11,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageVersion Include="MediatR" Version="14.2.0" />
</ItemGroup>
@@ -14,5 +14,12 @@
},
"Dev": {
"DefaultUserEmail": "dev@tnsu.kz"
},
"TnsuPortal": {
"BaseUrl": "https://api.tnsu.kz",
"TenantId": "",
"ClientId": "",
"ClientSecret": "",
"Scope": ""
}
}
@@ -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<DatabaseMigrationInitializer>();
services.Configure<TnsuPortalOptions>(configuration.GetSection("TnsuPortal"));
services.AddHttpClient<TnsuPortalTokenProvider>();
services.AddHttpClient<HttpTnsuPortalClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<TnsuPortalOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
});
return services;
}
}
@@ -11,6 +11,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
</Project>
@@ -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<EmployeeDirectoryInfo> 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<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<ProjectExecutorInfo>>([]);
}
}
@@ -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);
@@ -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;
}
@@ -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<TnsuPortalOptions> options)
{
private string? _cachedToken;
private DateTimeOffset _expiresAt = DateTimeOffset.MinValue;
public async Task<string> 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<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = opts.ClientId,
["client_secret"] = opts.ClientSecret,
["scope"] = opts.Scope
}),
cancellationToken);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<TokenResponse>(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);
}