Skip to content

Commit

Permalink
Update crdt project list API to show all user projects to allow them …
Browse files Browse the repository at this point in the history
…to upload (#1248)

* rework how AuthHelpers.cs get made using a LexboxServer instead of a Uri

* move user projects query into ProjectService.cs

* allow user to choose which project to upload to from FW Lite

* simplify matching of local and remote projects

* show a message about being required to login again when your login has expired for a project that's already syncing


* always show crdt column

* set project OriginDomain to null if the initial sync fails

* Prevent simultaneous imports
This is already partially assumed in the code.

* Handle failed upload

---------

Co-authored-by: Tim Haasdyk <[email protected]>
  • Loading branch information
hahn-kev and myieye authored Nov 19, 2024
1 parent 77f6b3c commit 412aa71
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 108 deletions.
4 changes: 2 additions & 2 deletions backend/FwLite/LcmCrdt/CurrentProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async ValueTask<ProjectData> GetProjectData()

private static string CacheKey(CrdtProject project)
{
return project.DbPath + "|ProjectData";
return project.Name + "|ProjectData";
}

private static string CacheKey(Guid projectId)
Expand Down Expand Up @@ -57,7 +57,7 @@ private void RemoveProjectDataCache()
memoryCache.Remove(CacheKey(Project));
}

