Skip to content

Commit

Permalink
setup fw-headless to sync crdt changes back to lexbox
Browse files Browse the repository at this point in the history
  • Loading branch information
hahn-kev committed Nov 4, 2024
1 parent 417e6a8 commit 8328eea
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 14 deletions.
27 changes: 27 additions & 0 deletions backend/FwHeadless/CrdtSyncService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using LcmCrdt;
using LcmCrdt.RemoteSync;
using SIL.Harmony;

namespace FwHeadless;

public class CrdtSyncService(
CrdtHttpSyncService httpSyncService,
IHttpClientFactory httpClientFactory,
CurrentProjectService currentProjectService,
DataModel dataModel,
ILogger<CrdtSyncService> logger)
{
public async Task Sync()
{
var lexboxRemoteServer = await httpSyncService.CreateProjectSyncable(
currentProjectService.ProjectData,
httpClientFactory.CreateClient(FwHeadlessKernel.LexboxHttpClientName)
);
var syncResults = await dataModel.SyncWith(lexboxRemoteServer);
if (!syncResults.IsSynced) throw new InvalidOperationException("Sync failed");
logger.LogInformation(
"Synced with Lexbox, Downloaded changes: {MissingFromLocal}, Uploaded changes: {MissingFromRemote}",
syncResults.MissingFromLocal.Length,
syncResults.MissingFromRemote.Length);
}
}
11 changes: 10 additions & 1 deletion backend/FwHeadless/FwHeadlessKernel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using FwDataMiniLcmBridge;
using FwLiteProjectSync;
using LcmCrdt;
using Microsoft.Extensions.Options;

namespace FwHeadless;

public static class FwHeadlessKernel
{
public const string LexboxHttpClientName = "LexboxHttpClient";
public static void AddFwHeadless(this IServiceCollection services)
{
services
Expand All @@ -20,5 +22,12 @@ public static void AddFwHeadless(this IServiceCollection services)
.AddLcmCrdtClient()
.AddFwDataBridge()
.AddFwLiteProjectSync();
services.AddScoped<CrdtSyncService>();
services.AddTransient<HttpClientAuthHandler>();
services.AddHttpClient(LexboxHttpClientName,
(provider, client) =>
{
client.BaseAddress = new Uri(provider.GetRequiredService<IOptions<FwHeadlessConfig>>().Value.LexboxUrl);
}).AddHttpMessageHandler<HttpClientAuthHandler>();
}
};
}
73 changes: 73 additions & 0 deletions backend/FwHeadless/HttpClientAuthHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Net;
using LexCore;
using LexCore.Auth;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

namespace FwHeadless;

public class HttpClientAuthHandler(IOptions<FwHeadlessConfig> config, IMemoryCache cache, ILogger<HttpClientAuthHandler> logger) : DelegatingHandler
{
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
throw new NotSupportedException("use async apis");
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var lexboxUrl = new Uri(config.Value.LexboxUrl);
if (request.RequestUri?.Authority != lexboxUrl.Authority)
{
return await base.SendAsync(request, cancellationToken);
}
try
{
await SetAuthHeader(request, cancellationToken, lexboxUrl);
}
catch (Exception e)
{
throw new InvalidOperationException("Unable to set auth header", e);
}
return await base.SendAsync(request, cancellationToken);
}

private async Task SetAuthHeader(HttpRequestMessage request, CancellationToken cancellationToken, Uri lexboxUrl)
{
var cookieContainer = new CookieContainer();
cookieContainer.Add(new Cookie(LexAuthConstants.AuthCookieName, await GetToken(cancellationToken), null, lexboxUrl.Authority));
request.Headers.Add("Cookie", cookieContainer.GetCookieHeader(lexboxUrl));
}

