using System.Net.Http.Json; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using QMax.Api.Configuration; using QMax.Api.Infrastructure.Max; namespace QMax.Api.Controllers; [ApiController] [AllowAnonymous] [Route("admin/max")] public sealed class AdminMaxController( IHttpClientFactory httpClientFactory, IOptions options) : ControllerBase { private readonly QMaxOptions _options = options.Value; [HttpGet] public async Task Index([FromQuery] string pairingCode, CancellationToken cancellationToken) { if (!IsPairingCodeValid(pairingCode)) { return Content(AdminShell("

QMAX MAX Admin

Open this page with ?pairingCode=....

"), "text/html", Encoding.UTF8); } var snapshot = await WorkerGetAsync("snapshot", cancellationToken); var status = await WorkerGetAsync("status", cancellationToken); var screenshot = string.IsNullOrWhiteSpace(snapshot?.ScreenshotPngBase64) ? "

No screenshot yet.

" : $""; var encodedPairingCode = Uri.EscapeDataString(pairingCode); var jsAuthorized = status?.IsAuthorized == true ? "true" : "false"; var jsStage = JsonSerializer.Serialize(status?.LoginStage ?? ""); var html = $$"""

QMAX MAX Admin

Status: {{Html(status?.Status)}} | Stage: {{Html(status?.LoginStage)}} | Authorized: {{status?.IsAuthorized}} | URL: {{Html(snapshot?.Url)}}

Click directly on the screenshot to control the remote MAX browser. QMAX will forward the click and refresh the image.

{{screenshot}}
"""; return Content(AdminShell(html), "text/html", Encoding.UTF8); } [HttpGet("inspect/{kind}")] public async Task Inspect([FromRoute] string kind, [FromQuery] string pairingCode, CancellationToken cancellationToken) { if (!IsPairingCodeValid(pairingCode)) return Unauthorized(); var workerPath = kind.ToLowerInvariant() switch { "dom" => "inspect/dom", "storage" => "inspect/storage", "indexeddb-sample" => "inspect/indexeddb/sample", "network" => "inspect/network", _ => null }; if (workerPath is null) return NotFound(); return Content(await WorkerGetRawAsync(workerPath, cancellationToken), "application/json", Encoding.UTF8); } [HttpGet("updates")] public async Task Updates([FromQuery] string pairingCode, CancellationToken cancellationToken) { if (!IsPairingCodeValid(pairingCode)) return Unauthorized(); return Content(await WorkerGetRawAsync("updates", cancellationToken), "application/json", Encoding.UTF8); } [HttpPost("login/start")] public async Task StartLogin([FromForm] string pairingCode, CancellationToken cancellationToken) { if (!IsPairingCodeValid(pairingCode)) return Unauthorized(); await WorkerPostAsync("login/start", new { phoneNumber = _options.MaxPhoneNumber }, cancellationToken); return Redirect($"/admin/max?pairingCode={Uri.EscapeDataString(pairingCode)}"); } [HttpPost("login/code")] public async Task Code([FromForm] string pairingCode, [FromForm] string code, CancellationToken cancellationToken) { if (!IsPairingCodeValid(pairingCode)) return Unauthorized(); await WorkerPostAsync("login/code", new { code, waitMs = 60000 }, cancellationToken); return Redirect($"/admin/max?pairingCode={Uri.EscapeDataString(pairingCode)}"); } [HttpPost("click")] public async Task Click([FromForm] string pairingCode, [FromForm] int x, [FromForm] int y, CancellationToken cancellationToken) { if (!IsPairingCodeValid(pairingCode)) return Unauthorized(); await WorkerPostAsync("browser/click", new { x, y }, cancellationToken); return Redirect($"/admin/max?pairingCode={Uri.EscapeDataString(pairingCode)}"); } [HttpPost("type")] public async Task Type([FromForm] string pairingCode, [FromForm] string text, CancellationToken cancellationToken) { if (!IsPairingCodeValid(pairingCode)) return Unauthorized(); await WorkerPostAsync("browser/type", new { text }, cancellationToken); return Redirect($"/admin/max?pairingCode={Uri.EscapeDataString(pairingCode)}"); } [HttpPost("press")] public async Task Press([FromForm] string pairingCode, [FromForm] string key, CancellationToken cancellationToken) { if (!IsPairingCodeValid(pairingCode)) return Unauthorized(); await WorkerPostAsync("browser/press", new { key = string.IsNullOrWhiteSpace(key) ? "Enter" : key }, cancellationToken); return Redirect($"/admin/max?pairingCode={Uri.EscapeDataString(pairingCode)}"); } private async Task WorkerGetAsync(string path, CancellationToken cancellationToken) { var client = httpClientFactory.CreateClient(); client.BaseAddress = new Uri(_options.MaxWorkerBaseUrl.TrimEnd('/') + "/"); return await client.GetFromJsonAsync(path, cancellationToken); } private async Task WorkerGetRawAsync(string path, CancellationToken cancellationToken) { var client = httpClientFactory.CreateClient(); client.BaseAddress = new Uri(_options.MaxWorkerBaseUrl.TrimEnd('/') + "/"); return await client.GetStringAsync(path, cancellationToken); } private async Task WorkerPostAsync(string path, object body, CancellationToken cancellationToken) { var client = httpClientFactory.CreateClient(); client.BaseAddress = new Uri(_options.MaxWorkerBaseUrl.TrimEnd('/') + "/"); using var response = await client.PostAsJsonAsync(path, body, cancellationToken); response.EnsureSuccessStatusCode(); } private bool IsPairingCodeValid(string pairingCode) { return !string.IsNullOrWhiteSpace(_options.PairingCode) && string.Equals(pairingCode, _options.PairingCode, StringComparison.Ordinal); } private static string AdminShell(string body) { return $$""" QMAX MAX Admin
{{body}}
"""; } private static string Html(string? value) { return System.Net.WebUtility.HtmlEncode(value ?? ""); } }