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
|
||||
ports:
|
||||
- "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>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</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>
|
||||
</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
|
||||
{
|
||||
private readonly IPalantirClient _client;
|
||||
private readonly IPalantirAuthClient _authClient;
|
||||
|
||||
public AuthController(IPalantirClient client, IPalantirAuthClient authClient)
|
||||
public AuthController(IPalantirClient client)
|
||||
{
|
||||
_client = client;
|
||||
_authClient = authClient;
|
||||
}
|
||||
|
||||
[HttpGet("[action]")]
|
||||
@ -27,7 +25,7 @@ public class AuthController : ControllerBase
|
||||
[HttpPost("[action]")]
|
||||
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 =>
|
||||
{
|
||||
|
||||
@ -1,8 +1,23 @@
|
||||
using Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
|
||||
using Suspectus.Gandalf.Palantir.Client.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
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.AddOpenApi();
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2"/>
|
||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -4,5 +4,8 @@
|
||||
"Default": "Information",
|
||||
"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;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/internal/[controller]")]
|
||||
public class AuthController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[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)
|
||||
{
|
||||
services.AddHttpClient<IPalantirAuthClient, PalantirAuthClient>(opt =>
|
||||
{
|
||||
opt.BaseAddress = new Uri("https://localhost:7269/api/auth/");
|
||||
})
|
||||
services.AddHttpClient<IPalantirInternalAuthClient, PalantirInternalAuthClient>(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/auth/"); })
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
|
||||
{
|
||||
ServerCertificateCustomValidationCallback =
|
||||
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>();
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Net.Http.Json;
|
||||
using LanguageExt.Common;
|
||||
using Suspectus.Gandalf.Core.Abstractions.DTOs;
|
||||
using Suspectus.Gandalf.Core.Abstractions.Extensions;
|
||||
using Suspectus.Gandalf.Palantir.Contracts.Controller.Auth;
|
||||
|
||||
@ -7,29 +8,58 @@ namespace Suspectus.Gandalf.Palantir.Client;
|
||||
|
||||
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 interface IPalantirInternalAppClient
|
||||
{
|
||||
public Task<Result<AppInfo>> GetInfo(string appId);
|
||||
}
|
||||
|
||||
public class PalantirClient : IPalantirClient
|
||||
{
|
||||
public PalantirClient(IPalantirAuthClient authClient)
|
||||
public PalantirClient(
|
||||
IPalantirInternalClient internalClient
|
||||
)
|
||||
{
|
||||
Auth = authClient;
|
||||
Internal = internalClient;
|
||||
}
|
||||
|
||||
public IPalantirAuthClient Auth { get; init; }
|
||||
public IPalantirInternalClient Internal { get; init; }
|
||||
}
|
||||
|
||||
public class PalantirAuthClient : IPalantirAuthClient
|
||||
public class PalantirInternalAuthClient : IPalantirInternalAuthClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public PalantirAuthClient(HttpClient http)
|
||||
public PalantirInternalAuthClient(HttpClient 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