Skip to content

Commit

Permalink
create fw lite shared (#1285)
Browse files Browse the repository at this point in the history
* rename LfNext solution dir to FwLite to match folders

* pull code out of LocalWebApp and into new FwLiteShared project

* move import project service into FwLiteShared

* rename ProjectsService.cs to CrdtProjectsService.cs

* create CombinedProjectsService and call new methods from ProjectRoutes

* move history code into HistoryService and out of LocalWebApp routes

* fix history service query because it wasn't being executed by linq2db

* add method for creating an example CRDT project from scratch

* create AuthService and rename AuthHelpers to OAuthClient
  • Loading branch information
hahn-kev authored Nov 29, 2024
1 parent c6d233f commit 8b4f158
Show file tree
Hide file tree
Showing 44 changed files with 639 additions and 452 deletions.
9 changes: 8 additions & 1 deletion LexBox.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -165,6 +171,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}
Expand Down
4 changes: 2 additions & 2 deletions backend/FwHeadless/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ static async Task<Results<Ok<SyncResult>, NotFound, ProblemHttpResult>> ExecuteM
SendReceiveService srService,
IOptions<FwHeadlessConfig> config,
FwDataFactory fwDataFactory,
ProjectsService projectsService,
CrdtProjectsService projectsService,
ProjectLookupService projectLookupService,
CrdtFwdataProjectSyncService syncService,
CrdtHttpSyncService crdtHttpSyncService,
Expand Down Expand Up @@ -137,7 +137,7 @@ static async Task<FwDataMiniLcmApi> SetupFwData(FwDataProject fwDataProject,
static async Task<CrdtProject> SetupCrdtProject(string crdtFile,
ProjectLookupService projectLookupService,
Guid projectId,
ProjectsService projectsService,
CrdtProjectsService projectsService,
string projectFolder,
Guid fwProjectId,
string lexboxUrl)
Expand Down
1 change: 0 additions & 1 deletion backend/FwLite/FwLiteDesktop/FwLiteDesktop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" />
<PackageReference Include="NReco.Logging.File" Version="1.2.1" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/FwLiteDesktop/FwLiteDesktopKernel.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
1 change: 0 additions & 1 deletion backend/FwLite/FwLiteDesktop/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public async Task InitializeAsync()
File.Move(Path.Combine(fwDataProjectPath, "sena-3.fwdata"), fwDataProject.FilePath);
var fwDataMiniLcmApi = services.GetRequiredService<FwDataFactory>().GetFwDataMiniLcmApi(fwDataProject, false);

var crdtProject = await services.GetRequiredService<ProjectsService>()
var crdtProject = await services.GetRequiredService<CrdtProjectsService>()
.CreateProject(new(projectName, FwProjectId: fwDataMiniLcmApi.ProjectId, SeedNewProjectData: false));
var crdtMiniLcmApi = (CrdtMiniLcmApi)await services.OpenCrdtProject(crdtProject);
return (crdtMiniLcmApi, fwDataMiniLcmApi, services, cleanup);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task InitializeAsync()
_services.ServiceProvider.GetRequiredService<IOptions<LcmCrdtConfig>>().Value.ProjectPath;
if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true);
Directory.CreateDirectory(crdtProjectsFolder);
var crdtProject = await _services.ServiceProvider.GetRequiredService<ProjectsService>()
var crdtProject = await _services.ServiceProvider.GetRequiredService<CrdtProjectsService>()
.CreateProject(new(_projectName, FwProjectId: FwDataApi.ProjectId, SeedNewProjectData: false));
CrdtApi = (CrdtMiniLcmApi) await _services.ServiceProvider.OpenCrdtProject(crdtProject);
}
Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/FwLiteProjectSync/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static Task<int> Main(string[] args)
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<Program>>();
var fwdataApi = services.GetRequiredService<FwDataFactory>().GetFwDataMiniLcmApi(fwProjectName, true);
var projectsService = services.GetRequiredService<ProjectsService>();
var projectsService = services.GetRequiredService<CrdtProjectsService>();
var crdtProject = projectsService.GetProject(crdtProjectName);
if (crdtProject is null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using LcmCrdt;

namespace LocalWebApp.Auth;
namespace FwLiteShared.Auth;

public class AuthConfig
{
Expand Down
44 changes: 44 additions & 0 deletions backend/FwLite/FwLiteShared/Auth/AuthService.cs
Original file line number Diff line number Diff line change
@@ -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<AuthConfig> options)
{
public IAsyncEnumerable<ServerStatus> 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<string> 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<string?> GetLoggedInName(LexboxServer server)
{
return await clientFactory.GetClient(server).GetCurrentName();
}
}
7 changes: 7 additions & 0 deletions backend/FwLite/FwLiteShared/Auth/IRedirectUrlProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace FwLiteShared.Auth;

public interface IRedirectUrlProvider
{
string? GetRedirectUrl();
bool ShouldRecreateAuthHelper(string? redirectUrl);
}
Original file line number Diff line number Diff line change
@@ -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<LoggerAdapter> logger): IIdentityLogger
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,56 @@
using System.Net.Http.Headers;
using System.Security.Cryptography;
using LocalWebApp.Routes;
using LocalWebApp.Services;
using FwLiteShared.Projects;
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;