public async Task SetProjectSyncOrigin(Uri domain, Guid? id)
public async Task SetProjectSyncOrigin(Uri? domain, Guid? id)
{
var originDomain = ProjectData.GetOriginDomain(domain);
if (id is null)
Expand Down
8 changes: 8 additions & 0 deletions backend/FwLite/LocalWebApp/Auth/AuthConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using LcmCrdt;

namespace LocalWebApp.Auth;

Expand All @@ -15,6 +16,13 @@ public LexboxServer GetServerByAuthority(string authority)
{
return LexboxServers.FirstOrDefault(s => s.Authority.Authority == authority) ?? throw new ArgumentException($"Server {authority} not found");
}

public LexboxServer GetServer(ProjectData projectData)
{
var originDomain = projectData.OriginDomain;
if (string.IsNullOrEmpty(originDomain)) throw new InvalidOperationException("No origin domain in project data");
return GetServerByAuthority(new Uri(originDomain).Authority);
}
public LexboxServer GetServer(string serverName)
{
return LexboxServers.FirstOrDefault(s => s.DisplayName == serverName) ?? throw new ArgumentException($"Server {serverName} not found");
Expand Down
25 changes: 18 additions & 7 deletions backend/FwLite/LocalWebApp/Auth/AuthHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Net.Http.Headers;
using System.Security.Cryptography;
using LocalWebApp.Routes;
using LocalWebApp.Services;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
Expand All @@ -21,7 +22,8 @@ public class AuthHelpers
private readonly IHttpMessageHandlerFactory _httpMessageHandlerFactory;
private readonly OAuthService _oAuthService;
private readonly UrlContext _urlContext;
private readonly Uri _authority;
private readonly LexboxServer _lexboxServer;
private readonly LexboxProjectService _lexboxProjectService;
private readonly ILogger<AuthHelpers> _logger;
private readonly IPublicClientApplication _application;
AuthenticationResult? _authResult;
Expand All @@ -32,14 +34,16 @@ public AuthHelpers(LoggerAdapter loggerAdapter,
LinkGenerator linkGenerator,
OAuthService oAuthService,
UrlContext urlContext,
Uri authority,
LexboxServer lexboxServer,
LexboxProjectService lexboxProjectService,
ILogger<AuthHelpers> logger,
IHostEnvironment hostEnvironment)
{
_httpMessageHandlerFactory = httpMessageHandlerFactory;
_oAuthService = oAuthService;
_urlContext = urlContext;
_authority = authority;
_lexboxServer = lexboxServer;
_lexboxProjectService = lexboxProjectService;
_logger = logger;
(var hostUrl, _isRedirectHostGuess) = urlContext.GetUrl();
_redirectHost = HostString.FromUriComponent(hostUrl);
Expand All @@ -56,7 +60,7 @@ public AuthHelpers(LoggerAdapter loggerAdapter,
.WithLogging(loggerAdapter, hostEnvironment.IsDevelopment())
.WithHttpClientFactory(new HttpClientFactoryAdapter(httpMessageHandlerFactory))
.WithRedirectUri(redirectUri)
.WithOidcAuthority(authority.ToString())
.WithOidcAuthority(lexboxServer.Authority.ToString())
.Build();
_ = MsalCacheHelper.CreateAsync(BuildCacheProperties(options.Value.CacheFileName)).ContinueWith(
task =>
Expand Down Expand Up @@ -109,9 +113,10 @@ public HttpClient GetHttpClient()
}
}

public async Task<OAuthService.SignInResult> SignIn(CancellationToken cancellation = default)
public async Task<OAuthService.SignInResult> SignIn(string returnUrl, CancellationToken cancellation = default)
{
return await _oAuthService.SubmitLoginRequest(_application, cancellation);
InvalidateProjectCache();
return await _oAuthService.SubmitLoginRequest(_application, returnUrl, cancellation);
}

public async Task Logout()
Expand All @@ -122,6 +127,12 @@ public async Task Logout()
{
await _application.RemoveAsync(account);
}
InvalidateProjectCache();
}

private void InvalidateProjectCache()
{
_lexboxProjectService.InvalidateProjectsCache(_lexboxServer);
}

private async ValueTask<AuthenticationResult?> GetAuth()
Expand Down Expand Up @@ -177,7 +188,7 @@ await _application

var handler = _httpMessageHandlerFactory.CreateHandler(AuthHttpClientName);
var client = new HttpClient(handler, false);
client.BaseAddress = _authority;
client.BaseAddress = _lexboxServer.Authority;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", auth.AccessToken);
return client;
}
Expand Down
28 changes: 12 additions & 16 deletions backend/FwLite/LocalWebApp/Auth/AuthHelpersFactory.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
using System.Collections.Concurrent;
using LcmCrdt;
using LocalWebApp.Services;
using Microsoft.Extensions.Options;

namespace LocalWebApp.Auth;

public class AuthHelpersFactory(
IServiceProvider provider,
ProjectContext projectContext,
IOptions<AuthConfig> options,
IHttpContextAccessor contextAccessor)
{
private readonly ConcurrentDictionary<string, AuthHelpers> _helpers = new();

private string AuthorityKey(Uri authority) =>
authority.GetComponents(UriComponents.HostAndPort, UriFormat.Unescaped);
private string AuthorityKey(LexboxServer server) => "AuthHelper|" + server.Authority.Authority;

/// <summary>
/// gets an Auth Helper for the given authority
/// gets an Auth Helper for the given server
/// </summary>
/// <param name="authority">should include scheme, host and port, no path</param>
public AuthHelpers GetHelper(Uri authority)
public AuthHelpers GetHelper(LexboxServer server)
{
var helper = _helpers.GetOrAdd(AuthorityKey(authority),
static (host, arg) => ActivatorUtilities.CreateInstance<AuthHelpers>(arg.provider, arg.authority),
(authority, provider));
var helper = _helpers.GetOrAdd(AuthorityKey(server),
static (host, arg) => ActivatorUtilities.CreateInstance<AuthHelpers>(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())
{
_helpers.TryRemove(AuthorityKey(authority), out _);
return GetHelper(authority);
_helpers.TryRemove(AuthorityKey(server), out _);
return GetHelper(server);
}

return helper;
Expand All @@ -40,12 +41,7 @@ public AuthHelpers GetHelper(ProjectData project)
{
var originDomain = project.OriginDomain;
if (string.IsNullOrEmpty(originDomain)) throw new InvalidOperationException("No origin domain in project data");
return GetHelper(new Uri(originDomain));
}

public AuthHelpers GetHelper(LexboxServer server)
{
return GetHelper(server.Authority);
return GetHelper(options.Value.GetServer(project));
}

/// <summary>
Expand Down
16 changes: 11 additions & 5 deletions backend/FwLite/LocalWebApp/Auth/OAuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ namespace LocalWebApp.Auth;
public class OAuthService(ILogger<OAuthService> logger, IHostApplicationLifetime applicationLifetime, IOptions<AuthConfig> options) : BackgroundService
{
public record SignInResult(Uri? AuthUri, bool HandledBySystemWebView);
public async Task<SignInResult> SubmitLoginRequest(IPublicClientApplication application, CancellationToken cancellation)
public async Task<SignInResult> SubmitLoginRequest(IPublicClientApplication application,
string returnUrl,
CancellationToken cancellation)
{
if (options.Value.SystemWebViewLogin)
{
await HandleSystemWebViewLogin(application, cancellation);
return new(null, true);
}
var request = new OAuthLoginRequest(application);
var request = new OAuthLoginRequest(application, returnUrl);
if (!_requestChannel.Writer.TryWrite(request))
{
throw new InvalidOperationException("Only one request at a time");
Expand All @@ -40,15 +42,15 @@ private async Task HandleSystemWebViewLogin(IPublicClientApplication application
.ExecuteAsync(cancellation);
}

public async Task<AuthenticationResult> FinishLoginRequest(Uri uri, CancellationToken cancellation = default)
public async Task<(AuthenticationResult, string ClientReturnUrl)> FinishLoginRequest(Uri uri, CancellationToken cancellation = default)
{
var queryString = HttpUtility.ParseQueryString(uri.Query);
var state = queryString.Get("state") ?? throw new InvalidOperationException("State is null");
if (!_oAuthLoginRequests.TryGetValue(state, out var request))
throw new InvalidOperationException("Invalid state");
//step 5
request.SetReturnUri(uri);
return await request.GetAuthenticationResult(applicationLifetime.ApplicationStopping.Merge(cancellation));
return (await request.GetAuthenticationResult(applicationLifetime.ApplicationStopping.Merge(cancellation)), request.ClientReturnUrl);
//step 8
}

Expand Down Expand Up @@ -90,7 +92,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
/// instead we have to do this so we can use the currently open browser, redirect it to the auth url passed in here and then once it's done and the callback comes to our server,
/// send that call to here so that MSAL can pull out the access token
/// </summary>
public class OAuthLoginRequest(IPublicClientApplication app) : ICustomWebUi
public class OAuthLoginRequest(IPublicClientApplication app, string clientReturnUrl) : ICustomWebUi
{
public IPublicClientApplication Application { get; } = app;
public string? State { get; private set; }
Expand Down Expand Up @@ -124,4 +126,8 @@ public void SetException(Exception e)
}

public Task<AuthenticationResult> GetAuthenticationResult(CancellationToken cancellation) => _resultTcs.Task.WaitAsync(cancellation);
/// <summary>
/// url to return the client to once the login is finished
/// </summary>
public string ClientReturnUrl { get; } = clientReturnUrl;
}
2 changes: 1 addition & 1 deletion backend/FwLite/LocalWebApp/LocalAppKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static IServiceCollection AddLocalAppServices(this IServiceCollection ser
services.AddAuthHelpers(environment);
services.AddSingleton<UrlContext>();
services.AddScoped<SyncService>();
services.AddScoped<LexboxProjectService>();
services.AddSingleton<LexboxProjectService>();
services.AddSingleton<ChangeEventBus>();
services.AddSingleton<ImportFwdataService>();
services.AddSingleton<BackgroundSyncService>();
Expand Down
12 changes: 7 additions & 5 deletions backend/FwLite/LocalWebApp/Routes/AuthRoutes.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Security.AccessControl;
using System.Web;
using LocalWebApp.Auth;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace LocalWebApp.Routes;
Expand All @@ -23,12 +24,13 @@ public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app)
});
});
group.MapGet("/login/{authority}",
async (AuthHelpersFactory factory, string authority, IOptions<AuthConfig> options) =>
async (AuthHelpersFactory factory, string authority, IOptions<AuthConfig> options, [FromHeader] string referer) =>
{
var result = await factory.GetHelper(options.Value.GetServerByAuthority(authority)).SignIn();
var returnUrl = new Uri(referer).PathAndQuery;
var result = await factory.GetHelper(options.Value.GetServerByAuthority(authority)).SignIn(returnUrl);
if (result.HandledBySystemWebView)
{
return Results.Redirect("/");
return Results.Redirect(returnUrl);
}

if (result.AuthUri is null) throw new InvalidOperationException("AuthUri is null");
Expand All @@ -43,8 +45,8 @@ public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app)
context.Request.Path);
uriBuilder.Query = context.Request.QueryString.ToUriComponent();

await oAuthService.FinishLoginRequest(uriBuilder.Uri);
return Results.Redirect("/");
var (_, returnUrl) = await oAuthService.FinishLoginRequest(uriBuilder.Uri);
return Results.Redirect(returnUrl);
}).WithName(CallbackRoute);
group.MapGet("/me/{authority}",
async (AuthHelpersFactory factory, string authority, IOptions<AuthConfig> options) =>
Expand Down
26 changes: 17 additions & 9 deletions backend/FwLite/LocalWebApp/Routes/ProjectRoutes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using LocalWebApp.Auth;
using LocalWebApp.Hubs;
using LocalWebApp.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MiniLcm;
using MiniLcm.Models;
Expand All @@ -24,7 +25,9 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap
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, false, false, true, server.Authority.Authority, p.Id)).ToArray());
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;
Expand Down Expand Up @@ -77,16 +80,21 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap
SyncService syncService,
IOptions<AuthConfig> options,
CurrentProjectService currentProjectService,
string serverAuthority) =>
string serverAuthority,
[FromQuery] Guid lexboxProjectId) =>
{
var server = options.Value.GetServerByAuthority(serverAuthority);
var foundProjectGuid =
await lexboxProjectService.GetLexboxProjectId(server, currentProjectService.ProjectData.Name);
if (foundProjectGuid is null)
return Results.BadRequest(
$"Project code {currentProjectService.ProjectData.Name} not found on lexbox");
await currentProjectService.SetProjectSyncOrigin(server.Authority, foundProjectGuid);
await syncService.ExecuteSync();
await currentProjectService.SetProjectSyncOrigin(server.Authority, lexboxProjectId);
try
{
await syncService.ExecuteSync();
}
catch
{
await currentProjectService.SetProjectSyncOrigin(null, null);
throw;
}
lexboxProjectService.InvalidateProjectsCache(server);
return TypedResults.Ok();
});
group.MapPost("/download/crdt/{serverAuthority}/{newProjectName}",
Expand Down
Loading

0 comments on commit 412aa71

Please sign in to comment.