Add Redis caching and AppProxyController for internal API communication
This commit is contained in:
parent
3e830d2488
commit
e6b0e4ab99
@ -10,4 +10,10 @@ services:
|
|||||||
POSTGRES_DB: gandalf_reborn
|
POSTGRES_DB: gandalf_reborn
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
token-cache:
|
||||||
|
image: redis:alpine
|
||||||
|
container_name: token-cache
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
@ -9,5 +9,13 @@
|
|||||||
<jdbc-url>jdbc:postgresql://localhost:5432/gandalf_reborn?logServerErrorDetail=True&password=root&user=root</jdbc-url>
|
<jdbc-url>jdbc:postgresql://localhost:5432/gandalf_reborn?logServerErrorDetail=True&password=root&user=root</jdbc-url>
|
||||||
<working-dir>$ProjectFileDir$</working-dir>
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
</data-source>
|
</data-source>
|
||||||
|
<data-source source="LOCAL" name="0@localhost" uuid="83278897-40f2-4e21-8bf8-d664f37bfde1">
|
||||||
|
<driver-ref>redis</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<configured-by-url>true</configured-by-url>
|
||||||
|
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:redis://localhost:6379/0</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Server.HttpSys;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Suspectus.Gandalf.Core.Abstractions.Extensions;
|
||||||
|
using Suspectus.Gandalf.Palantir.Client;
|
||||||
|
|
||||||
|
namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AppProxyController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IDistributedCache _tokenCache;
|
||||||
|
private readonly IPalantirClient _client;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public AppProxyController(IDistributedCache tokenCache, IPalantirClient client, IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_tokenCache = tokenCache;
|
||||||
|
_client = client;
|
||||||
|
_httpClient = httpClientFactory.CreateClient("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{appId}/{*path}")]
|
||||||
|
public async Task<IActionResult> Get(string appId, string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sessionCookie = Request.Cookies["MithrandirSession"];
|
||||||
|
|
||||||
|
if (sessionCookie is null)
|
||||||
|
{
|
||||||
|
return Unauthorized("Session expired.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var appInfoResult = await _client.Internal.App.GetInfo(appId);
|
||||||
|
|
||||||
|
if (!appInfoResult.IsSuccess)
|
||||||
|
{
|
||||||
|
return BadRequest(appInfoResult.Match<string?>(_ => null, e => e.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
var appInfo = appInfoResult.GetValue();
|
||||||
|
|
||||||
|
if (appInfo.IsMaster && path.StartsWith("api/internal"))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = await GetToken(sessionCookie, appId, cancellationToken);
|
||||||
|
|
||||||
|
_httpClient.BaseAddress = new Uri(appInfo.BaseAddress);
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var clientResponse = await _httpClient.GetAsync(path, cancellationToken);
|
||||||
|
|
||||||
|
if (!clientResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int)clientResponse.StatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var header in clientResponse.Content.Headers)
|
||||||
|
{
|
||||||
|
Response.Headers[header.Key] = header.Value.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response.WriteAsync(await clientResponse.Content.ReadAsStringAsync(cancellationToken), cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException e)
|
||||||
|
{
|
||||||
|
Response.StatusCode = e.StatusCode is null ? (int)HttpStatusCode.InternalServerError : (int)e.StatusCode;
|
||||||
|
await Response.WriteAsync(e.Message, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetToken(string subjectId, string appId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// TODO: Get actual token for subject and app
|
||||||
|
var accessTokenCacheKey = GetCacheKey(subjectId, appId, "access_token");
|
||||||
|
var accessToken = await _tokenCache.GetStringAsync(accessTokenCacheKey, cancellationToken);
|
||||||
|
|
||||||
|
if (accessToken is not null)
|
||||||
|
return accessToken;
|
||||||
|
|
||||||
|
const string newToken = "totallyARealToken"; // Replace with actual token retrieval logic
|
||||||
|
await _tokenCache.SetStringAsync(
|
||||||
|
accessTokenCacheKey,
|
||||||
|
newToken,
|
||||||
|
new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(1),
|
||||||
|
},
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
accessToken = newToken;
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCacheKey(string subjectId, string appId, string tokenType)
|
||||||
|
{
|
||||||
|
return $"{subjectId}:{appId}:{tokenType}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,12 +10,10 @@ namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
|
|||||||
public class AuthController : ControllerBase
|
public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IPalantirClient _client;
|
private readonly IPalantirClient _client;
|
||||||
private readonly IPalantirAuthClient _authClient;
|
|
||||||
|
|
||||||
public AuthController(IPalantirClient client, IPalantirAuthClient authClient)
|
public AuthController(IPalantirClient client)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_authClient = authClient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("[action]")]
|
[HttpGet("[action]")]
|
||||||
@ -27,7 +25,7 @@ public class AuthController : ControllerBase
|
|||||||
[HttpPost("[action]")]
|
[HttpPost("[action]")]
|
||||||
public async Task<IActionResult> Login([FromBody] LoginCommand loginCommand)
|
public async Task<IActionResult> Login([FromBody] LoginCommand loginCommand)
|
||||||
{
|
{
|
||||||
var validationResult = await _client.Auth.ValidateCredentials(new ValidateCredentialsCommand { Password = loginCommand.Password, UsernameOrEmail = loginCommand.UsernameOrEmail });
|
var validationResult = await _client.Internal.Auth.ValidateCredentials(new ValidateCredentialsCommand { Password = loginCommand.Password, UsernameOrEmail = loginCommand.UsernameOrEmail });
|
||||||
|
|
||||||
return validationResult.Match<IActionResult>(valid =>
|
return validationResult.Match<IActionResult>(valid =>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,8 +1,23 @@
|
|||||||
|
using Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
|
||||||
using Suspectus.Gandalf.Palantir.Client.Extensions;
|
using Suspectus.Gandalf.Palantir.Client.Extensions;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddPalantirClient();
|
builder.Services.AddPalantirClient();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient("test")
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
|
||||||
|
{
|
||||||
|
ServerCertificateCustomValidationCallback =
|
||||||
|
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddStackExchangeRedisCache(opt =>
|
||||||
|
{
|
||||||
|
var connectionString = builder.Configuration.GetConnectionString("Redis");
|
||||||
|
opt.Configuration = connectionString;
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2"/>
|
||||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -4,5 +4,8 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Redis": "localhost:6379"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace Suspectus.Gandalf.Core.Abstractions.DTOs;
|
||||||
|
|
||||||
|
public class AppInfo
|
||||||
|
{
|
||||||
|
public required string Id { get; set; }
|
||||||
|
public required bool IsMaster { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public required string BaseAddress { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using HashidsNet;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Suspectus.Gandalf.Core.Abstractions.DTOs;
|
||||||
|
using Suspectus.Gandalf.Palantir.Data.Database;
|
||||||
|
|
||||||
|
namespace Suspectus.Gandalf.Palantir.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/internal/[controller]")]
|
||||||
|
public class AppController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ApplicationContext _context;
|
||||||
|
private readonly IHashids _hashids;
|
||||||
|
|
||||||
|
public AppController(ApplicationContext context, IHashids hashids)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_hashids = hashids;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("info/{appId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetInfo([FromRoute][Required] string appId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var decodedId = _hashids.DecodeSingleLong(appId);
|
||||||
|
var appInfo = await _context.Apps
|
||||||
|
.Where(x => x.Id == decodedId)
|
||||||
|
.Select(x => new AppInfo
|
||||||
|
{
|
||||||
|
Id = appId,
|
||||||
|
IsMaster = x.Tenant!.IsMaster,
|
||||||
|
Name = x.Name,
|
||||||
|
BaseAddress = x.Tenant!.IsMaster ? "http://localhost:5035/" : "null"
|
||||||
|
})
|
||||||
|
.SingleOrDefaultAsync(cancellationToken: cancellationToken);
|
||||||
|
return Ok(appInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("info")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> GetInfos(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var appInfos = await _context.Apps.Select(x => new AppInfo
|
||||||
|
{
|
||||||
|
Id = _hashids.EncodeLong(x.Id!.Value),
|
||||||
|
IsMaster = x.Tenant!.IsMaster,
|
||||||
|
Name = x.Name,
|
||||||
|
BaseAddress = x.Tenant!.IsMaster ? "http://localhost:5035/" : "null"
|
||||||
|
}).ToListAsync(cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return Ok(appInfos);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ using Suspectus.Gandalf.Palantir.Contracts.Controller.Auth;
|
|||||||
namespace Suspectus.Gandalf.Palantir.Api.Controllers;
|
namespace Suspectus.Gandalf.Palantir.Api.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/internal/[controller]")]
|
||||||
public class AuthController(IMediator mediator) : ControllerBase
|
public class AuthController(IMediator mediator) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpPost("[action]")]
|
[HttpPost("[action]")]
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using HashidsNet;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Suspectus.Gandalf.Core.Abstractions.DTOs;
|
||||||
|
using Suspectus.Gandalf.Palantir.Data.Database;
|
||||||
|
|
||||||
|
namespace Suspectus.Gandalf.Palantir.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class TenantController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ApplicationContext _context;
|
||||||
|
private readonly IHashids _hashids;
|
||||||
|
|
||||||
|
public TenantController(ApplicationContext context, IHashids hashids)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_hashids = hashids;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Get(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantEntities = await _context.Tenants.ToListAsync(cancellationToken: cancellationToken);
|
||||||
|
return Ok(tenantEntities);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,15 +6,21 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddPalantirClient(this IServiceCollection services)
|
public static IServiceCollection AddPalantirClient(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddHttpClient<IPalantirAuthClient, PalantirAuthClient>(opt =>
|
services.AddHttpClient<IPalantirInternalAuthClient, PalantirInternalAuthClient>(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/auth/"); })
|
||||||
{
|
|
||||||
opt.BaseAddress = new Uri("https://localhost:7269/api/auth/");
|
|
||||||
})
|
|
||||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
|
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
|
||||||
{
|
{
|
||||||
ServerCertificateCustomValidationCallback =
|
ServerCertificateCustomValidationCallback =
|
||||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddHttpClient<IPalantirInternalAppClient, PalantirInternalAppClient>(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/app/"); })
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
|
||||||
|
{
|
||||||
|
ServerCertificateCustomValidationCallback =
|
||||||
|
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddScoped<IPalantirInternalClient, PalantirInternalClient>();
|
||||||
services.AddScoped<IPalantirClient, PalantirClient>();
|
services.AddScoped<IPalantirClient, PalantirClient>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using LanguageExt.Common;
|
using LanguageExt.Common;
|
||||||
|
using Suspectus.Gandalf.Core.Abstractions.DTOs;
|
||||||
using Suspectus.Gandalf.Core.Abstractions.Extensions;
|
using Suspectus.Gandalf.Core.Abstractions.Extensions;
|
||||||
using Suspectus.Gandalf.Palantir.Contracts.Controller.Auth;
|
using Suspectus.Gandalf.Palantir.Contracts.Controller.Auth;
|
||||||
|
|
||||||
@ -7,29 +8,58 @@ namespace Suspectus.Gandalf.Palantir.Client;
|
|||||||
|
|
||||||
public interface IPalantirClient
|
public interface IPalantirClient
|
||||||
{
|
{
|
||||||
IPalantirAuthClient Auth { get; init; }
|
IPalantirInternalClient Internal { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IPalantirAuthClient
|
public interface IPalantirInternalClient
|
||||||
|
{
|
||||||
|
IPalantirInternalAuthClient Auth { get; init; }
|
||||||
|
IPalantirInternalAppClient App { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PalantirInternalClient : IPalantirInternalClient
|
||||||
|
{
|
||||||
|
public PalantirInternalClient(
|
||||||
|
IPalantirInternalAuthClient auth,
|
||||||
|
IPalantirInternalAppClient app
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Auth = auth;
|
||||||
|
App = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPalantirInternalAuthClient Auth { get; init; }
|
||||||
|
public IPalantirInternalAppClient App { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface IPalantirInternalAuthClient
|
||||||
{
|
{
|
||||||
public Task<Result<bool>> ValidateCredentials(ValidateCredentialsCommand validateCredentialsCommand);
|
public Task<Result<bool>> ValidateCredentials(ValidateCredentialsCommand validateCredentialsCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PalantirClient : IPalantirClient
|
public interface IPalantirInternalAppClient
|
||||||
{
|
{
|
||||||
public PalantirClient(IPalantirAuthClient authClient)
|
public Task<Result<AppInfo>> GetInfo(string appId);
|
||||||
{
|
|
||||||
Auth = authClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IPalantirAuthClient Auth { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PalantirAuthClient : IPalantirAuthClient
|
public class PalantirClient : IPalantirClient
|
||||||
|
{
|
||||||
|
public PalantirClient(
|
||||||
|
IPalantirInternalClient internalClient
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Internal = internalClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPalantirInternalClient Internal { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PalantirInternalAuthClient : IPalantirInternalAuthClient
|
||||||
{
|
{
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
|
|
||||||
public PalantirAuthClient(HttpClient http)
|
public PalantirInternalAuthClient(HttpClient http)
|
||||||
{
|
{
|
||||||
_http = http;
|
_http = http;
|
||||||
}
|
}
|
||||||
@ -54,3 +84,39 @@ public class PalantirAuthClient : IPalantirAuthClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PalantirInternalAppClient : IPalantirInternalAppClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
|
||||||
|
public PalantirInternalAppClient(HttpClient http)
|
||||||
|
{
|
||||||
|
_http = http;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<AppInfo>> GetInfo(string appId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _http.GetAsync($"info/{appId}");
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return $"status: {response.StatusCode}".AsErrorResult<AppInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<AppInfo>();
|
||||||
|
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
return "InternalApp not found.".AsErrorResult<AppInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException e)
|
||||||
|
{
|
||||||
|
return $"status: {e.StatusCode} message: {e.Message}".AsErrorResult<AppInfo>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user