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