private async ValueTask<string> GetToken(CancellationToken cancellationToken)
{
try
{
return await cache.GetOrCreateAsync("LexboxAuthToken",
async entry =>
{
if (InnerHandler is null) throw new InvalidOperationException("InnerHandler is null");
logger.LogInformation("Getting auth token");
var client = new HttpClient(InnerHandler);
client.BaseAddress = new Uri(config.Value.LexboxUrl);
var response = await client.PostAsJsonAsync("/api/login",
new LoginRequest(config.Value.LexboxPassword, config.Value.LexboxUsername),
cancellationToken);
response.EnsureSuccessStatusCode();
var cookies = response.Headers.GetValues("Set-Cookie");
var cookieContainer = new CookieContainer();
cookieContainer.SetCookies(response.RequestMessage!.RequestUri!, cookies.Single());
var authCookie = cookieContainer.GetAllCookies()
.FirstOrDefault(c => c.Name == LexAuthConstants.AuthCookieName);
if (authCookie is null) throw new InvalidOperationException("Auth cookie not found");
entry.SetValue(authCookie.Value);
entry.AbsoluteExpiration = authCookie.Expires;
logger.LogInformation("Got auth token: {AuthToken}", authCookie.Value);
return authCookie.Value;
}) ?? throw new NullReferenceException("unable to get the login token");
}
catch (Exception e)
{
throw new InvalidOperationException("Unable to get auth token", e);
}
}
}
74 changes: 64 additions & 10 deletions backend/FwHeadless/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using FwHeadless;
using FwDataMiniLcmBridge;
using FwDataMiniLcmBridge.Api;
using FwLiteProjectSync;
using LcmCrdt;
using LcmCrdt.RemoteSync;
using LexData;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -41,7 +43,7 @@

app.Run();

static async Task<Results<Ok<CrdtFwdataProjectSyncService.SyncResult>, NotFound>> ExecuteMergeRequest(
static async Task<Results<Ok<CrdtFwdataProjectSyncService.SyncResult>, NotFound, ProblemHttpResult>> ExecuteMergeRequest(
ILogger<Program> logger,
IServiceProvider services,
SendReceiveService srService,
Expand All @@ -50,6 +52,8 @@
ProjectsService projectsService,
ProjectLookupService projectLookupService,
CrdtFwdataProjectSyncService syncService,
CrdtHttpSyncService crdtHttpSyncService,
IHttpClientFactory httpClientFactory,
Guid projectId,
bool dryRun = false)
{
Expand All @@ -67,6 +71,11 @@
return TypedResults.NotFound();
}
logger.LogInformation("Project code is {projectCode}", projectCode);
//if we can't sync with lexbox fail fast
if (!await crdtHttpSyncService.TestAuth(httpClientFactory.CreateClient(FwHeadlessKernel.LexboxHttpClientName)))
{
return TypedResults.Problem("Unable to authenticate with Lexbox");
}

var projectFolder = Path.Join(config.Value.ProjectStorageRoot, $"{projectCode}-{projectId}");
if (!Directory.Exists(projectFolder)) Directory.CreateDirectory(projectFolder);
Expand All @@ -77,6 +86,29 @@
logger.LogDebug("crdtFile: {crdtFile}", crdtFile);
logger.LogDebug("fwDataFile: {fwDataFile}", fwDataProject.FilePath);

var fwdataApi = SetupFwData(fwDataProject, srService, projectCode, logger, fwDataFactory);
var crdtProject = await SetupCrdtProject(crdtFile, projectLookupService, projectId, projectsService, projectFolder, fwdataApi.ProjectId, config.Value.LexboxUrl);

var miniLcmApi = await services.OpenCrdtProject(crdtProject);
var crdtSyncService = services.GetRequiredService<CrdtSyncService>();
await crdtSyncService.Sync();


var result = await syncService.Sync(miniLcmApi, fwdataApi, dryRun);
logger.LogInformation("Sync result, CrdtChanges: {CrdtChanges}, FwdataChanges: {FwdataChanges}", result.CrdtChanges, result.FwdataChanges);

await crdtSyncService.Sync();
var srResult2 = srService.SendReceive(fwDataProject, projectCode);
logger.LogInformation("Send/Receive result after CRDT sync: {srResult2}", srResult2.Output);
return TypedResults.Ok(result);
}

static FwDataMiniLcmApi SetupFwData(FwDataProject fwDataProject,
SendReceiveService srService,
string projectCode,
ILogger<Program> logger,
FwDataFactory fwDataFactory)
{
if (File.Exists(fwDataProject.FilePath))
{
var srResult = srService.SendReceive(fwDataProject, projectCode);
Expand All @@ -87,15 +119,37 @@
var srResult = srService.Clone(fwDataProject, projectCode);
logger.LogInformation("Send/Receive result: {srResult}", srResult.Output);
}

var fwdataApi = fwDataFactory.GetFwDataMiniLcmApi(fwDataProject, true);
var crdtProject = File.Exists(crdtFile) ?
new CrdtProject("crdt", crdtFile) :
await projectsService.CreateProject(new("crdt", SeedNewProjectData: false, Path: projectFolder, FwProjectId: fwdataApi.ProjectId));
var miniLcmApi = await services.OpenCrdtProject(crdtProject);
var result = await syncService.Sync(miniLcmApi, fwdataApi, dryRun);
logger.LogInformation("Sync result, CrdtChanges: {CrdtChanges}, FwdataChanges: {FwdataChanges}", result.CrdtChanges, result.FwdataChanges);
var srResult2 = srService.SendReceive(fwDataProject, projectCode);
logger.LogInformation("Send/Receive result after CRDT sync: {srResult2}", srResult2.Output);
return TypedResults.Ok(result);
return fwdataApi;
}

