Add multi-user MAX authentication and tenant isolation

This commit is contained in:
Курнат Андрей
2026-07-14 07:35:04 +03:00
parent 582f99ed0e
commit 440de7325f
36 changed files with 904 additions and 118 deletions
@@ -4,6 +4,8 @@ public interface IMaxBridgeClient
{
Task<MaxBridgeStatus> GetStatusAsync(CancellationToken cancellationToken);
Task<MaxBridgeStatus> BeginPhoneLoginAsync(string phoneNumber, CancellationToken cancellationToken);
Task<MaxBridgeStatus> BeginNewPhoneLoginAsync(string phoneNumber, CancellationToken cancellationToken) =>
BeginPhoneLoginAsync(phoneNumber, cancellationToken);
Task<MaxBridgeStatus> SubmitLoginCodeAsync(string code, CancellationToken cancellationToken);
Task<MaxBrowserSnapshot> GetSnapshotAsync(CancellationToken cancellationToken);
Task<IReadOnlyList<MaxChatUpdate>> FetchUpdatesAsync(CancellationToken cancellationToken);
@@ -45,6 +45,9 @@ public sealed class MockMaxBridgeClient : IMaxBridgeClient
return Task.FromResult<IReadOnlyList<MaxChatUpdate>>(_updates);
}
public Task<MaxBridgeStatus> BeginNewPhoneLoginAsync(string phoneNumber, CancellationToken cancellationToken) =>
BeginPhoneLoginAsync(phoneNumber, cancellationToken);
public Task<IReadOnlyList<MaxContact>> FetchContactsAsync(CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<MaxContact>>([
@@ -2,12 +2,14 @@ using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Options;
using QMax.Api.Configuration;
using QMax.Api.Infrastructure.Auth;
namespace QMax.Api.Infrastructure.Max;
public sealed class WorkerMaxBridgeClient(
HttpClient httpClient,
IOptions<QMaxOptions> options,
ICurrentUserAccessor currentUser,
ILogger<WorkerMaxBridgeClient> logger) : IMaxBridgeClient
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
@@ -21,7 +23,17 @@ public sealed class WorkerMaxBridgeClient(
public async Task<MaxBridgeStatus> BeginPhoneLoginAsync(string phoneNumber, CancellationToken cancellationToken)
{
return await SendAsync<MaxBridgeStatus>(HttpMethod.Post, "/login/start", new { phoneNumber }, cancellationToken)
return await BeginPhoneLoginCoreAsync(phoneNumber, forceNewSession: false, cancellationToken);
}
public async Task<MaxBridgeStatus> BeginNewPhoneLoginAsync(string phoneNumber, CancellationToken cancellationToken)
{
return await BeginPhoneLoginCoreAsync(phoneNumber, forceNewSession: true, cancellationToken);
}
private async Task<MaxBridgeStatus> BeginPhoneLoginCoreAsync(string phoneNumber, bool forceNewSession, CancellationToken cancellationToken)
{
return await SendAsync<MaxBridgeStatus>(HttpMethod.Post, "/login/start", new { phoneNumber, force = forceNewSession }, cancellationToken)
?? ErrorStatus("Worker returned an empty login status.");
}
@@ -171,7 +183,9 @@ public sealed class WorkerMaxBridgeClient(
{
httpClient.BaseAddress ??= new Uri(_options.MaxWorkerBaseUrl.TrimEnd('/') + "/");
var path = $"media/fetch?url={Uri.EscapeDataString(remoteUrl)}";
var response = await httpClient.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
using var request = new HttpRequestMessage(HttpMethod.Get, path);
AddAccountHeader(request);
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(cancellationToken);
@@ -217,6 +231,7 @@ public sealed class WorkerMaxBridgeClient(
{
httpClient.BaseAddress ??= new Uri(_options.MaxWorkerBaseUrl.TrimEnd('/') + "/");
using var request = new HttpRequestMessage(method, path.TrimStart('/'));
AddAccountHeader(request);
if (body is not null)
{
request.Content = JsonContent.Create(body, options: JsonOptions);
@@ -239,6 +254,15 @@ public sealed class WorkerMaxBridgeClient(
}
}
private void AddAccountHeader(HttpRequestMessage request)
{
if (currentUser.UserId is not { } userId)
{
throw new InvalidOperationException("A QMAX user context is required for a PyMax request.");
}
request.Headers.Add("X-QMax-Account-Id", userId.ToString("N"));
}
private static MaxBridgeStatus ErrorStatus(string error)
{
return new MaxBridgeStatus("Worker", false, "Unavailable", "WorkerUnavailable", null, null, error, DateTimeOffset.UtcNow);