using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ModeusSchedule.Abstractions; namespace SfeduSchedule.Plugin.UniVerse; public sealed class UniVersePlugin : IPlugin { private const string ApiKeyScheme = "ApiKey"; public string Name => "UniVerse"; public void ConfigureServices(IServiceCollection services) { services.AddHttpClient(); } public void MapEndpoints(IEndpointRouteBuilder endpoints) { endpoints.MapGet("/plugins/universe/subid", async (string? fullname, IUniverseUserLookupService lookupService, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(fullname)) return Results.BadRequest("Query parameter 'fullname' is required."); var result = await lookupService.FindSubIdAsync(fullname, cancellationToken); return result.Status switch { UniverseUserLookupStatus.Found => Results.Text(result.SubId, "text/plain"), UniverseUserLookupStatus.NotFound => Results.NotFound(), UniverseUserLookupStatus.UpstreamError => Results.Problem( result.ErrorMessage, statusCode: StatusCodes.Status502BadGateway), _ => Results.Problem(statusCode: StatusCodes.Status500InternalServerError) }; }) .RequireAuthorization(policy => policy .AddAuthenticationSchemes(ApiKeyScheme) .RequireAuthenticatedUser()) .WithName("UniVerseGetSubId") .Produces(StatusCodes.Status200OK, "text/plain") .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status502BadGateway); } } public interface IUniverseUserLookupService { Task FindSubIdAsync(string fullName, CancellationToken cancellationToken = default); } public sealed class UniverseUserLookupService( HttpClient httpClient, IConfiguration configuration, ILogger logger) : IUniverseUserLookupService { private const string AccessControlApiPath = "access-control/api"; private const string UsersQuery = """ query($t:String!){ users(filter:{text:$t}, pager:{activePage:1,pageSize:10}) { items { id name displayName description } } } """; private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); public async Task FindSubIdAsync( string fullName, CancellationToken cancellationToken = default) { var token = configuration["TOKEN"]; if (string.IsNullOrWhiteSpace(token)) return UniverseUserLookupResult.UpstreamError("Modeus TOKEN is not configured."); var modeusUrl = configuration["MODEUS_URL"]; if (string.IsNullOrWhiteSpace(modeusUrl)) return UniverseUserLookupResult.UpstreamError("MODEUS_URL is not configured."); httpClient.BaseAddress = new Uri(modeusUrl, UriKind.Absolute); using var request = new HttpRequestMessage(HttpMethod.Post, AccessControlApiPath); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Content = JsonContent.Create( new UniverseUsersGraphQlRequest( UsersQuery, new UniverseUsersGraphQlVariables(fullName)), options: JsonOptions); using var response = await httpClient.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { logger.LogWarning( "Modeus access-control/api returned {StatusCode} while searching user by fullName.", response.StatusCode); return UniverseUserLookupResult.UpstreamError( $"Modeus access-control/api returned {(int)response.StatusCode}."); } UniverseUsersGraphQlResponse? payload; try { await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); payload = await JsonSerializer.DeserializeAsync( contentStream, JsonOptions, cancellationToken); } catch (JsonException exception) { logger.LogWarning(exception, "Unable to deserialize Modeus access-control/api response."); return UniverseUserLookupResult.UpstreamError("Modeus access-control/api returned invalid JSON."); } if (payload?.Errors is { Count: > 0 }) { logger.LogWarning( "Modeus access-control/api returned GraphQL errors: {Errors}", JsonSerializer.Serialize(payload.Errors, JsonOptions)); return UniverseUserLookupResult.UpstreamError("Modeus access-control/api returned GraphQL errors."); } var users = payload?.Data?.Users?.Items; if (users is null || users.Count == 0) return UniverseUserLookupResult.NotFound(); var subId = users[0].Name; return string.IsNullOrWhiteSpace(subId) ? UniverseUserLookupResult.NotFound() : UniverseUserLookupResult.Found(subId); } } public enum UniverseUserLookupStatus { Found, NotFound, UpstreamError }