Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create fw lite shared #1285

Merged
merged 14 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -28,7 +28,7 @@
var crdtProjectsFolder =
rootServiceProvider.GetRequiredService<IOptions<LcmCrdtConfig>>().Value.ProjectPath;
if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true);
rootServiceProvider.Dispose();

Check warning on line 31 in backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

Dispose synchronously blocks. Await DisposeAsync instead. (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD103.md)

Directory.CreateDirectory(crdtProjectsFolder);
await DownloadSena3();
Expand All @@ -54,7 +54,7 @@
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
Loading