/// <summary>
/// when injected directly it will use the authority of the current project, to get a different authority use <see cref="AuthHelpersFactory"/>
/// when injected directly it will use the authority of the current project, to get a different authority use <see cref="OAuthClientFactory"/>
/// helper class for using MSAL.net
/// docs: https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/overview
/// </summary>
public class AuthHelpers
public class OAuthClient
{
public static IReadOnlyCollection<string> 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<AuthHelpers> _logger;
private readonly ILogger<OAuthClient> _logger;
private readonly IPublicClientApplication _application;
AuthenticationResult? _authResult;

public AuthHelpers(LoggerAdapter loggerAdapter,
public OAuthClient(LoggerAdapter loggerAdapter,
IHttpMessageHandlerFactory httpMessageHandlerFactory,
IOptions<AuthConfig> options,
LinkGenerator linkGenerator,
IRedirectUrlProvider? redirectUrlProvider,
OAuthService oAuthService,
UrlContext urlContext,
LexboxServer lexboxServer,
LexboxProjectService lexboxProjectService,
ILogger<AuthHelpers> logger,
ILogger<OAuthClient> logger,
IHostEnvironment hostEnvironment)
{
_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(
Expand Down Expand Up @@ -99,11 +90,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
{
Expand Down Expand Up @@ -178,10 +164,10 @@ await _application
return auth?.AccessToken;
}

/// <summary>
/// <summary>]
/// will return null if no auth token is available
/// </summary>
public async ValueTask<HttpClient?> CreateClient()
public async ValueTask<HttpClient?> CreateHttpClient()
{
var auth = await GetAuth();
if (auth is null) return null;
Expand Down
47 changes: 47 additions & 0 deletions backend/FwLite/FwLiteShared/Auth/OAuthClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Concurrent;
using LcmCrdt;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace FwLiteShared.Auth;

public class OAuthClientFactory(IServiceProvider provider,
IOptions<AuthConfig> options,
IRedirectUrlProvider? redirectUrlProvider,
ILogger<OAuthClientFactory> logger)
{
private readonly ConcurrentDictionary<string, OAuthClient> _helpers = new();

private string AuthorityKey(LexboxServer server) => "AuthHelper|" + server.Authority.Authority;

/// <summary>
/// gets an Auth Helper for the given server
/// </summary>
public OAuthClient GetClient(LexboxServer server)
{
var helper = _helpers.GetOrAdd(AuthorityKey(server),
static (host, arg) => ActivatorUtilities.CreateInstance<OAuthClient>(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
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 GetClient(server);
}

return helper;
}

/// <summary>
/// get auth helper for a given project
/// </summary>
public OAuthClient GetClient(ProjectData project)
{
var originDomain = project.OriginDomain;
if (string.IsNullOrEmpty(originDomain)) throw new InvalidOperationException("No origin domain in project data");
return GetClient(options.Value.GetServer(project));
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -36,7 +37,7 @@ public async Task<SignInResult> 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);
Expand Down Expand Up @@ -69,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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace LocalWebApp.Utils;
namespace FwLiteShared;

public static class CancellationTokenExtensions
{
Expand Down
Loading

0 comments on commit 8b4f158

Please sign in to comment.