From 0b9cf4d2bf6e272d4f1e45ae5f864d97c9b8e05d Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 11:35:45 +0700 Subject: [PATCH 01/12] rename LfNext solution dir to FwLite to match folders --- LexBox.sln | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LexBox.sln b/LexBox.sln index 612c0e95e..f9a8c25e0 100644 --- a/LexBox.sln +++ b/LexBox.sln @@ -21,7 +21,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing", "backend\Testing\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FixFwData", "backend\FixFwData\FixFwData.csproj", "{D7FC8B93-15A1-4D0B-9EAB-45596DB147F4}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LfNext", "LfNext", "{7B6E21C4-5AF4-4505-B7D9-59A3886C5090}" + Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FwLite", "FwLite", "{7B6E21C4-5AF4-4505-B7D9-59A3886C5090}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LfClassicData", "backend\LfClassicData\LfClassicData.csproj", "{E8BB768B-C3DC-4BE6-9B9F-82319E05AF86}" EndProject @@ -165,6 +165,7 @@ Global {5A9011D8-6EC1-4550-BDD7-AFF00DB2B921} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} {00AE5440-0E36-4488-935B-5B11301BA57D} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} {ECBA46AB-AF87-4D4D-9716-FD77264B817F} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} + {73DC604C-C501-410D-B56B-0544AD6EF1C2} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {440AE83C-6DB0-4F18-B2C1-BCD33F0645B6} From c75a5067408dc9920e735d4a1ee313f94460c2c9 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 11:36:35 +0700 Subject: [PATCH 02/12] pull code out of LocalWebApp and into new FwLiteShared project --- LexBox.sln | 6 ++ .../FwLite/FwLiteDesktop/FwLiteDesktop.csproj | 1 - .../FwLiteDesktop/FwLiteDesktopKernel.cs | 2 +- backend/FwLite/FwLiteDesktop/MauiProgram.cs | 1 - .../Auth/AuthConfig.cs | 2 +- .../Auth/AuthHelpers.cs | 31 +++------- .../Auth/AuthHelpersFactory.cs | 31 +++------- .../FwLiteShared/Auth/IRedirectUrlProvider.cs | 7 +++ .../Auth/LoggerAdapter.cs | 5 +- .../Auth/OAuthService.cs | 5 +- .../CancellationTokenExtensions.cs | 2 +- .../ChangeEventBus.cs | 24 +------- .../FwLite/FwLiteShared/FwLiteShared.csproj | 22 ++++++++ .../FwLite/FwLiteShared/FwLiteSharedKernel.cs | 56 +++++++++++++++++++ .../LexboxProjectService.cs | 11 ++-- .../Sync}/BackgroundSyncService.cs | 7 ++- .../Sync}/SyncService.cs | 9 ++- .../LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs | 23 +++++++- backend/FwLite/LocalWebApp/LocalAppKernel.cs | 39 ++----------- backend/FwLite/LocalWebApp/LocalWebApp.csproj | 8 ++- .../FwLite/LocalWebApp/LocalWebAppServer.cs | 2 +- .../FwLite/LocalWebApp/Routes/AuthRoutes.cs | 2 +- .../LocalWebApp/Routes/ProjectRoutes.cs | 4 +- .../FwLite/LocalWebApp/Routes/TestRoutes.cs | 3 +- .../Services/ServerRedirectUrlProvider.cs | 29 ++++++++++ backend/FwLite/LocalWebApp/UrlContext.cs | 2 +- 26 files changed, 203 insertions(+), 131 deletions(-) rename backend/FwLite/{LocalWebApp => FwLiteShared}/Auth/AuthConfig.cs (97%) rename backend/FwLite/{LocalWebApp => FwLiteShared}/Auth/AuthHelpers.cs (88%) rename backend/FwLite/{LocalWebApp => FwLiteShared}/Auth/AuthHelpersFactory.cs (53%) create mode 100644 backend/FwLite/FwLiteShared/Auth/IRedirectUrlProvider.cs rename backend/FwLite/{LocalWebApp => FwLiteShared}/Auth/LoggerAdapter.cs (88%) rename backend/FwLite/{LocalWebApp => FwLiteShared}/Auth/OAuthService.cs (98%) rename backend/FwLite/{LocalWebApp/Utils => FwLiteShared}/CancellationTokenExtensions.cs (89%) rename backend/FwLite/{LocalWebApp/Services => FwLiteShared}/ChangeEventBus.cs (52%) create mode 100644 backend/FwLite/FwLiteShared/FwLiteShared.csproj create mode 100644 backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs rename backend/FwLite/{LocalWebApp/Services => FwLiteShared}/LexboxProjectService.cs (97%) rename backend/FwLite/{LocalWebApp => FwLiteShared/Sync}/BackgroundSyncService.cs (95%) rename backend/FwLite/{LocalWebApp => FwLiteShared/Sync}/SyncService.cs (95%) create mode 100644 backend/FwLite/LocalWebApp/Services/ServerRedirectUrlProvider.cs diff --git a/LexBox.sln b/LexBox.sln index f9a8c25e0..cfa667dcd 100644 --- a/LexBox.sln +++ b/LexBox.sln @@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniLcm.Tests", "backend\Fw EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FwHeadless", "backend\FwHeadless\FwHeadless.csproj", "{ECBA46AB-AF87-4D4D-9716-FD77264B817F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FwLiteShared", "backend\FwLite\FwLiteShared\FwLiteShared.csproj", "{73DC604C-C501-410D-B56B-0544AD6EF1C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -145,6 +147,10 @@ Global {ECBA46AB-AF87-4D4D-9716-FD77264B817F}.Debug|Any CPU.Build.0 = Debug|Any CPU {ECBA46AB-AF87-4D4D-9716-FD77264B817F}.Release|Any CPU.ActiveCfg = Release|Any CPU {ECBA46AB-AF87-4D4D-9716-FD77264B817F}.Release|Any CPU.Build.0 = Release|Any CPU + {73DC604C-C501-410D-B56B-0544AD6EF1C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73DC604C-C501-410D-B56B-0544AD6EF1C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73DC604C-C501-410D-B56B-0544AD6EF1C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73DC604C-C501-410D-B56B-0544AD6EF1C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj b/backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj index 043dacd5d..77615b177 100644 --- a/backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj +++ b/backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj @@ -80,7 +80,6 @@ - diff --git a/backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs b/backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs index 0d7727ee2..ece707252 100644 --- a/backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs +++ b/backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs @@ -1,7 +1,7 @@ using Windows.ApplicationModel; using FwLiteDesktop.ServerBridge; +using FwLiteShared.Auth; using LcmCrdt; -using LocalWebApp.Auth; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/backend/FwLite/FwLiteDesktop/MauiProgram.cs b/backend/FwLite/FwLiteDesktop/MauiProgram.cs index 2dfbdb6fd..ef91d179f 100644 --- a/backend/FwLite/FwLiteDesktop/MauiProgram.cs +++ b/backend/FwLite/FwLiteDesktop/MauiProgram.cs @@ -1,7 +1,6 @@ using FwLiteDesktop.ServerBridge; using LcmCrdt; using LocalWebApp; -using LocalWebApp.Auth; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Maui.LifecycleEvents; diff --git a/backend/FwLite/LocalWebApp/Auth/AuthConfig.cs b/backend/FwLite/FwLiteShared/Auth/AuthConfig.cs similarity index 97% rename from backend/FwLite/LocalWebApp/Auth/AuthConfig.cs rename to backend/FwLite/FwLiteShared/Auth/AuthConfig.cs index 542aae2db..788316c6e 100644 --- a/backend/FwLite/LocalWebApp/Auth/AuthConfig.cs +++ b/backend/FwLite/FwLiteShared/Auth/AuthConfig.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using LcmCrdt; -namespace LocalWebApp.Auth; +namespace FwLiteShared.Auth; public class AuthConfig { diff --git a/backend/FwLite/LocalWebApp/Auth/AuthHelpers.cs b/backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs similarity index 88% rename from backend/FwLite/LocalWebApp/Auth/AuthHelpers.cs rename to backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs index b33738f06..a0331e3c8 100644 --- a/backend/FwLite/LocalWebApp/Auth/AuthHelpers.cs +++ b/backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs @@ -1,12 +1,11 @@ using System.Net.Http.Headers; -using System.Security.Cryptography; -using LocalWebApp.Routes; -using LocalWebApp.Services; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; -namespace LocalWebApp.Auth; +namespace FwLiteShared.Auth; /// /// when injected directly it will use the authority of the current project, to get a different authority use @@ -17,11 +16,9 @@ public class AuthHelpers { public static IReadOnlyCollection DefaultScopes { get; } = ["profile", "openid"]; public const string AuthHttpClientName = "AuthHttpClient"; - private readonly HostString _redirectHost; - private readonly bool _isRedirectHostGuess; + public string? RedirectUrl { get; } private readonly IHttpMessageHandlerFactory _httpMessageHandlerFactory; private readonly OAuthService _oAuthService; - private readonly UrlContext _urlContext; private readonly LexboxServer _lexboxServer; private readonly LexboxProjectService _lexboxProjectService; private readonly ILogger _logger; @@ -31,9 +28,8 @@ public class AuthHelpers public AuthHelpers(LoggerAdapter loggerAdapter, IHttpMessageHandlerFactory httpMessageHandlerFactory, IOptions options, - LinkGenerator linkGenerator, + IRedirectUrlProvider? redirectUrlProvider, OAuthService oAuthService, - UrlContext urlContext, LexboxServer lexboxServer, LexboxProjectService lexboxProjectService, ILogger logger, @@ -41,25 +37,19 @@ public AuthHelpers(LoggerAdapter loggerAdapter, { _httpMessageHandlerFactory = httpMessageHandlerFactory; _oAuthService = oAuthService; - _urlContext = urlContext; _lexboxServer = lexboxServer; _lexboxProjectService = lexboxProjectService; _logger = logger; - (var hostUrl, _isRedirectHostGuess) = urlContext.GetUrl(); - _redirectHost = HostString.FromUriComponent(hostUrl); - var redirectUri = options.Value.SystemWebViewLogin + RedirectUrl = options.Value.SystemWebViewLogin ? "http://localhost" //system web view will always have no path, changing this will not do anything in that case - : linkGenerator.GetUriByRouteValues(AuthRoutes.CallbackRoute, - new RouteValueDictionary(), - hostUrl.Scheme, - _redirectHost); + : redirectUrlProvider?.GetRedirectUrl() ?? throw new InvalidOperationException("No IRedirectUrlProvider configured, required for non-system web view login"); //todo configure token cache as seen here //https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache _application = PublicClientApplicationBuilder.Create(options.Value.ClientId) .WithExperimentalFeatures() .WithLogging(loggerAdapter, hostEnvironment.IsDevelopment()) .WithHttpClientFactory(new HttpClientFactoryAdapter(httpMessageHandlerFactory)) - .WithRedirectUri(redirectUri) + .WithRedirectUri(RedirectUrl) .WithOidcAuthority(lexboxServer.Authority.ToString()) .Build(); _ = MsalCacheHelper.CreateAsync(BuildCacheProperties(options.Value.CacheFileName)).ContinueWith( @@ -99,11 +89,6 @@ private static StorageCreationProperties BuildCacheProperties(string cacheFileNa return propertiesBuilder.Build(); } - public bool IsHostUrlValid() - { - return !_isRedirectHostGuess || _redirectHost == HostString.FromUriComponent(_urlContext.GetUrl().host); - } - private class HttpClientFactoryAdapter(IHttpMessageHandlerFactory httpMessageHandlerFactory) : IMsalHttpClientFactory { diff --git a/backend/FwLite/LocalWebApp/Auth/AuthHelpersFactory.cs b/backend/FwLite/FwLiteShared/Auth/AuthHelpersFactory.cs similarity index 53% rename from backend/FwLite/LocalWebApp/Auth/AuthHelpersFactory.cs rename to backend/FwLite/FwLiteShared/Auth/AuthHelpersFactory.cs index d9fc64aac..8a433b446 100644 --- a/backend/FwLite/LocalWebApp/Auth/AuthHelpersFactory.cs +++ b/backend/FwLite/FwLiteShared/Auth/AuthHelpersFactory.cs @@ -1,15 +1,15 @@ using System.Collections.Concurrent; using LcmCrdt; -using LocalWebApp.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace LocalWebApp.Auth; +namespace FwLiteShared.Auth; -public class AuthHelpersFactory( - IServiceProvider provider, - ProjectContext projectContext, +public class AuthHelpersFactory(IServiceProvider provider, IOptions options, - IHttpContextAccessor contextAccessor) + IRedirectUrlProvider? redirectUrlProvider, + ILogger logger) { private readonly ConcurrentDictionary _helpers = new(); @@ -24,9 +24,10 @@ public AuthHelpers GetHelper(LexboxServer server) static (host, arg) => ActivatorUtilities.CreateInstance(arg.provider, arg.server), (server, provider)); //an auth helper can get created based on the server host, however in development that will not be the same as the client host - //so we need to recreate it if the host is not valid - if (!helper.IsHostUrlValid()) + //so we need to recreate it if the host is not valid, this is only required when not using system web view login + if (!options.Value.SystemWebViewLogin && redirectUrlProvider is not null && redirectUrlProvider.ShouldRecreateAuthHelper(helper.RedirectUrl)) { + logger.LogInformation("Recreating auth helper with Redirect Url {RedirectUrl}", helper.RedirectUrl); _helpers.TryRemove(AuthorityKey(server), out _); return GetHelper(server); } @@ -43,18 +44,4 @@ public AuthHelpers GetHelper(ProjectData project) if (string.IsNullOrEmpty(originDomain)) throw new InvalidOperationException("No origin domain in project data"); return GetHelper(options.Value.GetServer(project)); } - - /// - /// get the auth helper for the current project, this method is used when trying to inject an AuthHelper into a service - /// - /// when not in the context of a project (typically requests include the project name in the path) - public AuthHelpers GetCurrentHelper() - { - if (projectContext.Project is null) - throw new InvalidOperationException("No current project, probably not in a request context"); - var currentProjectService = - contextAccessor.HttpContext?.RequestServices.GetRequiredService(); - if (currentProjectService is null) throw new InvalidOperationException("No current project service"); - return GetHelper(currentProjectService.ProjectData); - } } diff --git a/backend/FwLite/FwLiteShared/Auth/IRedirectUrlProvider.cs b/backend/FwLite/FwLiteShared/Auth/IRedirectUrlProvider.cs new file mode 100644 index 000000000..4c2b2541b --- /dev/null +++ b/backend/FwLite/FwLiteShared/Auth/IRedirectUrlProvider.cs @@ -0,0 +1,7 @@ +namespace FwLiteShared.Auth; + +public interface IRedirectUrlProvider +{ + string? GetRedirectUrl(); + bool ShouldRecreateAuthHelper(string? redirectUrl); +} diff --git a/backend/FwLite/LocalWebApp/Auth/LoggerAdapter.cs b/backend/FwLite/FwLiteShared/Auth/LoggerAdapter.cs similarity index 88% rename from backend/FwLite/LocalWebApp/Auth/LoggerAdapter.cs rename to backend/FwLite/FwLiteShared/Auth/LoggerAdapter.cs index ea75bdd5c..dab270f29 100644 --- a/backend/FwLite/LocalWebApp/Auth/LoggerAdapter.cs +++ b/backend/FwLite/FwLiteShared/Auth/LoggerAdapter.cs @@ -1,6 +1,7 @@ -using Microsoft.IdentityModel.Abstractions; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Abstractions; -namespace LocalWebApp.Auth; +namespace FwLiteShared.Auth; public class LoggerAdapter(ILogger logger): IIdentityLogger { diff --git a/backend/FwLite/LocalWebApp/Auth/OAuthService.cs b/backend/FwLite/FwLiteShared/Auth/OAuthService.cs similarity index 98% rename from backend/FwLite/LocalWebApp/Auth/OAuthService.cs rename to backend/FwLite/FwLiteShared/Auth/OAuthService.cs index fe9d9c94a..511a1e967 100644 --- a/backend/FwLite/LocalWebApp/Auth/OAuthService.cs +++ b/backend/FwLite/FwLiteShared/Auth/OAuthService.cs @@ -1,11 +1,12 @@ using System.Threading.Channels; using System.Web; -using LocalWebApp.Utils; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensibility; -namespace LocalWebApp.Auth; +namespace FwLiteShared.Auth; //this class is commented with a number of step comments, these are the steps in the OAuth flow //if a step comes before a method that means it awaits that call, if it comes after that means it resumes after the above await diff --git a/backend/FwLite/LocalWebApp/Utils/CancellationTokenExtensions.cs b/backend/FwLite/FwLiteShared/CancellationTokenExtensions.cs similarity index 89% rename from backend/FwLite/LocalWebApp/Utils/CancellationTokenExtensions.cs rename to backend/FwLite/FwLiteShared/CancellationTokenExtensions.cs index 35da0129f..51e0c859b 100644 --- a/backend/FwLite/LocalWebApp/Utils/CancellationTokenExtensions.cs +++ b/backend/FwLite/FwLiteShared/CancellationTokenExtensions.cs @@ -1,4 +1,4 @@ -namespace LocalWebApp.Utils; +namespace FwLiteShared; public static class CancellationTokenExtensions { diff --git a/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs b/backend/FwLite/FwLiteShared/ChangeEventBus.cs similarity index 52% rename from backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs rename to backend/FwLite/FwLiteShared/ChangeEventBus.cs index 06d331d8e..29734cc0e 100644 --- a/backend/FwLite/LocalWebApp/Services/ChangeEventBus.cs +++ b/backend/FwLite/FwLiteShared/ChangeEventBus.cs @@ -1,33 +1,13 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using LcmCrdt; -using LocalWebApp.Hubs; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Caching.Memory; using MiniLcm.Models; -namespace LocalWebApp.Services; +namespace FwLiteShared; -public class ChangeEventBus( - ProjectContext projectContext, - IHubContext hubContext, - ILogger logger, - IMemoryCache cache) +public class ChangeEventBus(ProjectContext projectContext) : IDisposable { - public IDisposable ListenForEntryChanges(string projectName, string connectionId) => - _entryUpdated - .Where(n => n.ProjectName == projectName) - .Subscribe(n => OnEntryChangedExternal(n.Entry, connectionId)); - - private void OnEntryChangedExternal(Entry e, string connectionId) - { - var currentFilter = CrdtMiniLcmApiHub.CurrentProjectFilter(cache, connectionId); - if (currentFilter.Invoke(e)) - { - _ = hubContext.Clients.Client(connectionId).OnEntryUpdated(e); - } - } private record struct ChangeNotification(Entry Entry, string ProjectName); diff --git a/backend/FwLite/FwLiteShared/FwLiteShared.csproj b/backend/FwLite/FwLiteShared/FwLiteShared.csproj new file mode 100644 index 000000000..62f7bf575 --- /dev/null +++ b/backend/FwLite/FwLiteShared/FwLiteShared.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs new file mode 100644 index 000000000..ed53f6bb5 --- /dev/null +++ b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs @@ -0,0 +1,56 @@ +using FwDataMiniLcmBridge; +using FwLiteShared.Auth; +using FwLiteShared.Sync; +using LcmCrdt; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace FwLiteShared; + +public static class FwLiteSharedKernel +{ + public static IServiceCollection AddFwLiteShared(this IServiceCollection services, IHostEnvironment environment) + { + services.AddHttpClient(); + services.AddAuthHelpers(environment); + services.AddLcmCrdtClient(); + services.AddFwDataBridge(); + + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(s => s.GetRequiredService()); + return services; + } + + private static void AddAuthHelpers(this IServiceCollection services, IHostEnvironment environment) + { + services.AddSingleton(); + services.AddScoped(CurrentAuthHelperFactory); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddOptionsWithValidateOnStart().BindConfiguration("Auth").ValidateDataAnnotations(); + services.AddSingleton(); + var httpClientBuilder = services.AddHttpClient(AuthHelpers.AuthHttpClientName); + if (environment.IsDevelopment()) + { + // Allow self-signed certificates in development + httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true + }; + }); + } + } + + private static AuthHelpers CurrentAuthHelperFactory(this IServiceProvider serviceProvider) + { + var authHelpersFactory = serviceProvider.GetRequiredService(); + var currentProjectService = serviceProvider.GetRequiredService(); + return authHelpersFactory.GetHelper(currentProjectService.ProjectData); + } +} diff --git a/backend/FwLite/LocalWebApp/Services/LexboxProjectService.cs b/backend/FwLite/FwLiteShared/LexboxProjectService.cs similarity index 97% rename from backend/FwLite/LocalWebApp/Services/LexboxProjectService.cs rename to backend/FwLite/FwLiteShared/LexboxProjectService.cs index c392ce503..92905222b 100644 --- a/backend/FwLite/LocalWebApp/Services/LexboxProjectService.cs +++ b/backend/FwLite/FwLiteShared/LexboxProjectService.cs @@ -1,11 +1,14 @@ -using LcmCrdt; -using LocalWebApp.Auth; -using Microsoft.Extensions.Options; +using System.Net.Http.Json; +using FwLiteShared.Auth; +using FwLiteShared.Sync; +using LcmCrdt; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using MiniLcm.Push; -namespace LocalWebApp.Services; +namespace FwLiteShared; public class LexboxProjectService( AuthHelpersFactory helpersFactory, diff --git a/backend/FwLite/LocalWebApp/BackgroundSyncService.cs b/backend/FwLite/FwLiteShared/Sync/BackgroundSyncService.cs similarity index 95% rename from backend/FwLite/LocalWebApp/BackgroundSyncService.cs rename to backend/FwLite/FwLiteShared/Sync/BackgroundSyncService.cs index bbe93c26c..5d5a691ec 100644 --- a/backend/FwLite/LocalWebApp/BackgroundSyncService.cs +++ b/backend/FwLite/FwLiteShared/Sync/BackgroundSyncService.cs @@ -1,9 +1,12 @@ using System.Threading.Channels; -using SIL.Harmony; using LcmCrdt; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SIL.Harmony; -namespace LocalWebApp; +namespace FwLiteShared.Sync; public class BackgroundSyncService( ProjectsService projectsService, diff --git a/backend/FwLite/LocalWebApp/SyncService.cs b/backend/FwLite/FwLiteShared/Sync/SyncService.cs similarity index 95% rename from backend/FwLite/LocalWebApp/SyncService.cs rename to backend/FwLite/FwLiteShared/Sync/SyncService.cs index 7c4be59f6..a4db72ac3 100644 --- a/backend/FwLite/LocalWebApp/SyncService.cs +++ b/backend/FwLite/FwLiteShared/Sync/SyncService.cs @@ -1,13 +1,12 @@ -using SIL.Harmony; +using FwLiteShared.Auth; using LcmCrdt; using LcmCrdt.RemoteSync; -using LocalWebApp.Auth; -using LocalWebApp.Services; +using Microsoft.Extensions.Logging; using MiniLcm; using MiniLcm.Models; -using SIL.Harmony.Entities; +using SIL.Harmony; -namespace LocalWebApp; +namespace FwLiteShared.Sync; public class SyncService( DataModel dataModel, diff --git a/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs b/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs index dcc92ae36..16d20526c 100644 --- a/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs +++ b/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs @@ -1,6 +1,9 @@ -using LcmCrdt; +using FwLiteShared; +using FwLiteShared.Sync; +using LcmCrdt; using LcmCrdt.Data; using LocalWebApp.Services; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Memory; using MiniLcm; using MiniLcm.Models; @@ -15,7 +18,9 @@ public class CrdtMiniLcmApiHub( ChangeEventBus changeEventBus, CurrentProjectService projectContext, LexboxProjectService lexboxProjectService, - IMemoryCache memoryCache) : MiniLcmApiHubBase(miniLcmApi) + IMemoryCache memoryCache, + IHubContext hubContext +) : MiniLcmApiHubBase(miniLcmApi) { public const string ProjectRouteKey = "project"; public static string ProjectGroup(string projectName) => "crdt-" + projectName; @@ -31,12 +36,24 @@ public override async Task OnConnectedAsync() await syncService.ExecuteSync(); Cleanup = [ - changeEventBus.ListenForEntryChanges(projectContext.Project.Name, Context.ConnectionId) + changeEventBus.OnEntryUpdated.Subscribe(e => OnEntryChangedExternal(e, hubContext, memoryCache, Context.ConnectionId)) ]; await lexboxProjectService.ListenForProjectChanges(projectContext.ProjectData, Context.ConnectionAborted); } + private static void OnEntryChangedExternal(Entry entry, + IHubContext hubContext, + IMemoryCache cache, + string connectionId) + { + var currentFilter = CurrentProjectFilter(cache, connectionId); + if (currentFilter.Invoke(entry)) + { + _ = hubContext.Clients.Client(connectionId).OnEntryUpdated(entry); + } + } + public override async Task OnDisconnectedAsync(Exception? exception) { await base.OnDisconnectedAsync(exception); diff --git a/backend/FwLite/LocalWebApp/LocalAppKernel.cs b/backend/FwLite/LocalWebApp/LocalAppKernel.cs index aa27e512d..bc46f7c86 100644 --- a/backend/FwLite/LocalWebApp/LocalAppKernel.cs +++ b/backend/FwLite/LocalWebApp/LocalAppKernel.cs @@ -2,9 +2,11 @@ using SIL.Harmony; using FwLiteProjectSync; using FwDataMiniLcmBridge; +using FwLiteShared; +using FwLiteShared.Auth; +using FwLiteShared.Sync; using LcmCrdt; using LocalWebApp.Services; -using LocalWebApp.Auth; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Options; @@ -17,18 +19,11 @@ public static class LocalAppKernel public static IServiceCollection AddLocalAppServices(this IServiceCollection services, IHostEnvironment environment) { services.AddHttpContextAccessor(); - services.AddHttpClient(); - services.AddAuthHelpers(environment); services.AddSingleton(); - services.AddScoped(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(s => s.GetRequiredService()); - services.AddLcmCrdtClient(); + services.AddFwLiteShared(environment); services.AddFwLiteProjectSync(); - services.AddFwDataBridge(); services.AddOptions().BindConfiguration("LocalWebApp"); @@ -44,28 +39,4 @@ public static IServiceCollection AddLocalAppServices(this IServiceCollection ser }); return services; } - - private static void AddAuthHelpers(this IServiceCollection services, IHostEnvironment environment) - { - services.AddSingleton(); - services.AddTransient(sp => sp.GetRequiredService().GetCurrentHelper()); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddOptionsWithValidateOnStart().BindConfiguration("Auth").ValidateDataAnnotations(); - services.AddSingleton(); - var httpClientBuilder = services.AddHttpClient(AuthHelpers.AuthHttpClientName); - if (environment.IsDevelopment()) - { - // Allow self-signed certificates in development - httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => - { - return new HttpClientHandler - { - ClientCertificateOptions = ClientCertificateOption.Manual, - ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true - }; - }); - } - - } } diff --git a/backend/FwLite/LocalWebApp/LocalWebApp.csproj b/backend/FwLite/LocalWebApp/LocalWebApp.csproj index 6d03a3e54..981acd9f3 100644 --- a/backend/FwLite/LocalWebApp/LocalWebApp.csproj +++ b/backend/FwLite/LocalWebApp/LocalWebApp.csproj @@ -21,9 +21,7 @@ - - @@ -38,11 +36,13 @@ icu.net.dll.config PreserveNewest + + @@ -76,4 +76,8 @@ PreserveNewest + + + + diff --git a/backend/FwLite/LocalWebApp/LocalWebAppServer.cs b/backend/FwLite/LocalWebApp/LocalWebAppServer.cs index b05a93b34..773fd1d9e 100644 --- a/backend/FwLite/LocalWebApp/LocalWebAppServer.cs +++ b/backend/FwLite/LocalWebApp/LocalWebAppServer.cs @@ -1,9 +1,9 @@ using FwDataMiniLcmBridge; using FwDataMiniLcmBridge.LcmUtils; +using FwLiteShared.Auth; using LcmCrdt; using LocalWebApp; using LocalWebApp.Hubs; -using LocalWebApp.Auth; using LocalWebApp.Routes; using LocalWebApp.Utils; using Microsoft.AspNetCore.SignalR; diff --git a/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs b/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs index 21f521741..ec850e2e4 100644 --- a/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs @@ -1,6 +1,6 @@ using System.Security.AccessControl; using System.Web; -using LocalWebApp.Auth; +using FwLiteShared.Auth; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; diff --git a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs index 62316ba1b..4de336915 100644 --- a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs @@ -1,7 +1,9 @@ using System.Text.RegularExpressions; using FwDataMiniLcmBridge; +using FwLiteShared; +using FwLiteShared.Auth; +using FwLiteShared.Sync; using LcmCrdt; -using LocalWebApp.Auth; using LocalWebApp.Hubs; using LocalWebApp.Services; using Microsoft.AspNetCore.Mvc; diff --git a/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs b/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs index 91cf0ef3d..965442f43 100644 --- a/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/TestRoutes.cs @@ -1,4 +1,5 @@ -using LocalWebApp.Hubs; +using FwLiteShared; +using LocalWebApp.Hubs; using LocalWebApp.Services; using Microsoft.OpenApi.Models; using MiniLcm; diff --git a/backend/FwLite/LocalWebApp/Services/ServerRedirectUrlProvider.cs b/backend/FwLite/LocalWebApp/Services/ServerRedirectUrlProvider.cs new file mode 100644 index 000000000..531c84c59 --- /dev/null +++ b/backend/FwLite/LocalWebApp/Services/ServerRedirectUrlProvider.cs @@ -0,0 +1,29 @@ +using FwLiteShared.Auth; +using LocalWebApp.Routes; + +namespace LocalWebApp.Services; + +public class ServerRedirectUrlProvider(LinkGenerator linkGenerator, UrlContext urlContext): IRedirectUrlProvider +{ + public string? GetRedirectUrl() + { + var (hostUrl, _) = urlContext.GetUrl(); + return RedirectUrlFromHost(hostUrl); + } + + private string? RedirectUrlFromHost(Uri hostUrl) + { + var redirectHost = HostString.FromUriComponent(hostUrl); + return linkGenerator.GetUriByRouteValues(AuthRoutes.CallbackRoute, + new RouteValueDictionary(), + hostUrl.Scheme, + redirectHost); + } + + public bool ShouldRecreateAuthHelper(string? redirectUrl) + { + var (hostUrl, guess) = urlContext.GetUrl(); + if (guess) return false; + return RedirectUrlFromHost(hostUrl) != redirectUrl; + } +} diff --git a/backend/FwLite/LocalWebApp/UrlContext.cs b/backend/FwLite/LocalWebApp/UrlContext.cs index ec261a555..bafefe88f 100644 --- a/backend/FwLite/LocalWebApp/UrlContext.cs +++ b/backend/FwLite/LocalWebApp/UrlContext.cs @@ -18,7 +18,7 @@ public class UrlContext(IServer server, IHttpContextAccessor contextAccessor) var uriBuilder = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host.Replace("127.0.0.1", "localhost"), httpContext.Request.Host.Port ?? 80); return (uriBuilder.Uri, false); } - var address = server.Features.Get()?.Addresses.FirstOrDefault() ?? throw new InvalidOperationException("No server address"); + var address = server.Features.Get()?.Addresses.FirstOrDefault(a => a.StartsWith("http:")) ?? throw new InvalidOperationException("No server address"); if (address.StartsWith("http://127.0.0.1")) address = address.Replace("http://127.0.0.1", "http://localhost"); if (address.StartsWith("https://127.0.0.1")) address = address.Replace("https://127.0.0.1", "http://localhost"); return (new Uri(address), true); From 714d22f84cc25cd6e3deb25384498b7afc5194f6 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 11:42:35 +0700 Subject: [PATCH 03/12] move import project service into FwLiteShared --- backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs | 1 + backend/FwLite/FwLiteShared/FwLiteShared.csproj | 2 ++ backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs | 4 ++++ .../Projects}/ImportFwdataService.cs | 6 ++++-- .../FwLiteShared/{ => Projects}/LexboxProjectService.cs | 2 +- backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs | 1 + backend/FwLite/LocalWebApp/LocalAppKernel.cs | 2 -- backend/FwLite/LocalWebApp/Routes/ImportRoutes.cs | 3 ++- backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs | 1 + 9 files changed, 16 insertions(+), 6 deletions(-) rename backend/FwLite/{LocalWebApp/Services => FwLiteShared/Projects}/ImportFwdataService.cs (93%) rename backend/FwLite/FwLiteShared/{ => Projects}/LexboxProjectService.cs (99%) diff --git a/backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs b/backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs index a0331e3c8..8c24014be 100644 --- a/backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs +++ b/backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs @@ -1,4 +1,5 @@ using System.Net.Http.Headers; +using FwLiteShared.Projects; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/backend/FwLite/FwLiteShared/FwLiteShared.csproj b/backend/FwLite/FwLiteShared/FwLiteShared.csproj index 62f7bf575..926c1ac3f 100644 --- a/backend/FwLite/FwLiteShared/FwLiteShared.csproj +++ b/backend/FwLite/FwLiteShared/FwLiteShared.csproj @@ -7,6 +7,7 @@ + @@ -16,6 +17,7 @@ + diff --git a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs index ed53f6bb5..7eced133a 100644 --- a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs +++ b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs @@ -1,5 +1,7 @@ using FwDataMiniLcmBridge; +using FwLiteProjectSync; using FwLiteShared.Auth; +using FwLiteShared.Projects; using FwLiteShared.Sync; using LcmCrdt; using Microsoft.Extensions.DependencyInjection; @@ -15,7 +17,9 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service services.AddAuthHelpers(environment); services.AddLcmCrdtClient(); services.AddFwDataBridge(); + services.AddFwLiteProjectSync(); + services.AddSingleton(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); diff --git a/backend/FwLite/LocalWebApp/Services/ImportFwdataService.cs b/backend/FwLite/FwLiteShared/Projects/ImportFwdataService.cs similarity index 93% rename from backend/FwLite/LocalWebApp/Services/ImportFwdataService.cs rename to backend/FwLite/FwLiteShared/Projects/ImportFwdataService.cs index 20f6ab04c..29cb6dcab 100644 --- a/backend/FwLite/LocalWebApp/Services/ImportFwdataService.cs +++ b/backend/FwLite/FwLiteShared/Projects/ImportFwdataService.cs @@ -1,11 +1,13 @@ using System.Diagnostics; -using FwLiteProjectSync; using FwDataMiniLcmBridge; +using FwLiteProjectSync; using Humanizer; using LcmCrdt; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using MiniLcm; -namespace LocalWebApp.Services; +namespace FwLiteShared.Projects; public class ImportFwdataService( ProjectsService projectsService, diff --git a/backend/FwLite/FwLiteShared/LexboxProjectService.cs b/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs similarity index 99% rename from backend/FwLite/FwLiteShared/LexboxProjectService.cs rename to backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs index 92905222b..a973566a9 100644 --- a/backend/FwLite/FwLiteShared/LexboxProjectService.cs +++ b/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Options; using MiniLcm.Push; -namespace FwLiteShared; +namespace FwLiteShared.Projects; public class LexboxProjectService( AuthHelpersFactory helpersFactory, diff --git a/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs b/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs index 16d20526c..e9725f397 100644 --- a/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs +++ b/backend/FwLite/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs @@ -1,4 +1,5 @@ using FwLiteShared; +using FwLiteShared.Projects; using FwLiteShared.Sync; using LcmCrdt; using LcmCrdt.Data; diff --git a/backend/FwLite/LocalWebApp/LocalAppKernel.cs b/backend/FwLite/LocalWebApp/LocalAppKernel.cs index bc46f7c86..4b375f243 100644 --- a/backend/FwLite/LocalWebApp/LocalAppKernel.cs +++ b/backend/FwLite/LocalWebApp/LocalAppKernel.cs @@ -21,9 +21,7 @@ public static IServiceCollection AddLocalAppServices(this IServiceCollection ser services.AddHttpContextAccessor(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddFwLiteShared(environment); - services.AddFwLiteProjectSync(); services.AddOptions().BindConfiguration("LocalWebApp"); diff --git a/backend/FwLite/LocalWebApp/Routes/ImportRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ImportRoutes.cs index 426044b4a..cc057a783 100644 --- a/backend/FwLite/LocalWebApp/Routes/ImportRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ImportRoutes.cs @@ -1,4 +1,5 @@ - using SIL.Harmony.Db; + using FwLiteShared.Projects; + using SIL.Harmony.Db; using LocalWebApp.Services; using Microsoft.OpenApi.Models; using MiniLcm; diff --git a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs index 4de336915..a70d3606f 100644 --- a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs @@ -2,6 +2,7 @@ using FwDataMiniLcmBridge; using FwLiteShared; using FwLiteShared.Auth; +using FwLiteShared.Projects; using FwLiteShared.Sync; using LcmCrdt; using LocalWebApp.Hubs; From d6df70c61ad239663217112dec74eb36d546264f Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 11:43:50 +0700 Subject: [PATCH 04/12] rename ProjectsService.cs to CrdtProjectsService.cs --- backend/FwHeadless/Program.cs | 4 ++-- .../FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs | 2 +- backend/FwLite/FwLiteProjectSync/Program.cs | 2 +- .../FwLite/FwLiteShared/Projects/ImportFwdataService.cs | 4 ++-- backend/FwLite/FwLiteShared/Sync/BackgroundSyncService.cs | 8 ++++---- backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs | 2 +- backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs | 2 +- backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs | 2 +- .../{ProjectsService.cs => CrdtProjectsService.cs} | 2 +- backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 4 ++-- backend/FwLite/LocalWebApp/LocalWebAppServer.cs | 2 +- backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs | 6 +++--- 12 files changed, 20 insertions(+), 20 deletions(-) rename backend/FwLite/LcmCrdt/{ProjectsService.cs => CrdtProjectsService.cs} (95%) diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index 95e175463..620635504 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -58,7 +58,7 @@ static async Task, NotFound, ProblemHttpResult>> ExecuteM SendReceiveService srService, IOptions config, FwDataFactory fwDataFactory, - ProjectsService projectsService, + CrdtProjectsService projectsService, ProjectLookupService projectLookupService, CrdtFwdataProjectSyncService syncService, CrdtHttpSyncService crdtHttpSyncService, @@ -137,7 +137,7 @@ static async Task SetupFwData(FwDataProject fwDataProject, static async Task SetupCrdtProject(string crdtFile, ProjectLookupService projectLookupService, Guid projectId, - ProjectsService projectsService, + CrdtProjectsService projectsService, string projectFolder, Guid fwProjectId, string lexboxUrl) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs index 0b0443e0d..4f7f2275d 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs @@ -57,7 +57,7 @@ public async Task InitializeAsync() _services.ServiceProvider.GetRequiredService>().Value.ProjectPath; if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true); Directory.CreateDirectory(crdtProjectsFolder); - var crdtProject = await _services.ServiceProvider.GetRequiredService() + var crdtProject = await _services.ServiceProvider.GetRequiredService() .CreateProject(new(_projectName, FwProjectId: FwDataApi.ProjectId, SeedNewProjectData: true)); CrdtApi = (CrdtMiniLcmApi) await _services.ServiceProvider.OpenCrdtProject(crdtProject); } diff --git a/backend/FwLite/FwLiteProjectSync/Program.cs b/backend/FwLite/FwLiteProjectSync/Program.cs index e59799d28..d587f5fb0 100644 --- a/backend/FwLite/FwLiteProjectSync/Program.cs +++ b/backend/FwLite/FwLiteProjectSync/Program.cs @@ -52,7 +52,7 @@ public static Task Main(string[] args) var services = scope.ServiceProvider; var logger = services.GetRequiredService>(); var fwdataApi = services.GetRequiredService().GetFwDataMiniLcmApi(fwProjectName, true); - var projectsService = services.GetRequiredService(); + var projectsService = services.GetRequiredService(); var crdtProject = projectsService.GetProject(crdtProjectName); if (crdtProject is null) { diff --git a/backend/FwLite/FwLiteShared/Projects/ImportFwdataService.cs b/backend/FwLite/FwLiteShared/Projects/ImportFwdataService.cs index 29cb6dcab..e78cc7a42 100644 --- a/backend/FwLite/FwLiteShared/Projects/ImportFwdataService.cs +++ b/backend/FwLite/FwLiteShared/Projects/ImportFwdataService.cs @@ -10,7 +10,7 @@ namespace FwLiteShared.Projects; public class ImportFwdataService( - ProjectsService projectsService, + CrdtProjectsService crdtProjectsService, ILogger logger, FwDataFactory fwDataFactory, FieldWorksProjectList fieldWorksProjectList, @@ -28,7 +28,7 @@ public async Task Import(string projectName) try { using var fwDataApi = fwDataFactory.GetFwDataMiniLcmApi(fwDataProject, false); - var project = await projectsService.CreateProject(new(fwDataProject.Name, + var project = await crdtProjectsService.CreateProject(new(fwDataProject.Name, SeedNewProjectData: false, FwProjectId: fwDataApi.ProjectId, AfterCreate: async (provider, project) => diff --git a/backend/FwLite/FwLiteShared/Sync/BackgroundSyncService.cs b/backend/FwLite/FwLiteShared/Sync/BackgroundSyncService.cs index 5d5a691ec..cacac6867 100644 --- a/backend/FwLite/FwLiteShared/Sync/BackgroundSyncService.cs +++ b/backend/FwLite/FwLiteShared/Sync/BackgroundSyncService.cs @@ -9,7 +9,7 @@ namespace FwLiteShared.Sync; public class BackgroundSyncService( - ProjectsService projectsService, + CrdtProjectsService crdtProjectsService, IHostApplicationLifetime applicationLifetime, ProjectContext projectContext, ILogger logger, @@ -31,7 +31,7 @@ public void TriggerSync(Guid projectId, Guid? ignoredClientId = null) return; } - var crdtProject = projectsService.GetProject(projectData.Name); + var crdtProject = crdtProjectsService.GetProject(projectData.Name); if (crdtProject is null) { logger.LogWarning("Received project update for unknown project {ProjectName}", projectData.Name); @@ -61,7 +61,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { //need to wait until application is started, otherwise Server urls will be unknown which prevents creating downstream services await StartedAsync(); - var crdtProjects = await projectsService.ListProjects(); + var crdtProjects = await crdtProjectsService.ListProjects(); foreach (var crdtProject in crdtProjects) { await SyncProject(crdtProject); @@ -79,7 +79,7 @@ private async Task SyncProject(CrdtProject crdtProject) { try { - await using var serviceScope = projectsService.CreateProjectScope(crdtProject); + await using var serviceScope = crdtProjectsService.CreateProjectScope(crdtProject); await serviceScope.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); var syncService = serviceScope.ServiceProvider.GetRequiredService(); return await syncService.ExecuteSync(); diff --git a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs index 359edff20..3173e290d 100644 --- a/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs @@ -43,7 +43,7 @@ public async Task InitializeAsync() { await _crdtDbContext.Database.OpenConnectionAsync(); //can't use ProjectsService.CreateProject because it opens and closes the db context, this would wipe out the in memory db. - await ProjectsService.InitProjectDb(_crdtDbContext, + await CrdtProjectsService.InitProjectDb(_crdtDbContext, new ProjectData("Sena 3", Guid.NewGuid(), null, Guid.NewGuid())); await _services.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); } diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs index bc93339cc..1ce5a0321 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs @@ -37,7 +37,7 @@ public async Task InitializeAsync() _crdtDbContext = _services.ServiceProvider.GetRequiredService(); await _crdtDbContext.Database.OpenConnectionAsync(); //can't use ProjectsService.CreateProject because it opens and closes the db context, this would wipe out the in memory db. - await ProjectsService.InitProjectDb(_crdtDbContext, + await CrdtProjectsService.InitProjectDb(_crdtDbContext, new ProjectData("Sena 3", Guid.NewGuid(), null, Guid.NewGuid())); await _services.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); diff --git a/backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs b/backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs index cdaa72ab0..4a7d33833 100644 --- a/backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/OpenProjectTests.cs @@ -15,7 +15,7 @@ public async Task OpeningAProjectWorks() using var host = builder.Build(); var services = host.Services; var asyncScope = services.CreateAsyncScope(); - await asyncScope.ServiceProvider.GetRequiredService() + await asyncScope.ServiceProvider.GetRequiredService() .CreateProject(new(Name: "OpeningAProjectWorks", Path: "", SeedNewProjectData: true)); var miniLcmApi = (CrdtMiniLcmApi)await asyncScope.ServiceProvider.OpenCrdtProject(new CrdtProject("OpeningAProjectWorks", sqliteConnectionString)); diff --git a/backend/FwLite/LcmCrdt/ProjectsService.cs b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs similarity index 95% rename from backend/FwLite/LcmCrdt/ProjectsService.cs rename to backend/FwLite/LcmCrdt/CrdtProjectsService.cs index 63aba27fa..d310d3349 100644 --- a/backend/FwLite/LcmCrdt/ProjectsService.cs +++ b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs @@ -7,7 +7,7 @@ namespace LcmCrdt; -public class ProjectsService(IServiceProvider provider, ProjectContext projectContext, ILogger logger, IOptions config, IMemoryCache memoryCache) +public class CrdtProjectsService(IServiceProvider provider, ProjectContext projectContext, ILogger logger, IOptions config, IMemoryCache memoryCache) { public Task ListProjects() { diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 9fd13ce3e..33020ea49 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -36,7 +36,7 @@ public static IServiceCollection AddLcmCrdtClient(this IServiceCollection servic services.AddScoped(); services.AddScoped(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddHttpClient(); services.AddSingleton(provider => new RefitSettings @@ -173,7 +173,7 @@ public static Task OpenCrdtProject(this IServiceProvider services, //this method must not be async, otherwise Setting the project scope will not work as expected. //the project is stored in the async scope, if a new scope is created in this method then it will be gone once the method returns //making the lcm api unusable - var projectsService = services.GetRequiredService(); + var projectsService = services.GetRequiredService(); projectsService.SetProjectScope(project); return LoadMiniLcmApi(services); } diff --git a/backend/FwLite/LocalWebApp/LocalWebAppServer.cs b/backend/FwLite/LocalWebApp/LocalWebAppServer.cs index 773fd1d9e..91f5ba5da 100644 --- a/backend/FwLite/LocalWebApp/LocalWebAppServer.cs +++ b/backend/FwLite/LocalWebApp/LocalWebAppServer.cs @@ -89,7 +89,7 @@ public static WebApplication SetupAppServer(WebApplicationOptions options, Actio var projectName = context.GetProjectName(); if (!string.IsNullOrWhiteSpace(projectName)) { - var projectsService = context.RequestServices.GetRequiredService(); + var projectsService = context.RequestServices.GetRequiredService(); projectsService.SetProjectScope(projectsService.GetProject(projectName) ?? throw new InvalidOperationException( $"Project {projectName} not found")); diff --git a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs index a70d3606f..0dcdb507b 100644 --- a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs @@ -37,7 +37,7 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap }); group.MapGet("/localProjects", async ( - ProjectsService projectService, + CrdtProjectsService projectService, FieldWorksProjectList fieldWorksProjectList) => { var crdtProjects = await projectService.ListProjects(); @@ -67,7 +67,7 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap return projects.Values; }); group.MapPost("/project", - async (ProjectsService projectService, string name) => + async (CrdtProjectsService projectService, string name) => { if (string.IsNullOrWhiteSpace(name)) return Results.BadRequest("Project name is required"); @@ -103,7 +103,7 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap group.MapPost("/download/crdt/{serverAuthority}/{projectId}", async (LexboxProjectService lexboxProjectService, IOptions options, - ProjectsService projectService, + CrdtProjectsService projectService, Guid projectId, [FromQuery] string projectName, string serverAuthority From d45cd5121fad06d98073de3ea00a5aa06708d326 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 12:50:14 +0700 Subject: [PATCH 05/12] create CombinedProjectsService and call new methods from ProjectRoutes --- .../FwLite/FwLiteShared/FwLiteSharedKernel.cs | 1 + .../Projects/CombinedProjectsService.cs | 84 +++++++++++++++++++ .../FwLite/FwLiteShared/Sync/SyncService.cs | 19 +++++ backend/FwLite/LcmCrdt/CrdtProjectsService.cs | 9 +- .../LocalWebApp/Routes/ProjectRoutes.cs | 74 ++-------------- 5 files changed, 118 insertions(+), 69 deletions(-) create mode 100644 backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs diff --git a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs index 7eced133a..a149661a8 100644 --- a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs +++ b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs @@ -22,6 +22,7 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service services.AddSingleton(); services.AddScoped(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(s => s.GetRequiredService()); diff --git a/backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs b/backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs new file mode 100644 index 000000000..f53b28070 --- /dev/null +++ b/backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs @@ -0,0 +1,84 @@ +using FwDataMiniLcmBridge; +using FwLiteShared.Auth; +using FwLiteShared.Sync; +using LcmCrdt; +using Microsoft.Extensions.DependencyInjection; + +namespace FwLiteShared.Projects; + +public record ProjectModel( + string Name, + bool Crdt, + bool Fwdata, + bool Lexbox = false, + string? ServerAuthority = null, + Guid? Id = null); + +public record ServerProjects(LexboxServer Server, ProjectModel[] Projects); +public class CombinedProjectsService(LexboxProjectService lexboxProjectService, CrdtProjectsService crdtProjectsService, + FieldWorksProjectList fieldWorksProjectList) +{ + public async Task RemoteProjects() + { + var lexboxServers = lexboxProjectService.Servers(); + ServerProjects[] serverProjects = new ServerProjects[lexboxServers.Length]; + for (var i = 0; i < lexboxServers.Length; i++) + { + var server = lexboxServers[i]; + var lexboxProjects = await lexboxProjectService.GetLexboxProjects(server); + serverProjects[i] = new ServerProjects(server, + lexboxProjects.Select(p => new ProjectModel(p.Name, + Crdt: p.IsCrdtProject, + Fwdata: false, + Lexbox: true, + server.Authority.Authority, + p.Id)) + .ToArray()); + } + + return serverProjects; + } + + public async Task> LocalProjects() + { + var crdtProjects = await crdtProjectsService.ListProjects(); + //todo get project Id and use that to specify the Id in the model. Also pull out server + var projects = crdtProjects.ToDictionary(p => p.Name, + p => + { + var uri = p.Data?.OriginDomain is not null ? new Uri(p.Data.OriginDomain) : null; + return new ProjectModel(p.Name, + true, + false, + p.Data?.OriginDomain is not null, + uri?.Authority, + p.Data?.Id); + }); + //basically populate projects and indicate if they are lexbox or fwdata + foreach (var p in fieldWorksProjectList.EnumerateProjects()) + { + if (projects.TryGetValue(p.Name, out var project)) + { + projects[p.Name] = project with { Fwdata = true }; + } + else + { + projects.Add(p.Name, new ProjectModel(p.Name, false, true)); + } + } + + return projects.Values; + } + + public async Task DownloadProject(Guid lexboxProjectId, string projectName, LexboxServer server) + { + await crdtProjectsService.CreateProject(new(projectName, + lexboxProjectId, + server.Authority, + async (provider, project) => + { + await provider.GetRequiredService().ExecuteSync(); + }, + SeedNewProjectData: false)); + } +} diff --git a/backend/FwLite/FwLiteShared/Sync/SyncService.cs b/backend/FwLite/FwLiteShared/Sync/SyncService.cs index a4db72ac3..23ee22035 100644 --- a/backend/FwLite/FwLiteShared/Sync/SyncService.cs +++ b/backend/FwLite/FwLiteShared/Sync/SyncService.cs @@ -1,4 +1,5 @@ using FwLiteShared.Auth; +using FwLiteShared.Projects; using LcmCrdt; using LcmCrdt.RemoteSync; using Microsoft.Extensions.Logging; @@ -14,6 +15,7 @@ public class SyncService( AuthHelpersFactory authHelpersFactory, CurrentProjectService currentProjectService, ChangeEventBus changeEventBus, + LexboxProjectService lexboxProjectService, IMiniLcmApi lexboxApi, ILogger logger) { @@ -74,4 +76,21 @@ private async Task SendNotifications(SyncResults syncResults) _ => null }; } + + public async Task UploadProject(Guid lexboxProjectId, LexboxServer server) + { + await currentProjectService.SetProjectSyncOrigin(server.Authority, lexboxProjectId); + try + { + await ExecuteSync(); + } + catch + { + await currentProjectService.SetProjectSyncOrigin(null, null); + throw; + } + + //todo maybe decouple this + lexboxProjectService.InvalidateProjectsCache(server); + } } diff --git a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs index d310d3349..213ab7ff8 100644 --- a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs +++ b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs @@ -1,4 +1,5 @@ -using SIL.Harmony; +using System.Text.RegularExpressions; +using SIL.Harmony; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -7,7 +8,7 @@ namespace LcmCrdt; -public class CrdtProjectsService(IServiceProvider provider, ProjectContext projectContext, ILogger logger, IOptions config, IMemoryCache memoryCache) +public partial class CrdtProjectsService(IServiceProvider provider, ProjectContext projectContext, ILogger logger, IOptions config, IMemoryCache memoryCache) { public Task ListProjects() { @@ -44,6 +45,7 @@ public record CreateProjectRequest( public async Task CreateProject(CreateProjectRequest request) { + if (!ProjectName().IsMatch(request.Name)) throw new InvalidOperationException("Project name is invalid"); //poor man's sanitation var name = Path.GetFileName(request.Name); var sqliteFile = Path.Combine(request.Path ?? config.Value.ProjectPath, $"{name}.sqlite"); @@ -104,4 +106,7 @@ public void SetActiveProject(string name) var project = GetProject(name) ?? throw new InvalidOperationException($"Crdt Project {name} not found"); SetProjectScope(project); } + + [GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9-_]+$")] + private static partial Regex ProjectName(); } diff --git a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs index 0dcdb507b..334167a19 100644 --- a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs @@ -20,52 +20,12 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap { var group = app.MapGroup("/api").WithOpenApi(); group.MapGet("/remoteProjects", - async ( - LexboxProjectService lexboxProjectService, - IOptions options) => + async (CombinedProjectsService combinedProjectsService) => { - var serversProjects = new Dictionary(); - foreach (var server in options.Value.LexboxServers) - { - var lexboxProjects = await lexboxProjectService.GetLexboxProjects(server); - serversProjects.Add(server.Authority.Authority, lexboxProjects.Select(p => new ProjectModel - (p.Name, Crdt: p.IsCrdtProject, Fwdata: false, Lexbox: true, server.Authority.Authority, p.Id)) - .ToArray()); - } - - return serversProjects; + return (await combinedProjectsService.RemoteProjects()).ToDictionary(p => p.Server.Authority.Authority, p => p.Projects); }); group.MapGet("/localProjects", - async ( - CrdtProjectsService projectService, - FieldWorksProjectList fieldWorksProjectList) => - { - var crdtProjects = await projectService.ListProjects(); - //todo get project Id and use that to specify the Id in the model. Also pull out server - var projects = crdtProjects.ToDictionary(p => p.Name, p => - { - var uri = p.Data?.OriginDomain is not null ? new Uri(p.Data.OriginDomain) : null; - return new ProjectModel(p.Name, - true, - false, - p.Data?.OriginDomain is not null, - uri?.Authority, - p.Data?.Id); - }); - //basically populate projects and indicate if they are lexbox or fwdata - foreach (var p in fieldWorksProjectList.EnumerateProjects()) - { - if (projects.TryGetValue(p.Name, out var project)) - { - projects[p.Name] = project with { Fwdata = true }; - } - else - { - projects.Add(p.Name, new ProjectModel(p.Name, false, true)); - } - } - return projects.Values; - }); + async (CombinedProjectsService combinedProjectsService) => await combinedProjectsService.LocalProjects()); group.MapPost("/project", async (CrdtProjectsService projectService, string name) => { @@ -87,23 +47,12 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap [FromQuery] Guid lexboxProjectId) => { var server = options.Value.GetServerByAuthority(serverAuthority); - await currentProjectService.SetProjectSyncOrigin(server.Authority, lexboxProjectId); - try - { - await syncService.ExecuteSync(); - } - catch - { - await currentProjectService.SetProjectSyncOrigin(null, null); - throw; - } - lexboxProjectService.InvalidateProjectsCache(server); + await syncService.UploadProject(lexboxProjectId, server); return TypedResults.Ok(); }); group.MapPost("/download/crdt/{serverAuthority}/{projectId}", - async (LexboxProjectService lexboxProjectService, - IOptions options, - CrdtProjectsService projectService, + async (IOptions options, + CombinedProjectsService combinedProjectsService, Guid projectId, [FromQuery] string projectName, string serverAuthority @@ -112,21 +61,12 @@ string serverAuthority if (!ProjectName().IsMatch(projectName)) return Results.BadRequest("Project name is invalid"); var server = options.Value.GetServerByAuthority(serverAuthority); - await projectService.CreateProject(new(projectName, - projectId, - server.Authority, - async (provider, project) => - { - await provider.GetRequiredService().ExecuteSync(); - }, - SeedNewProjectData: false)); + await combinedProjectsService.DownloadProject(projectId, projectName, server); return TypedResults.Ok(); }); return group; } - public record ProjectModel(string Name, bool Crdt, bool Fwdata, bool Lexbox = false, string? ServerAuthority = null, Guid? Id = null); - private static async Task AfterCreate(IServiceProvider provider, CrdtProject project) { var lexboxApi = provider.GetRequiredService(); From fe93f213dea4a80019fbcabaf08968e60bb6e189 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 13:08:27 +0700 Subject: [PATCH 06/12] move history code into HistoryService and out of LocalWebApp routes --- backend/FwLite/LcmCrdt/HistoryService.cs | 102 ++++++++++++++++++ backend/FwLite/LcmCrdt/LcmCrdt.csproj | 1 + backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | 1 + .../LocalWebApp/Routes/ActivityRoutes.cs | 33 +----- .../LocalWebApp/Routes/HistoryRoutes.cs | 62 ++--------- 5 files changed, 111 insertions(+), 88 deletions(-) create mode 100644 backend/FwLite/LcmCrdt/HistoryService.cs diff --git a/backend/FwLite/LcmCrdt/HistoryService.cs b/backend/FwLite/LcmCrdt/HistoryService.cs new file mode 100644 index 000000000..c3910fe46 --- /dev/null +++ b/backend/FwLite/LcmCrdt/HistoryService.cs @@ -0,0 +1,102 @@ +using Humanizer; +using SIL.Harmony; +using SIL.Harmony.Changes; +using SIL.Harmony.Core; +using SIL.Harmony.Db; +using LinqToDB; +using SIL.Harmony.Entities; + +namespace LcmCrdt; +public record ProjectActivity( + Guid CommitId, + DateTimeOffset Timestamp, + List> Changes) +{ + public string ChangeName => ChangeNameHelper(Changes); + + private static string ChangeNameHelper(List> changeEntities) + { + return changeEntities switch + { + { Count: 0 } => "No changes", + { Count: 1 } => changeEntities[0].Change switch + { + //todo call JsonPatchChange.Summarize() instead of this + IChange change when change.GetType().Name.StartsWith("JsonPatchChange") => "Change " + + change.EntityType.Name, + IChange change => change.GetType().Name.Humanize() + }, + { Count: var count } => $"{count} changes" + }; + } +} + +public record HistoryLineItem( + Guid CommitId, + Guid EntityId, + DateTimeOffset Timestamp, + Guid? SnapshotId, + string? ChangeName, + IObjectWithId? Entity, + string? EntityName) +{ + public HistoryLineItem( + Guid commitId, + Guid entityId, + DateTimeOffset timestamp, + Guid? snapshotId, + IChange? change, + IObjectBase? entity, + string typeName) : this(commitId, + entityId, + new DateTimeOffset(timestamp.Ticks, + TimeSpan.Zero), //todo this is a workaround for linq2db bug where it reads a date and assumes it's local when it's UTC + snapshotId, + change?.GetType().Name, + (IObjectWithId?) entity?.DbObject, + typeName) + { + } +} + +public class HistoryService(ICrdtDbContext dbContext, DataModel dataModel) +{ + public IAsyncEnumerable ProjectActivity() + { + return dbContext.Commits + .DefaultOrderDescending() + .Take(20) + .Select(c => new ProjectActivity(c.Id, c.HybridDateTime.DateTime, c.ChangeEntities)) + .AsAsyncEnumerable(); + } + + public async Task GetSnapshot(Guid snapshotId) + { + return await dbContext.Snapshots.SingleOrDefaultAsync(s => s.Id == snapshotId); + } + + public async Task GetObject(DateTime timestamp, Guid entityId) + { + //todo requires the timestamp to be exact, otherwise the change made on that timestamp will not be included + //consider using a commitId and looking up the timestamp, but then we should be exact to the commit which we aren't right now. + return await dataModel.GetAtTime(new DateTimeOffset(timestamp), entityId); + } + + public IAsyncEnumerable GetHistory(Guid entityId) + { + var query = from commit in dbContext.Commits.DefaultOrder() + from snapshot in dbContext.Snapshots.LeftJoin( + s => s.CommitId == commit.Id && s.EntityId == entityId) + from change in dbContext.Set>().LeftJoin(c => + c.CommitId == commit.Id && c.EntityId == entityId) + where snapshot.Id != null || change.EntityId != null + select new HistoryLineItem(commit.Id, + entityId, + commit.HybridDateTime.DateTime, + snapshot.Id, + change.Change, + snapshot.Entity, + snapshot.TypeName); + return query.AsAsyncEnumerable(); + } +} diff --git a/backend/FwLite/LcmCrdt/LcmCrdt.csproj b/backend/FwLite/LcmCrdt/LcmCrdt.csproj index c2382ec8a..7f1302edb 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdt.csproj +++ b/backend/FwLite/LcmCrdt/LcmCrdt.csproj @@ -13,6 +13,7 @@ + diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 33020ea49..df6f9202a 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -35,6 +35,7 @@ public static IServiceCollection AddLcmCrdtClient(this IServiceCollection servic ); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); diff --git a/backend/FwLite/LocalWebApp/Routes/ActivityRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ActivityRoutes.cs index f14f8cb1b..4d00e619e 100644 --- a/backend/FwLite/LocalWebApp/Routes/ActivityRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ActivityRoutes.cs @@ -25,38 +25,7 @@ public static IEndpointConventionBuilder MapActivities(this WebApplication app) }); return operation; }); - group.MapGet("/", - (ICrdtDbContext dbcontext) => - { - return dbcontext.Commits - .DefaultOrderDescending() - .Take(20) - .Select(c => new Activity(c.Id, c.HybridDateTime.DateTime, c.ChangeEntities)) - .AsAsyncEnumerable(); - }); + group.MapGet("/", (HistoryService historyService) => historyService.ProjectActivity()); return group; } - - private static string ChangeName(List> changeEntities) - { - return changeEntities switch - { - { Count: 0 } => "No changes", - { Count: 1 } => changeEntities[0].Change switch - { - //todo call JsonPatchChange.Summarize() instead of this - IChange change when change.GetType().Name.StartsWith("JsonPatchChange") => "Change " + change.EntityType.Name, - IChange change => change.GetType().Name.Humanize() - }, - { Count: var count } => $"{count} changes" - }; - } - - public record Activity( - Guid CommitId, - DateTimeOffset Timestamp, - List> Changes) - { - public string ChangeName => ChangeName(Changes); - } } diff --git a/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs b/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs index 4ea387061..ad76d9baa 100644 --- a/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/HistoryRoutes.cs @@ -1,4 +1,5 @@ -using SIL.Harmony; +using LcmCrdt; +using SIL.Harmony; using SIL.Harmony.Changes; using SIL.Harmony.Core; using SIL.Harmony.Db; @@ -23,63 +24,12 @@ public static IEndpointConventionBuilder MapHistoryRoutes(this WebApplication ap return operation; }); group.MapGet("/snapshot/{snapshotId:guid}", - async (Guid snapshotId, ICrdtDbContext dbcontext) => - { - return await dbcontext.Snapshots.Where(s => s.Id == snapshotId).SingleOrDefaultAsync(); - }); + async (Guid snapshotId, HistoryService historyService) => await historyService.GetSnapshot(snapshotId)); group.MapGet("/snapshot/at/{timestamp}", - async (DateTime timestamp, Guid entityId, DataModel dataModel) => - { - //todo requires the timestamp to be exact, otherwise the change made on that timestamp will not be included - //consider using a commitId and looking up the timestamp, but then we should be exact to the commit which we aren't right now. - return await dataModel.GetAtTime(new DateTimeOffset(timestamp), entityId); - }); + async (DateTime timestamp, Guid entityId, HistoryService historyService) => + await historyService.GetObject(timestamp, entityId)); group.MapGet("/{entityId}", - (Guid entityId, ICrdtDbContext dbcontext) => - { - var query = from commit in dbcontext.Commits.DefaultOrder() - from snapshot in dbcontext.Snapshots.LeftJoin( - s => s.CommitId == commit.Id && s.EntityId == entityId) - from change in dbcontext.Set>().LeftJoin(c => - c.CommitId == commit.Id && c.EntityId == entityId) - where snapshot.Id != null || change.EntityId != null - select new HistoryLineItem(commit.Id, - entityId, - commit.HybridDateTime.DateTime, - snapshot.Id, - change.Change, - snapshot.Entity, - snapshot.TypeName); - return query.ToLinqToDB().AsAsyncEnumerable(); - }); + (Guid entityId, HistoryService historyService) => historyService.GetHistory(entityId)); return group; } - - public record HistoryLineItem( - Guid CommitId, - Guid EntityId, - DateTimeOffset Timestamp, - Guid? SnapshotId, - string? ChangeName, - IObjectBase? Entity, - string? EntityName) - { - public HistoryLineItem( - Guid commitId, - Guid entityId, - DateTimeOffset timestamp, - Guid? snapshotId, - IChange? change, - IObjectBase? entity, - string typeName) : this(commitId, - entityId, - new DateTimeOffset(timestamp.Ticks, - TimeSpan.Zero), //todo this is a workaround for linq2db bug where it reads a date and assumes it's local when it's UTC - snapshotId, - change?.GetType().Name, - entity, - typeName) - { - } - } } From b1c7560e429d061b1d041227e7bc063f5e4a2668 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 13:16:35 +0700 Subject: [PATCH 07/12] fix history service query because it wasn't being executed by linq2db --- backend/FwLite/LcmCrdt/HistoryService.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/FwLite/LcmCrdt/HistoryService.cs b/backend/FwLite/LcmCrdt/HistoryService.cs index c3910fe46..e70e6c412 100644 --- a/backend/FwLite/LcmCrdt/HistoryService.cs +++ b/backend/FwLite/LcmCrdt/HistoryService.cs @@ -4,6 +4,7 @@ using SIL.Harmony.Core; using SIL.Harmony.Db; using LinqToDB; +using LinqToDB.EntityFrameworkCore; using SIL.Harmony.Entities; namespace LcmCrdt; @@ -64,10 +65,10 @@ public class HistoryService(ICrdtDbContext dbContext, DataModel dataModel) public IAsyncEnumerable ProjectActivity() { return dbContext.Commits - .DefaultOrderDescending() - .Take(20) - .Select(c => new ProjectActivity(c.Id, c.HybridDateTime.DateTime, c.ChangeEntities)) - .AsAsyncEnumerable(); + .DefaultOrderDescending() + .Take(20) + .Select(c => new ProjectActivity(c.Id, c.HybridDateTime.DateTime, c.ChangeEntities)) + .AsAsyncEnumerable(); } public async Task GetSnapshot(Guid snapshotId) @@ -84,10 +85,11 @@ public async Task GetObject(DateTime timestamp, Guid entityId) public IAsyncEnumerable GetHistory(Guid entityId) { + var changeEntities = dbContext.Set>(); var query = from commit in dbContext.Commits.DefaultOrder() from snapshot in dbContext.Snapshots.LeftJoin( s => s.CommitId == commit.Id && s.EntityId == entityId) - from change in dbContext.Set>().LeftJoin(c => + from change in changeEntities.LeftJoin(c => c.CommitId == commit.Id && c.EntityId == entityId) where snapshot.Id != null || change.EntityId != null select new HistoryLineItem(commit.Id, @@ -97,6 +99,6 @@ from change in dbContext.Set>().LeftJoin(c => change.Change, snapshot.Entity, snapshot.TypeName); - return query.AsAsyncEnumerable(); + return query.ToLinqToDB().AsAsyncEnumerable(); } } From 1d1ddb5dbe305ab49c3f935f6b2448f841c5ccf4 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 13:40:37 +0700 Subject: [PATCH 08/12] add method for creating an example CRDT project from scratch --- backend/FwLite/LcmCrdt/CrdtProjectsService.cs | 62 +++++++++++++++- .../LocalWebApp/Routes/ProjectRoutes.cs | 70 ++----------------- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs index 213ab7ff8..aadc0dbaf 100644 --- a/backend/FwLite/LcmCrdt/CrdtProjectsService.cs +++ b/backend/FwLite/LcmCrdt/CrdtProjectsService.cs @@ -43,6 +43,11 @@ public record CreateProjectRequest( string? Path = null, Guid? FwProjectId = null); + public async Task CreateExampleProject(string name) + { + return await CreateProject(new(name, AfterCreate: SampleProjectData, SeedNewProjectData: true)); + } + public async Task CreateProject(CreateProjectRequest request) { if (!ProjectName().IsMatch(request.Name)) throw new InvalidOperationException("Project name is invalid"); @@ -108,5 +113,60 @@ public void SetActiveProject(string name) } [GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9-_]+$")] - private static partial Regex ProjectName(); + public static partial Regex ProjectName(); + + public static async Task SampleProjectData(IServiceProvider provider, CrdtProject project) + { + var lexboxApi = provider.GetRequiredService(); + await lexboxApi.CreateEntry(new() + { + Id = Guid.NewGuid(), + LexemeForm = { Values = { { "en", "Apple" } } }, + CitationForm = { Values = { { "en", "Apple" } } }, + LiteralMeaning = { Values = { { "en", "Fruit" } } }, + Senses = + [ + new() + { + Gloss = { Values = { { "en", "Fruit" } } }, + Definition = + { + Values = + { + { + "en", + "fruit with red, yellow, or green skin with a sweet or tart crispy white flesh" + } + } + }, + SemanticDomains = [], + ExampleSentences = [new() { Sentence = { Values = { { "en", "We ate an apple" } } } }] + } + ] + }); + + await lexboxApi.CreateWritingSystem(WritingSystemType.Vernacular, + new() + { + Id = Guid.NewGuid(), + Type = WritingSystemType.Vernacular, + WsId = "en", + Name = "English", + Abbreviation = "en", + Font = "Arial", + Exemplars = WritingSystem.LatinExemplars + }); + + await lexboxApi.CreateWritingSystem(WritingSystemType.Analysis, + new() + { + Id = Guid.NewGuid(), + Type = WritingSystemType.Analysis, + WsId = "en", + Name = "English", + Abbreviation = "en", + Font = "Arial", + Exemplars = WritingSystem.LatinExemplars + }); + } } diff --git a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs index 334167a19..dc29e4463 100644 --- a/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs @@ -14,7 +14,7 @@ namespace LocalWebApp.Routes; -public static partial class ProjectRoutes +public static class ProjectRoutes { public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication app) { @@ -33,16 +33,14 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap return Results.BadRequest("Project name is required"); if (projectService.ProjectExists(name)) return Results.BadRequest("Project already exists"); - if (!ProjectName().IsMatch(name)) + if (!CrdtProjectsService.ProjectName().IsMatch(name)) return Results.BadRequest("Only letters, numbers, '-' and '_' are allowed"); - await projectService.CreateProject(new(name, AfterCreate: AfterCreate, SeedNewProjectData: true)); + await projectService.CreateExampleProject(name); return TypedResults.Ok(); }); group.MapPost($"/upload/crdt/{{serverAuthority}}/{{{CrdtMiniLcmApiHub.ProjectRouteKey}}}", - async (LexboxProjectService lexboxProjectService, - SyncService syncService, + async (SyncService syncService, IOptions options, - CurrentProjectService currentProjectService, string serverAuthority, [FromQuery] Guid lexboxProjectId) => { @@ -58,7 +56,7 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap string serverAuthority ) => { - if (!ProjectName().IsMatch(projectName)) + if (!CrdtProjectsService.ProjectName().IsMatch(projectName)) return Results.BadRequest("Project name is invalid"); var server = options.Value.GetServerByAuthority(serverAuthority); await combinedProjectsService.DownloadProject(projectId, projectName, server); @@ -66,62 +64,4 @@ string serverAuthority }); return group; } - - private static async Task AfterCreate(IServiceProvider provider, CrdtProject project) - { - var lexboxApi = provider.GetRequiredService(); - await lexboxApi.CreateEntry(new() - { - Id = Guid.NewGuid(), - LexemeForm = { Values = { { "en", "Apple" } } }, - CitationForm = { Values = { { "en", "Apple" } } }, - LiteralMeaning = { Values = { { "en", "Fruit" } } }, - Senses = - [ - new() - { - Gloss = { Values = { { "en", "Fruit" } } }, - Definition = - { - Values = - { - { - "en", - "fruit with red, yellow, or green skin with a sweet or tart crispy white flesh" - } - } - }, - SemanticDomains = [], - ExampleSentences = [new() { Sentence = { Values = { { "en", "We ate an apple" } } } }] - } - ] - }); - - await lexboxApi.CreateWritingSystem(WritingSystemType.Vernacular, - new() - { - Id = Guid.NewGuid(), - Type = WritingSystemType.Vernacular, - WsId = "en", - Name = "English", - Abbreviation = "en", - Font = "Arial", - Exemplars = WritingSystem.LatinExemplars - }); - - await lexboxApi.CreateWritingSystem(WritingSystemType.Analysis, - new() - { - Id = Guid.NewGuid(), - Type = WritingSystemType.Analysis, - WsId = "en", - Name = "English", - Abbreviation = "en", - Font = "Arial", - Exemplars = WritingSystem.LatinExemplars - }); - } - - [GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9-_]+$")] - private static partial Regex ProjectName(); } From c5815f26882b5bbe53d329bd0b2ded4b66fddad4 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 13:41:06 +0700 Subject: [PATCH 09/12] create AuthService and rename AuthHelpers to OAuthClient --- .../FwLite/FwLiteShared/Auth/AuthService.cs | 44 +++++++++++++++++++ .../Auth/{AuthHelpers.cs => OAuthClient.cs} | 10 ++--- ...elpersFactory.cs => OAuthClientFactory.cs} | 16 +++---- .../FwLite/FwLiteShared/Auth/OAuthService.cs | 4 +- .../FwLite/FwLiteShared/FwLiteSharedKernel.cs | 11 ++--- .../Projects/LexboxProjectService.cs | 12 ++--- .../FwLite/FwLiteShared/Sync/SyncService.cs | 4 +- .../FwLite/LocalWebApp/Routes/AuthRoutes.cs | 33 ++++++-------- 8 files changed, 86 insertions(+), 48 deletions(-) create mode 100644 backend/FwLite/FwLiteShared/Auth/AuthService.cs rename backend/FwLite/FwLiteShared/Auth/{AuthHelpers.cs => OAuthClient.cs} (96%) rename backend/FwLite/FwLiteShared/Auth/{AuthHelpersFactory.cs => OAuthClientFactory.cs} (79%) diff --git a/backend/FwLite/FwLiteShared/Auth/AuthService.cs b/backend/FwLite/FwLiteShared/Auth/AuthService.cs new file mode 100644 index 000000000..ec7621a87 --- /dev/null +++ b/backend/FwLite/FwLiteShared/Auth/AuthService.cs @@ -0,0 +1,44 @@ +using FwLiteShared.Projects; +using Microsoft.Extensions.Options; + +namespace FwLiteShared.Auth; + +public record ServerStatus(string DisplayName, bool LoggedIn, string? LoggedInAs, string? Authority); +public class AuthService(LexboxProjectService lexboxProjectService, OAuthClientFactory clientFactory, IOptions options) +{ + public IAsyncEnumerable Servers() + { + return lexboxProjectService.Servers().ToAsyncEnumerable().SelectAwait(async s => + { + var currentName = await clientFactory.GetClient(s).GetCurrentName(); + return new ServerStatus(s.DisplayName, + !string.IsNullOrEmpty(currentName), + currentName, + s.Authority.Authority); + }); + } + + public async Task SignInWebView(LexboxServer server) + { + var result = await clientFactory.GetClient(server).SignIn(string.Empty);//does nothing here + if (!result.HandledBySystemWebView) throw new InvalidOperationException("Sign in not handled by system web view"); + } + + public async Task SignInWebApp(LexboxServer server, string returnUrl) + { + var result = await clientFactory.GetClient(server).SignIn(returnUrl); + if (result.HandledBySystemWebView) throw new InvalidOperationException("Sign in handled by system web view"); + if (result.AuthUri is null) throw new InvalidOperationException("AuthUri is null"); + return result.AuthUri.ToString(); + } + + public async Task Logout(LexboxServer server) + { + await clientFactory.GetClient(server).Logout(); + } + + public async Task GetLoggedInName(LexboxServer server) + { + return await clientFactory.GetClient(server).GetCurrentName(); + } +} diff --git a/backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs b/backend/FwLite/FwLiteShared/Auth/OAuthClient.cs similarity index 96% rename from backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs rename to backend/FwLite/FwLiteShared/Auth/OAuthClient.cs index 8c24014be..c38bc078f 100644 --- a/backend/FwLite/FwLiteShared/Auth/AuthHelpers.cs +++ b/backend/FwLite/FwLiteShared/Auth/OAuthClient.cs @@ -9,11 +9,11 @@ namespace FwLiteShared.Auth; /// -/// when injected directly it will use the authority of the current project, to get a different authority use +/// when injected directly it will use the authority of the current project, to get a different authority use /// helper class for using MSAL.net /// docs: https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/overview /// -public class AuthHelpers +public class OAuthClient { public static IReadOnlyCollection DefaultScopes { get; } = ["profile", "openid"]; public const string AuthHttpClientName = "AuthHttpClient"; @@ -22,18 +22,18 @@ public class AuthHelpers private readonly OAuthService _oAuthService; private readonly LexboxServer _lexboxServer; private readonly LexboxProjectService _lexboxProjectService; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IPublicClientApplication _application; AuthenticationResult? _authResult; - public AuthHelpers(LoggerAdapter loggerAdapter, + public OAuthClient(LoggerAdapter loggerAdapter, IHttpMessageHandlerFactory httpMessageHandlerFactory, IOptions options, IRedirectUrlProvider? redirectUrlProvider, OAuthService oAuthService, LexboxServer lexboxServer, LexboxProjectService lexboxProjectService, - ILogger logger, + ILogger logger, IHostEnvironment hostEnvironment) { _httpMessageHandlerFactory = httpMessageHandlerFactory; diff --git a/backend/FwLite/FwLiteShared/Auth/AuthHelpersFactory.cs b/backend/FwLite/FwLiteShared/Auth/OAuthClientFactory.cs similarity index 79% rename from backend/FwLite/FwLiteShared/Auth/AuthHelpersFactory.cs rename to backend/FwLite/FwLiteShared/Auth/OAuthClientFactory.cs index 8a433b446..1058e5aa2 100644 --- a/backend/FwLite/FwLiteShared/Auth/AuthHelpersFactory.cs +++ b/backend/FwLite/FwLiteShared/Auth/OAuthClientFactory.cs @@ -6,22 +6,22 @@ namespace FwLiteShared.Auth; -public class AuthHelpersFactory(IServiceProvider provider, +public class OAuthClientFactory(IServiceProvider provider, IOptions options, IRedirectUrlProvider? redirectUrlProvider, - ILogger logger) + ILogger logger) { - private readonly ConcurrentDictionary _helpers = new(); + private readonly ConcurrentDictionary _helpers = new(); private string AuthorityKey(LexboxServer server) => "AuthHelper|" + server.Authority.Authority; /// /// gets an Auth Helper for the given server /// - public AuthHelpers GetHelper(LexboxServer server) + public OAuthClient GetClient(LexboxServer server) { var helper = _helpers.GetOrAdd(AuthorityKey(server), - static (host, arg) => ActivatorUtilities.CreateInstance(arg.provider, arg.server), + static (host, arg) => ActivatorUtilities.CreateInstance(arg.provider, arg.server), (server, provider)); //an auth helper can get created based on the server host, however in development that will not be the same as the client host //so we need to recreate it if the host is not valid, this is only required when not using system web view login @@ -29,7 +29,7 @@ public AuthHelpers GetHelper(LexboxServer server) { logger.LogInformation("Recreating auth helper with Redirect Url {RedirectUrl}", helper.RedirectUrl); _helpers.TryRemove(AuthorityKey(server), out _); - return GetHelper(server); + return GetClient(server); } return helper; @@ -38,10 +38,10 @@ public AuthHelpers GetHelper(LexboxServer server) /// /// get auth helper for a given project /// - public AuthHelpers GetHelper(ProjectData project) + public OAuthClient GetClient(ProjectData project) { var originDomain = project.OriginDomain; if (string.IsNullOrEmpty(originDomain)) throw new InvalidOperationException("No origin domain in project data"); - return GetHelper(options.Value.GetServer(project)); + return GetClient(options.Value.GetServer(project)); } } diff --git a/backend/FwLite/FwLiteShared/Auth/OAuthService.cs b/backend/FwLite/FwLiteShared/Auth/OAuthService.cs index 511a1e967..9c0605a6c 100644 --- a/backend/FwLite/FwLiteShared/Auth/OAuthService.cs +++ b/backend/FwLite/FwLiteShared/Auth/OAuthService.cs @@ -37,7 +37,7 @@ public async Task SubmitLoginRequest(IPublicClientApplication appl private async Task HandleSystemWebViewLogin(IPublicClientApplication application, CancellationToken cancellation) { - var result = await application.AcquireTokenInteractive(AuthHelpers.DefaultScopes) + var result = await application.AcquireTokenInteractive(OAuthClient.DefaultScopes) .WithUseEmbeddedWebView(false) .WithSystemWebViewOptions(new() { }) .ExecuteAsync(cancellation); @@ -70,7 +70,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { //todo we can get stuck here if the user doesn't complete the login, this basically bricks the login at the moment. We need a timeout or something //step 2 - var result = await loginRequest.Application.AcquireTokenInteractive(AuthHelpers.DefaultScopes) + var result = await loginRequest.Application.AcquireTokenInteractive(OAuthClient.DefaultScopes) .WithCustomWebUi(loginRequest) .ExecuteAsync(stoppingToken); //step 7, causes step 8 to resume diff --git a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs index a149661a8..fc0ca094f 100644 --- a/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs +++ b/backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs @@ -31,13 +31,14 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service private static void AddAuthHelpers(this IServiceCollection services, IHostEnvironment environment) { - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddScoped(CurrentAuthHelperFactory); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddOptionsWithValidateOnStart().BindConfiguration("Auth").ValidateDataAnnotations(); services.AddSingleton(); - var httpClientBuilder = services.AddHttpClient(AuthHelpers.AuthHttpClientName); + var httpClientBuilder = services.AddHttpClient(OAuthClient.AuthHttpClientName); if (environment.IsDevelopment()) { // Allow self-signed certificates in development @@ -52,10 +53,10 @@ private static void AddAuthHelpers(this IServiceCollection services, IHostEnviro } } - private static AuthHelpers CurrentAuthHelperFactory(this IServiceProvider serviceProvider) + private static OAuthClient CurrentAuthHelperFactory(this IServiceProvider serviceProvider) { - var authHelpersFactory = serviceProvider.GetRequiredService(); + var authHelpersFactory = serviceProvider.GetRequiredService(); var currentProjectService = serviceProvider.GetRequiredService(); - return authHelpersFactory.GetHelper(currentProjectService.ProjectData); + return authHelpersFactory.GetClient(currentProjectService.ProjectData); } } diff --git a/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs b/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs index a973566a9..731ae4435 100644 --- a/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs +++ b/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs @@ -11,7 +11,7 @@ namespace FwLiteShared.Projects; public class LexboxProjectService( - AuthHelpersFactory helpersFactory, + OAuthClientFactory clientFactory, ILogger logger, IHttpMessageHandlerFactory httpMessageHandlerFactory, BackgroundSyncService backgroundSyncService, @@ -31,7 +31,7 @@ public async Task GetLexboxProjects(LexboxServer server) async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); - var httpClient = await helpersFactory.GetHelper(server).CreateClient(); + var httpClient = await clientFactory.GetClient(server).CreateClient(); if (httpClient is null) return []; try { @@ -52,7 +52,7 @@ private static string CacheKey(LexboxServer server) public async Task GetLexboxProjectId(LexboxServer server, string code) { - var httpClient = await helpersFactory.GetHelper(server).CreateClient(); + var httpClient = await clientFactory.GetClient(server).CreateClient(); if (httpClient is null) return null; try { @@ -89,7 +89,7 @@ public async Task ListenForProjectChanges(ProjectData projectData, CancellationT return connection; } - if (await helpersFactory.GetHelper(server).GetCurrentToken() is null) + if (await clientFactory.GetClient(server).GetCurrentToken() is null) { logger.LogWarning("Unable to create signalR client, user is not authenticated to {OriginDomain}", server.Authority); @@ -106,10 +106,10 @@ public async Task ListenForProjectChanges(ProjectData projectData, CancellationT connectionOptions.HttpMessageHandlerFactory = handler => { //use a client that does not validate certs in dev - return httpMessageHandlerFactory.CreateHandler(AuthHelpers.AuthHttpClientName); + return httpMessageHandlerFactory.CreateHandler(OAuthClient.AuthHttpClientName); }; connectionOptions.AccessTokenProvider = - async () => await helpersFactory.GetHelper(server).GetCurrentToken(); + async () => await clientFactory.GetClient(server).GetCurrentToken(); }) .Build(); diff --git a/backend/FwLite/FwLiteShared/Sync/SyncService.cs b/backend/FwLite/FwLiteShared/Sync/SyncService.cs index 23ee22035..8ed5a7651 100644 --- a/backend/FwLite/FwLiteShared/Sync/SyncService.cs +++ b/backend/FwLite/FwLiteShared/Sync/SyncService.cs @@ -12,7 +12,7 @@ namespace FwLiteShared.Sync; public class SyncService( DataModel dataModel, CrdtHttpSyncService remoteSyncServiceServer, - AuthHelpersFactory authHelpersFactory, + OAuthClientFactory oAuthClientFactory, CurrentProjectService currentProjectService, ChangeEventBus changeEventBus, LexboxProjectService lexboxProjectService, @@ -29,7 +29,7 @@ public async Task ExecuteSync() return new SyncResults([], [], false); } - var httpClient = await authHelpersFactory.GetHelper(project).CreateClient(); + var httpClient = await oAuthClientFactory.GetClient(project).CreateClient(); if (httpClient is null) { logger.LogWarning( diff --git a/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs b/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs index ec850e2e4..a270c3adc 100644 --- a/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs +++ b/backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs @@ -13,28 +13,21 @@ public record ServerStatus(string DisplayName, bool LoggedIn, string? LoggedInAs public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app) { var group = app.MapGroup("/api/auth").WithOpenApi(); - group.MapGet("/servers", (IOptions options, AuthHelpersFactory factory) => - { - return options.Value.LexboxServers.ToAsyncEnumerable().SelectAwait(async s => - { - var currentName = await factory.GetHelper(s).GetCurrentName(); - return new ServerStatus(s.DisplayName, - !string.IsNullOrEmpty(currentName), - currentName, s.Authority.Authority); - }); - }); + group.MapGet("/servers", (AuthService authService) => authService.Servers()); group.MapGet("/login/{authority}", - async (AuthHelpersFactory factory, string authority, IOptions options, [FromHeader] string referer) => + async (AuthService authService, string authority, IOptions options, [FromHeader] string referer) => { var returnUrl = new Uri(referer).PathAndQuery; - var result = await factory.GetHelper(options.Value.GetServerByAuthority(authority)).SignIn(returnUrl); - if (result.HandledBySystemWebView) + //todo blazor, once we're using blazor this endpoint will only be used for non webview logins + if (options.Value.SystemWebViewLogin) { + await authService.SignInWebView(options.Value.GetServerByAuthority(authority)); return Results.Redirect(returnUrl); } - - if (result.AuthUri is null) throw new InvalidOperationException("AuthUri is null"); - return Results.Redirect(result.AuthUri.ToString()); + else + { + return Results.Redirect(await authService.SignInWebApp(options.Value.GetServerByAuthority(authority), returnUrl)); + } }); group.MapGet("/oauth-callback", async (OAuthService oAuthService, HttpContext context) => @@ -49,14 +42,14 @@ public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app) return Results.Redirect(returnUrl); }).WithName(CallbackRoute); group.MapGet("/me/{authority}", - async (AuthHelpersFactory factory, string authority, IOptions options) => + async (AuthService authService, string authority, IOptions options) => { - return new { name = await factory.GetHelper(options.Value.GetServerByAuthority(authority)).GetCurrentName() }; + return new { name = await authService.GetLoggedInName(options.Value.GetServerByAuthority(authority)) }; }); group.MapGet("/logout/{authority}", - async (AuthHelpersFactory factory, string authority, IOptions options) => + async (AuthService authService, string authority, IOptions options) => { - await factory.GetHelper(options.Value.GetServerByAuthority(authority)).Logout(); + await authService.Logout(options.Value.GetServerByAuthority(authority)); return Results.Redirect("/"); }); return group; From 190ebb3a9243af8b4db5b415ce22570e1c2093dd Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 13:47:30 +0700 Subject: [PATCH 10/12] remove some useless stuff from LocalWebApp.csproj --- backend/FwLite/LocalWebApp/LocalWebApp.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/FwLite/LocalWebApp/LocalWebApp.csproj b/backend/FwLite/LocalWebApp/LocalWebApp.csproj index 981acd9f3..ca2ff601e 100644 --- a/backend/FwLite/LocalWebApp/LocalWebApp.csproj +++ b/backend/FwLite/LocalWebApp/LocalWebApp.csproj @@ -36,7 +36,6 @@ icu.net.dll.config PreserveNewest - @@ -76,8 +75,4 @@ PreserveNewest - - - - From 7343c9f43e9b1bfd2e857a10ea81e214a662ed01 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 27 Nov 2024 13:53:44 +0700 Subject: [PATCH 11/12] change OAuthClient method name from CreateClient to CreateHttpClient --- backend/FwLite/FwLiteShared/Auth/OAuthClient.cs | 4 ++-- backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs | 4 ++-- backend/FwLite/FwLiteShared/Sync/SyncService.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/FwLite/FwLiteShared/Auth/OAuthClient.cs b/backend/FwLite/FwLiteShared/Auth/OAuthClient.cs index c38bc078f..b9affb158 100644 --- a/backend/FwLite/FwLiteShared/Auth/OAuthClient.cs +++ b/backend/FwLite/FwLiteShared/Auth/OAuthClient.cs @@ -164,10 +164,10 @@ await _application return auth?.AccessToken; } - /// + /// ] /// will return null if no auth token is available /// - public async ValueTask CreateClient() + public async ValueTask CreateHttpClient() { var auth = await GetAuth(); if (auth is null) return null; diff --git a/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs b/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs index 731ae4435..b60b2e915 100644 --- a/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs +++ b/backend/FwLite/FwLiteShared/Projects/LexboxProjectService.cs @@ -31,7 +31,7 @@ public async Task GetLexboxProjects(LexboxServer server) async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); - var httpClient = await clientFactory.GetClient(server).CreateClient(); + var httpClient = await clientFactory.GetClient(server).CreateHttpClient(); if (httpClient is null) return []; try { @@ -52,7 +52,7 @@ private static string CacheKey(LexboxServer server) public async Task GetLexboxProjectId(LexboxServer server, string code) { - var httpClient = await clientFactory.GetClient(server).CreateClient(); + var httpClient = await clientFactory.GetClient(server).CreateHttpClient(); if (httpClient is null) return null; try { diff --git a/backend/FwLite/FwLiteShared/Sync/SyncService.cs b/backend/FwLite/FwLiteShared/Sync/SyncService.cs index 8ed5a7651..e797be14d 100644 --- a/backend/FwLite/FwLiteShared/Sync/SyncService.cs +++ b/backend/FwLite/FwLiteShared/Sync/SyncService.cs @@ -29,7 +29,7 @@ public async Task ExecuteSync() return new SyncResults([], [], false); } - var httpClient = await oAuthClientFactory.GetClient(project).CreateClient(); + var httpClient = await oAuthClientFactory.GetClient(project).CreateHttpClient(); if (httpClient is null) { logger.LogWarning( From 44ed45621df5f63389a43b6adbc2756dc392f56a Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 29 Nov 2024 09:38:32 +0700 Subject: [PATCH 12/12] fix merge conflicts --- .../Fixtures/Sena3SyncFixture.cs | 2 +- backend/FwLite/FwLiteShared/FwLiteShared.csproj | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs index 3bb84e1ca..cc3551181 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs @@ -54,7 +54,7 @@ public async Task InitializeAsync() File.Move(Path.Combine(fwDataProjectPath, "sena-3.fwdata"), fwDataProject.FilePath); var fwDataMiniLcmApi = services.GetRequiredService().GetFwDataMiniLcmApi(fwDataProject, false); - var crdtProject = await services.GetRequiredService() + var crdtProject = await services.GetRequiredService() .CreateProject(new(projectName, FwProjectId: fwDataMiniLcmApi.ProjectId, SeedNewProjectData: false)); var crdtMiniLcmApi = (CrdtMiniLcmApi)await services.OpenCrdtProject(crdtProject); return (crdtMiniLcmApi, fwDataMiniLcmApi, services, cleanup); diff --git a/backend/FwLite/FwLiteShared/FwLiteShared.csproj b/backend/FwLite/FwLiteShared/FwLiteShared.csproj index 926c1ac3f..9da6832da 100644 --- a/backend/FwLite/FwLiteShared/FwLiteShared.csproj +++ b/backend/FwLite/FwLiteShared/FwLiteShared.csproj @@ -1,24 +1,21 @@  - net8.0 - enable - enable - - - - + + + + - - - + + +