static async Task<CrdtProject> SetupCrdtProject(string crdtFile,
ProjectLookupService projectLookupService,
Guid projectId,
ProjectsService projectsService,
string projectFolder,
Guid fwProjectId,
string lexboxUrl)
{
if (File.Exists(crdtFile))
{
return new CrdtProject("crdt", crdtFile);
}
else
{
if (await projectLookupService.IsCrdtProject(projectId))
{
//todo determine what to do in this case, maybe we just download the project?
throw new InvalidOperationException("Project already exists, not sure why it's not on the server");
}
return await projectsService.CreateProject(new("crdt",
SeedNewProjectData: false,
Id: projectId,
Path: projectFolder,
FwProjectId: fwProjectId,
Domain: new Uri(lexboxUrl)));
}

}

6 changes: 6 additions & 0 deletions backend/FwHeadless/ProjectLookupService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using LexData;
using Microsoft.EntityFrameworkCore;
using SIL.Harmony.Core;

namespace FwHeadless;

Expand All @@ -13,4 +14,9 @@ public class ProjectLookupService(LexBoxDbContext dbContext)
.FirstOrDefaultAsync();
return projectCode;
}

public async Task<bool> IsCrdtProject(Guid projectId)
{
return await dbContext.Set<ServerCommit>().AnyAsync(c => c.ProjectId == projectId);
}
}
2 changes: 1 addition & 1 deletion backend/FwHeadless/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Information"
}
}
}
2 changes: 1 addition & 1 deletion backend/FwHeadless/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Information"
}
},
"AllowedHosts": "*"
Expand Down
11 changes: 11 additions & 0 deletions backend/FwLite/LcmCrdt/RemoteSync/CrdtHttpSyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public async ValueTask<bool> ShouldSync(ISyncHttp syncHttp)
{
var responseMessage = await syncHttp.HealthCheck();
_isHealthy = responseMessage.IsSuccessStatusCode;
if (!_isHealthy.Value)
{
logger.LogWarning("Health check failed, response status code {StatusCode}", responseMessage.StatusCode);
}
_lastHealthCheck = responseMessage.Headers.Date ?? DateTimeOffset.UtcNow;
}
catch (HttpRequestException e)
Expand Down Expand Up @@ -50,6 +54,13 @@ public async ValueTask<ISyncable> CreateProjectSyncable(ProjectData project, Htt
{
return new CrdtProjectSync(RestService.For<ISyncHttp>(client, refitSettings), project.Id, project.ClientId, this);
}

public async ValueTask<bool> TestAuth(HttpClient client)
{
logger.LogInformation("Testing auth, client base url: {ClientBaseUrl}", client.BaseAddress);
var syncable = await CreateProjectSyncable(new ProjectData("test", Guid.Empty, null, Guid.Empty), client);
return await syncable.ShouldSync();
}
}

internal class CrdtProjectSync(ISyncHttp restSyncClient, Guid projectId, Guid clientId, CrdtHttpSyncService httpSyncService) : ISyncable
Expand Down
2 changes: 1 addition & 1 deletion backend/LexBoxApi/Auth/AuthKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static class AuthKernel
{
public const string DefaultScheme = "JwtOrCookie";
public const string JwtOverBasicAuthUsername = "bearer";
public const string AuthCookieName = ".LexBoxAuth";
public const string AuthCookieName = LexAuthConstants.AuthCookieName;

public static void AddLexBoxAuth(IServiceCollection services,
IConfigurationRoot configuration,
Expand Down
1 change: 1 addition & 0 deletions backend/LexCore/Auth/LexAuthConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace LexCore.Auth;

public static class LexAuthConstants
{
public const string AuthCookieName = ".LexBoxAuth";
public const string RoleClaimType = "role";
public const string EmailClaimType = "email";
public const string UsernameClaimType = "user";
Expand Down

0 comments on commit 8328eea

Please sign in to comment.